Skip to content

Commit 9ee082e

Browse files
committed
Adding DIScope - an implementation of dependency injection container pattern
1 parent b9be2e0 commit 9ee082e

File tree

5 files changed

+363
-0
lines changed

5 files changed

+363
-0
lines changed

Assets/BossRoom/Scripts/Shared/Infrastructure.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
using UnityEngine;
5+
6+
namespace BossRoom.Scripts.Shared.Infrastructure
7+
{
8+
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor)]
9+
public sealed class Inject : Attribute
10+
{
11+
}
12+
13+
public class NoInstanceToInjectException : Exception
14+
{
15+
public NoInstanceToInjectException(string message) : base(message)
16+
{
17+
}
18+
}
19+
20+
public class ScopeNotFinalizedException : Exception
21+
{
22+
public ScopeNotFinalizedException(string message) : base(message)
23+
{
24+
}
25+
}
26+
27+
public interface IInstanceResolver
28+
{
29+
T Resolve<T>()
30+
where T : class;
31+
32+
void InjectIn(object obj);
33+
void InjectIn(GameObject obj);
34+
}
35+
36+
public sealed class DIScope : IInstanceResolver, IDisposable
37+
{
38+
private static DIScope m_rootScope;
39+
40+
private readonly DisposableGroup m_DisposableGroup = new DisposableGroup();
41+
private readonly Dictionary<Type, LazyBindDescriptor> m_LazyBindDescriptors = new Dictionary<Type, LazyBindDescriptor>();
42+
43+
private readonly DIScope m_Parent;
44+
private readonly Dictionary<Type, object> m_TypesToInstances = new Dictionary<Type, object>();
45+
private bool m_Disposed;
46+
47+
private bool m_ScopeConstructionComplete;
48+
49+
public DIScope(DIScope parent = null)
50+
{
51+
m_Parent = parent;
52+
BindInstanceAsSingle<IInstanceResolver, DIScope>(this);
53+
}
54+
55+
public static DIScope RootScope
56+
{
57+
get
58+
{
59+
if (m_rootScope == null) m_rootScope = new DIScope();
60+
61+
return m_rootScope;
62+
}
63+
}
64+
65+
public void Dispose()
66+
{
67+
if (!m_Disposed)
68+
{
69+
m_TypesToInstances.Clear();
70+
m_DisposableGroup.Dispose();
71+
m_Disposed = true;
72+
}
73+
}
74+
75+
public T Resolve<T>()
76+
where T : class
77+
{
78+
if (!m_ScopeConstructionComplete)
79+
throw new ScopeNotFinalizedException(
80+
$"Trying to Resolve type {typeof(T)}, but the DISCope is not yet finalized! You should call FinalizeScopeConstruction before any of the Resolve calls.");
81+
//if we have this type as lazy-bound instance - we are going to instantiate it now
82+
if (m_LazyBindDescriptors.TryGetValue(typeof(T), out var lazyBindDescriptor))
83+
{
84+
var instance = (T) InstantiateLazyBoundObject(lazyBindDescriptor);
85+
m_LazyBindDescriptors.Remove(typeof(T));
86+
return instance;
87+
}
88+
89+
if (!m_TypesToInstances.TryGetValue(typeof(T), out var value))
90+
{
91+
if (m_Parent != null) return m_Parent.Resolve<T>();
92+
93+
throw new NoInstanceToInjectException($"Injection of type {typeof(T)} failed.");
94+
}
95+
96+
return (T) value;
97+
}
98+
99+
public void InjectIn(object obj)
100+
{
101+
if (CachedReflectionUtility.TryGetInjectableMethod(obj.GetType(), out var injectionMethod))
102+
{
103+
var parameters = CachedReflectionUtility.GetMethodParameters(injectionMethod);
104+
105+
var paramColleciton = new object[parameters.Length];
106+
107+
for (var i = 0; i < parameters.Length; i++)
108+
{
109+
var parameter = parameters[i];
110+
111+
var genericResolveMethod = CachedReflectionUtility.GetTypedResolveMethod(parameter.ParameterType);
112+
var resolved = genericResolveMethod.Invoke(this, null);
113+
paramColleciton[i] = resolved;
114+
}
115+
116+
injectionMethod.Invoke(obj, paramColleciton);
117+
}
118+
}
119+
120+
public void InjectIn(GameObject go)
121+
{
122+
var components = go.GetComponentsInChildren<Component>();
123+
124+
foreach (var component in components) InjectIn(component);
125+
}
126+
127+
~DIScope()
128+
{
129+
Dispose();
130+
}
131+
132+
public void BindInstanceAsSingle<T>(T instance) where T : class
133+
{
134+
BindInstanceToType(instance, typeof(T));
135+
}
136+
137+
public void BindInstanceAsSingle<TInterface, TImplementation>(TImplementation instance)
138+
where TImplementation : class, TInterface
139+
where TInterface : class
140+
{
141+
BindInstanceAsSingle<TInterface>(instance);
142+
BindInstanceAsSingle(instance);
143+
}
144+
145+
private void BindInstanceToType(object instance, Type type)
146+
{
147+
m_TypesToInstances[type] = instance;
148+
}
149+
150+
public void BindAsSingle<TImplementation, TInterface>()
151+
where TImplementation : class, TInterface
152+
where TInterface : class
153+
{
154+
LazyBind(typeof(TImplementation), typeof(TInterface));
155+
}
156+
157+
public void BindAsSingle<TImplementation, TInterface, TInterface2>()
158+
where TImplementation : class, TInterface, TInterface2
159+
where TInterface : class
160+
where TInterface2 : class
161+
{
162+
LazyBind(typeof(TImplementation), typeof(TInterface), typeof(TInterface2));
163+
}
164+
165+
public void BindAsSingle<T>()
166+
where T : class
167+
{
168+
LazyBind(typeof(T));
169+
}
170+
171+
private void LazyBind(Type type, params Type[] typeAliases)
172+
{
173+
var descriptor = new LazyBindDescriptor(type, typeAliases);
174+
175+
foreach (var typeAlias in typeAliases) m_LazyBindDescriptors[typeAlias] = descriptor;
176+
177+
m_LazyBindDescriptors[type] = descriptor;
178+
}
179+
180+
private object InstantiateLazyBoundObject(LazyBindDescriptor descriptor)
181+
{
182+
object instance;
183+
if (CachedReflectionUtility.TryGetInjectableConstructor(descriptor.Type, out var constructor))
184+
{
185+
var parameters = GetResolvedInjectionMethodParameters(constructor);
186+
instance = constructor.Invoke(parameters);
187+
}
188+
else
189+
{
190+
instance = Activator.CreateInstance(descriptor.Type);
191+
InjectIn(instance);
192+
}
193+
194+
AddToDisposableGroupIfDisposable(instance);
195+
196+
BindInstanceToType(instance, descriptor.Type);
197+
198+
if (descriptor.InterfaceTypes != null)
199+
foreach (var interfaceType in descriptor.InterfaceTypes)
200+
BindInstanceToType(instance, interfaceType);
201+
202+
return instance;
203+
}
204+
205+
private void AddToDisposableGroupIfDisposable(object instance)
206+
{
207+
if (instance is IDisposable disposable) m_DisposableGroup.Add(disposable);
208+
}
209+
210+
/// <summary>
211+
/// This method forces the finalization of construction of DI Scope. It would inject all the instances passed to it directly.
212+
/// Objects that were bound by just type will be instantiated on their first use.
213+
/// </summary>
214+
public void FinalizeScopeConstruction()
215+
{
216+
if (m_ScopeConstructionComplete) return;
217+
218+
m_ScopeConstructionComplete = true;
219+
220+
var uniqueObjects = new HashSet<object>(m_TypesToInstances.Values);
221+
222+
foreach (var objectToInject in uniqueObjects)
223+
{
224+
InjectIn(objectToInject);
225+
}
226+
}
227+
228+
private object[] GetResolvedInjectionMethodParameters(MethodBase injectionMethod)
229+
{
230+
var parameters = CachedReflectionUtility.GetMethodParameters(injectionMethod);
231+
232+
var paramColleciton = new object[parameters.Length];
233+
234+
for (var i = 0; i < parameters.Length; i++)
235+
{
236+
var parameter = parameters[i];
237+
238+
var genericResolveMethod = CachedReflectionUtility.GetTypedResolveMethod(parameter.ParameterType);
239+
var resolved = genericResolveMethod.Invoke(this, null);
240+
paramColleciton[i] = resolved;
241+
}
242+
243+
return paramColleciton;
244+
}
245+
246+
private static class CachedReflectionUtility
247+
{
248+
private static readonly Dictionary<Type, MethodBase> k_CachedInjectableMethods = new Dictionary<Type, MethodBase>();
249+
private static readonly Dictionary<Type, ConstructorInfo> k_CachedInjectableConstructors = new Dictionary<Type, ConstructorInfo>();
250+
private static readonly Dictionary<MethodBase, ParameterInfo[]> k_CachedMethodParameters = new Dictionary<MethodBase, ParameterInfo[]>();
251+
private static readonly Dictionary<Type, MethodInfo> k_CachedResolveMethods = new Dictionary<Type, MethodInfo>();
252+
private static readonly Type k_InjectAttributeType = typeof(Inject);
253+
private static readonly HashSet<Type> k_ProcessedTypes = new HashSet<Type>();
254+
private static MethodInfo k_ResolveMethod;
255+
256+
public static bool TryGetInjectableConstructor(Type type, out ConstructorInfo method)
257+
{
258+
CacheTypeMethods(type);
259+
return k_CachedInjectableConstructors.TryGetValue(type, out method);
260+
}
261+
262+
private static void CacheTypeMethods(Type type)
263+
{
264+
if (k_ProcessedTypes.Contains(type)) return;
265+
266+
var constructors = type.GetConstructors();
267+
foreach (var constructorInfo in constructors)
268+
{
269+
var foundInjectionSite = constructorInfo.IsDefined(k_InjectAttributeType);
270+
if (foundInjectionSite)
271+
{
272+
k_CachedInjectableConstructors[type] = constructorInfo;
273+
var methodParameters = constructorInfo.GetParameters();
274+
k_CachedMethodParameters[constructorInfo] = methodParameters;
275+
break;
276+
}
277+
}
278+
279+
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
280+
281+
foreach (var methodInfo in methods)
282+
{
283+
var foundInjectionSite = methodInfo.IsDefined(k_InjectAttributeType);
284+
if (foundInjectionSite)
285+
{
286+
k_CachedInjectableMethods[type] = methodInfo;
287+
var methodParameters = methodInfo.GetParameters();
288+
k_CachedMethodParameters[methodInfo] = methodParameters;
289+
break;
290+
}
291+
}
292+
293+
294+
k_ProcessedTypes.Add(type);
295+
}
296+
297+
public static bool TryGetInjectableMethod(Type type, out MethodBase method)
298+
{
299+
CacheTypeMethods(type);
300+
return k_CachedInjectableMethods.TryGetValue(type, out method);
301+
}
302+
303+
public static ParameterInfo[] GetMethodParameters(MethodBase injectionMethod)
304+
{
305+
return k_CachedMethodParameters[injectionMethod];
306+
}
307+
308+
public static MethodInfo GetTypedResolveMethod(Type parameterType)
309+
{
310+
if (!k_CachedResolveMethods.TryGetValue(parameterType, out var resolveMethod))
311+
{
312+
if (k_ResolveMethod == null) k_ResolveMethod = typeof(DIScope).GetMethod("Resolve");
313+
resolveMethod = k_ResolveMethod.MakeGenericMethod(parameterType);
314+
k_CachedResolveMethods[parameterType] = resolveMethod;
315+
}
316+
317+
return resolveMethod;
318+
}
319+
}
320+
321+
private struct LazyBindDescriptor
322+
{
323+
public readonly Type Type;
324+
public readonly Type[] InterfaceTypes;
325+
326+
public LazyBindDescriptor(Type type, Type[] interfaceTypes)
327+
{
328+
Type = type;
329+
InterfaceTypes = interfaceTypes;
330+
}
331+
}
332+
}
333+
}

Assets/BossRoom/Scripts/Shared/Infrastructure/DIScope.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace BossRoom.Scripts.Shared.Infrastructure
5+
{
6+
public class DisposableGroup : IDisposable
7+
{
8+
private readonly List<IDisposable> m_Disposables = new List<IDisposable>();
9+
10+
public void Dispose()
11+
{
12+
foreach (var disposable in m_Disposables) disposable.Dispose();
13+
m_Disposables.Clear();
14+
}
15+
16+
public void Add(IDisposable disposable)
17+
{
18+
m_Disposables.Add(disposable);
19+
}
20+
}
21+
}

Assets/BossRoom/Scripts/Shared/Infrastructure/DisposableGroup.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)