-
Notifications
You must be signed in to change notification settings - Fork 1
/
DependencyContext.cs
607 lines (512 loc) · 21.5 KB
/
DependencyContext.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
namespace AncientLightStudios.Nanoject
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using JetBrains.Annotations;
using UnityEngine;
public sealed class DependencyContext
{
private readonly HolderByTypeAndQualifier<object> _resolvedComponents =
new HolderByTypeAndQualifier<object>();
private readonly HolderByTypeAndQualifier<DependencySet> _collections =
new HolderByTypeAndQualifier<DependencySet>();
private readonly HolderByTypeAndQualifier<UnresolvedComponent> _unresolvedComponents =
new HolderByTypeAndQualifier<UnresolvedComponent>();
public bool IsResolved => _unresolvedComponents.Count == 0;
public DependencyContext()
{
Declare(this);
}
public void DeclareQualified<T>(string qualifier, T instance = default)
{
DeclareInternal(typeof(T), instance, qualifier);
}
public void Declare<T>(T instance = default)
{
DeclareInternal(typeof(T), instance);
}
public void DeclareQualified(Type type, string qualifier, object instance = default)
{
DeclareInternal(type, instance, qualifier);
}
public void Declare(Type type, object instance = default)
{
DeclareInternal(type, instance);
}
private void DeclareInternal(Type type, object instance = default, string qualifier = "")
{
var unresolvedDependency =
instance == default ? new UnresolvedComponent(type) : new UnresolvedComponent(instance);
_unresolvedComponents.Put(type, qualifier, unresolvedDependency);
}
public T Get<T>(string qualifier = "")
{
if (!IsResolved)
{
throw new InvalidOperationException("This dependency context is not resolved.");
}
if (_resolvedComponents.TryGetUniqueValue(typeof(T), qualifier, out var result))
{
return (T) result;
}
throw new InvalidOperationException(
$"No unique dependency of type {typeof(T)} with qualifier '{qualifier}'.");
}
public List<T> GetAll<T>()
{
return _resolvedComponents.Where((type, qualifier) => typeof(T).IsAssignableFrom(type)).Cast<T>().ToList();
}
public void Resolve()
{
bool ResolveUnique(Type type, string qualifier, out object result)
{
if (_resolvedComponents
.Where((itsType, itsQualifier) => type.IsAssignableFrom(itsType) && itsQualifier == qualifier)
.UniqueMatch(out result))
{
return true;
}
if (Utils.IsCollectionType(type, out var collectionContentType))
{
// first check if there are any unresolved components of the given collection content type
// or an assignable type. we need to do this to make sure we don't inject a half-finished collection
// into a newly created dependency. We can only create collections after all components that
// make up the collection are resolved.
if (!_unresolvedComponents.Where((itsType, _) => collectionContentType.IsAssignableFrom(itsType))
.Any())
{
// if we already built a collection for this type and qualifier, use this.
if (!_collections.Where((itsType, itsQualifier) =>
collectionContentType == itsType && qualifier == itsQualifier).UniqueMatch(out var set))
{
// otherwise build a new one
set = Utils.MakeDependencySet(collectionContentType);
_collections.Put(type, qualifier, set);
}
// make sure the set contains all known matching components. Since it is a set, it doesn't
// matter if we add a component more than once.
foreach (var item in _resolvedComponents.Where((itsType, itsQualifier) =>
collectionContentType.IsAssignableFrom(itsType)
&& (qualifier == "" || qualifier == itsQualifier))
)
{
set.Add(item);
}
result = set;
return true;
}
}
result = default;
return false;
}
bool hasProgress;
do
{
hasProgress = false;
// try a round of resolving stuff.
_unresolvedComponents.ForEach((unresolvedType, unresolvedQualifier, unresolvedComponent) =>
{
if (!unresolvedComponent.TryResolve(ResolveUnique, out var instance))
{
return;
}
// we have resolved it, so we can add it to the known dependencies
_resolvedComponents.Put(unresolvedType, unresolvedQualifier, instance);
// and remove it from the unresolved dependencies
_unresolvedComponents.Remove(unresolvedType, unresolvedQualifier, unresolvedComponent);
hasProgress = true;
});
} while (hasProgress);
if (_unresolvedComponents.Count <= 0)
{
return;
}
_unresolvedComponents.ForEach((itsType, itsQualifier, item) =>
{
var actualQualifier = itsQualifier == "" ? "<no qualifier>" : itsQualifier;
Debug.Log($"[{actualQualifier}] {itsType.FullName}\n{item}");
});
throw new InvalidOperationException("There are unresolved dependencies left!");
}
}
internal abstract class DependencySet
{
public abstract void Add(object toAdd);
}
internal class TypedDependencySet<T> : DependencySet, IReadOnlyCollection<T>
{
private readonly HashSet<T> _set = new HashSet<T>();
public override void Add(object toAdd)
{
_set.Add((T) toAdd);
}
IEnumerator IEnumerable.GetEnumerator()
{
return _set.GetEnumerator();
}
public IEnumerator<T> GetEnumerator()
{
return _set.GetEnumerator();
}
public int Count => _set.Count;
}
/// <summary>
/// Various helper functions that don't fit anywhere else.
/// </summary>
internal static class Utils
{
/// <summary>
/// The supported collection type.
/// </summary>
private static readonly Type CollectionType = typeof(IReadOnlyCollection<>);
/// <summary>
/// An empty list, so we don't need to create unnecessary instances.
/// </summary>
public static readonly ICollection<string> EmptyList = new string[0];
/// <summary>
/// Checks if the given type is a supported collection type for injection. Supported is any <see cref="IReadOnlyCollection{T}"/>.
/// </summary>
/// <param name="type">the type argument</param>
/// <param name="collectionContentType">the content type of the collection (e.g. what is in the collection).</param>
/// <returns></returns>
public static bool IsCollectionType(Type type, out Type collectionContentType)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == CollectionType)
{
collectionContentType = type.GetGenericArguments()[0];
return true;
}
collectionContentType = default;
return false;
}
/// <summary>
/// Create a set to hold dependencies of the given type.
/// </summary>
/// <param name="t">The type that the set should hold.</param>
/// <returns>a DependencySet that can hold dependencies of the given type.</returns>
public static DependencySet MakeDependencySet(Type t)
{
return (DependencySet) Activator.CreateInstance(typeof(TypedDependencySet<>).MakeGenericType(t));
}
/// <summary>
/// Returns true if the given enumerable only contains one element, false otherwise. Returns the single element
/// inside the enumerable.
/// </summary>
public static bool UniqueMatch<T>(this IEnumerable<T> input, out T result)
{
var list = input.ToList();
if (list.Count != 1)
{
result = default;
return false;
}
result = list[0];
return true;
}
}
/// <summary>
/// Helper class for our nested dictionary structures to avoid some typing.
/// </summary>
/// <typeparam name="T"></typeparam>
internal class HolderByTypeAndQualifier<T>
{
private readonly Dictionary<Type, Dictionary<string, HashSet<T>>> _value =
new Dictionary<Type, Dictionary<string, HashSet<T>>>();
public int Count => _value.Count;
public IEnumerable<T> Where(Func<Type, string, bool> filter)
{
return _value.SelectMany(outer => outer.Value, (outer, inner) => new {outer, inner})
.Where(t => filter(t.outer.Key, t.inner.Key))
.SelectMany(t => t.inner.Value);
}
public void ForEach(Action<Type, string, T> handler)
{
foreach (var outer in _value.ToList())
{
foreach (var inner in outer.Value.ToList())
{
foreach (var item in inner.Value.ToList())
{
handler(outer.Key, inner.Key, item);
}
}
}
}
public void Put(Type type, string qualifier, T value)
{
if (!_value.TryGetValue(type, out var byQualifier))
{
byQualifier = new Dictionary<string, HashSet<T>>();
_value[type] = byQualifier;
}
if (!byQualifier.TryGetValue(qualifier, out var set))
{
set = new HashSet<T>();
byQualifier[qualifier] = set;
}
set.Add(value);
}
public bool TryGetValue(Type type, string qualifier, out HashSet<T> result)
{
if (_value.TryGetValue(type, out var byQualifier))
{
return byQualifier.TryGetValue(qualifier, out result);
}
result = default;
return false;
}
public bool TryGetUniqueValue(Type type, string qualifier, out T result)
{
if (TryGetValue(type, qualifier, out var set))
{
if (set.Count == 1)
{
result = set.First();
return true;
}
}
result = default;
return false;
}
public void Remove(Type type, string qualifier, T value)
{
if (!_value.TryGetValue(type, out var byQualifier))
{
return;
}
if (!byQualifier.TryGetValue(qualifier, out var set))
{
return;
}
set.Remove(value);
if (set.Count == 0)
{
byQualifier.Remove(qualifier);
}
if (byQualifier.Count == 0)
{
_value.Remove(type);
}
}
}
/// <summary>
/// Class representing a component that still needs to be resolved. Using this class allows to cache introspection
/// results and do a gradual resolve while saving intermediate resolution steps. This makes the whole process faster
/// and also allows for better debugging.
/// </summary>
internal class UnresolvedComponent
{
public delegate bool UniqueResolver(Type type, string qualifier, out object result);
/// <summary>
/// The existing instance of the dependency. Only set for objects with [LateInit] initializers.
/// </summary>
[CanBeNull] private readonly object _existingInstance;
/// <summary>
/// The initializer that should be used to initialize the instance (either a constructor or a [LateInit] method).
/// For dependencies with existing instances this can be null if the dependency has no [LateInit] method.
/// </summary>
[CanBeNull] private readonly MethodBase _initializer;
/// <summary>
/// The types of all parameters to be resolved.
/// </summary>
private readonly Type[] _types;
/// <summary>
/// The qualifiers of all parameters to be resolved.
/// </summary>
private readonly string[] _qualifiers;
/// <summary>
/// All currently resolved parameters.
/// </summary>
private readonly object[] _resolvedDependencies;
/// <summary>
/// Flags indicating if a certain parameter is already resolved.
/// </summary>
private readonly bool[] _isResolved;
/// <summary>
/// Safety flag to avoid resolving this component more than once.
/// </summary>
private bool _wasResolved;
/// <summary>
/// Constructor for [LateInit] components.
/// </summary>
/// <param name="existingInstance">the existing instance</param>
public UnresolvedComponent(object existingInstance)
{
var methods = existingInstance
.GetType()
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(it => it.GetCustomAttribute(typeof(LateInitAttribute)) != null)
.ToArray();
if (methods.Length > 1)
{
throw new ArgumentException(
$"Type {existingInstance.GetType().Name} has more than one [LateInit] annotated method. Only one method can be annotated.");
}
_existingInstance = existingInstance;
if (methods.Length == 0)
{
_initializer = null;
_types = new Type[0];
_qualifiers = new string[0];
_resolvedDependencies = new object[0];
_isResolved = new bool[0];
return;
}
_initializer = methods[0];
ReadMethodInfo(_initializer, out _types, out _qualifiers, out _resolvedDependencies, out _isResolved);
}
/// <summary>
/// Constructor for regular components.
/// </summary>
/// <param name="type">the type of the component</param>
public UnresolvedComponent(Type type)
{
var constructors = type.GetConstructors();
if (constructors.Length == 0)
{
throw new ArgumentException(
$"Type {type.Name} has no public constructor. Please create a public constructor to be used for initialization.");
}
if (constructors.Length > 1)
{
constructors = constructors
.Where(it => it.GetCustomAttribute(typeof(ConstructorAttribute)) != null)
.ToArray();
if (constructors.Length > 1)
{
throw new ArgumentException(
$"Type {type.Name} has more than one constructor annotated with [Constructor]. Only one constructor can be annotated.");
}
if (constructors.Length == 0)
{
throw new ArgumentException(
$"Type {type.Name} has more than one constructor but none is annotated with [Constructor]. Please annotate exactly one with [Constructor].");
}
}
_initializer = constructors[0];
ReadMethodInfo(_initializer, out _types, out _qualifiers, out _resolvedDependencies, out _isResolved);
}
/// <summary>
/// Helper function which reads information about a method through C# reflection and puts the results into
/// the given data structures.
/// </summary>
/// <param name="method">The method instance to read.</param>
/// <param name="types">Array containing the types of the parameters of the method.</param>
/// <param name="qualifiers">Array containing the qualifier of each parameter of the method.</param>
/// <param name="resolvedDependencies">Array with an empty space for each parameter of the method.
/// This is later filled when dependency resolution takes place.</param>
/// <param name="isResolved">Array with markers indicating if a parameter is already resolved. This will contain
/// one space for each parameter, all initialized to <c>false</c></param>
private static void ReadMethodInfo(MethodBase method,
out Type[] types,
out string[] qualifiers,
out object[] resolvedDependencies,
out bool[] isResolved)
{
var parameters = method.GetParameters();
var numberOfParameters = parameters.Length;
resolvedDependencies = new object[numberOfParameters];
types = new Type[numberOfParameters];
qualifiers = new string[numberOfParameters];
isResolved = new bool[numberOfParameters];
for (var index = 0; index < numberOfParameters; index++)
{
var parameter = parameters[index];
var qualifierAttribute = parameter.GetCustomAttribute<QualifierAttribute>();
var qualifier = "";
if (qualifierAttribute != null)
{
qualifier = qualifierAttribute.Name;
}
qualifiers[index] = qualifier;
types[index] = parameter.ParameterType;
isResolved[index] = false;
}
}
/// <summary>
/// Tries to resolve this component using the given resolver method.
/// </summary>
/// <param name="resolver">The resolver method to use.</param>
/// <param name="result">the resolved object. Only filled if resolution was successful.</param>
/// <returns><c>true</c> if the resolution was successful, <c>false</c> otherwise</returns>
/// <exception cref="InvalidOperationException">if this method is called after this component already has been
/// resolved.</exception>
public bool TryResolve(UniqueResolver resolver, out object result)
{
if (_wasResolved)
{
throw new InvalidOperationException(
"This component has already been resolved. This is almost certainly a bug.");
}
if (_initializer == null)
{
// we have an existing object and no [LateInit] method, so this is completely resolved.
result = _existingInstance;
_wasResolved = true;
return true;
}
var hasUnresolvedValues = false;
// walk over all dependencies and check if something can be resolved.
for (var i = 0; i < _resolvedDependencies.Length; i++)
{
if (_isResolved[i])
{
continue;
}
if (resolver(_types[i], _qualifiers[i], out var value))
{
_resolvedDependencies[i] = value;
_isResolved[i] = true;
continue;
}
hasUnresolvedValues = true;
}
// if all dependencies could be resolved call the constructor or [LateInit] method.
if (!hasUnresolvedValues)
{
if (_initializer is ConstructorInfo constructorInfo)
{
result = constructorInfo.Invoke(_resolvedDependencies);
}
else
{
_initializer.Invoke(_existingInstance, _resolvedDependencies);
result = _existingInstance;
}
_wasResolved = true;
return true;
}
result = default;
return false;
}
public override string ToString()
{
var sb = new StringBuilder();
sb.Append("Initializer: ");
sb.Append(_initializer);
sb.Append("\nUnresolved parameters (no match or no unique match):\n");
for (var i = 0; i < _isResolved.Length; i++)
{
if (_isResolved[i])
{
continue;
}
sb.Append(" - Parameter ")
.Append(i)
.Append(": ")
.Append(_types[i].Name);
if (_qualifiers[i].Length > 0)
{
sb.Append(" (Qualifier: ")
.Append(_qualifiers[i])
.Append(")");
}
}
return sb.ToString();
}
}
}