Skip to content

DIScope - an implementation of dependency injection container pattern #461

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Assets/BossRoom/Scripts/Shared/Infrastructure.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

372 changes: 372 additions & 0 deletions Assets/BossRoom/Scripts/Shared/Infrastructure/DIScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,372 @@
using System;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move those to Utilities? They seem pretty boss room indepandant and would be great to reuse in future samples

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely.

using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

namespace BossRoom.Scripts.Shared.Infrastructure
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor)]
public sealed class Inject : Attribute
{
}

public class NoInstanceToInjectException : Exception
{
public NoInstanceToInjectException(string message) : base(message)
{
}
}

public class ScopeNotFinalizedException : Exception
{
public ScopeNotFinalizedException(string message) : base(message)
{
}
}

public interface IInstanceResolver
{
T Resolve<T>()
where T : class;

void InjectIn(object obj);
void InjectIn(GameObject obj);
}

public sealed class DIScope : IInstanceResolver, IDisposable
{
private struct LazyBindDescriptor
{
public readonly Type Type;
public readonly Type[] InterfaceTypes;

public LazyBindDescriptor(Type type, Type[] interfaceTypes)
{
Type = type;
InterfaceTypes = interfaceTypes;
}
}

private static DIScope m_rootScope;

public static DIScope RootScope
{
get
{
if (m_rootScope == null)
{
m_rootScope = new DIScope();
}

return m_rootScope;
}
}

private readonly DisposableGroup m_DisposableGroup = new DisposableGroup();
private readonly Dictionary<Type, LazyBindDescriptor> m_LazyBindDescriptors = new Dictionary<Type, LazyBindDescriptor>();

private readonly DIScope m_Parent;
private readonly Dictionary<Type, object> m_TypesToInstances = new Dictionary<Type, object>();
private readonly HashSet<object> m_ObjectsWithInjectedDependencies = new HashSet<object>();
private bool m_Disposed;

private bool m_ScopeConstructionComplete;

public DIScope(DIScope parent = null)
{
m_Parent = parent;
BindInstanceAsSingle<IInstanceResolver, DIScope>(this);
}

~DIScope()
{
Dispose();
}

public void Dispose()
{
if (!m_Disposed)
{
m_TypesToInstances.Clear();
m_ObjectsWithInjectedDependencies.Clear();
m_DisposableGroup.Dispose();
m_Disposed = true;
}
}

public T Resolve<T>() where T : class
{
if (!m_ScopeConstructionComplete)
{
throw new ScopeNotFinalizedException(
$"Trying to Resolve type {typeof(T)}, but the DISCope is not yet finalized! You should call FinalizeScopeConstruction before any of the Resolve calls.");
}

//if we have this type as lazy-bound instance - we are going to instantiate it now
if (m_LazyBindDescriptors.TryGetValue(typeof(T), out var lazyBindDescriptor))
{
var instance = (T) InstantiateLazyBoundObject(lazyBindDescriptor);
m_LazyBindDescriptors.Remove(typeof(T));
return instance;
}

if (!m_TypesToInstances.TryGetValue(typeof(T), out var value))
{
if (m_Parent != null)
{
return m_Parent.Resolve<T>();
}

throw new NoInstanceToInjectException($"Injection of type {typeof(T)} failed.");
}

return (T) value;
}

public void InjectIn(object obj)
{
if (m_ObjectsWithInjectedDependencies.Contains(obj))
{
return;
}

if (CachedReflectionUtility.TryGetInjectableMethod(obj.GetType(), out var injectionMethod))
{
var parameters = CachedReflectionUtility.GetMethodParameters(injectionMethod);

var paramColleciton = new object[parameters.Length];

for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];

var genericResolveMethod = CachedReflectionUtility.GetTypedResolveMethod(parameter.ParameterType);
var resolved = genericResolveMethod.Invoke(this, null);
paramColleciton[i] = resolved;
}

injectionMethod.Invoke(obj, paramColleciton);
m_ObjectsWithInjectedDependencies.Add(obj);
}
}

public void InjectIn(GameObject go)
{
var components = go.GetComponentsInChildren<Component>(includeInactive:true);

foreach (var component in components)
{
InjectIn(component);
}
}

public void BindInstanceAsSingle<T>(T instance) where T : class
{
BindInstanceToType(instance, typeof(T));
}

public void BindInstanceAsSingle<TInterface, TImplementation>(TImplementation instance)
where TImplementation : class, TInterface
where TInterface : class
{
BindInstanceAsSingle<TInterface>(instance);
BindInstanceAsSingle(instance);
}

private void BindInstanceToType(object instance, Type type)
{
m_TypesToInstances[type] = instance;
}

public void BindAsSingle<TImplementation, TInterface>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason this has the same name as BindAsSingle with only one type?
Basically, this is a way to bind a default implementation for a given interface correct? Just "BindAsSingle" might be a bit confusing for that.
On another hand we don't want BindDefaultImplementationToInterfaceAsSingle...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there is no specific reason to go with an overloaded function, however I just have a preference for overloads when they seem to make sense. So the intent here is to allow the user to bind a concrete implementation type that will be available by it's concrete type (that's a default of the DIScope - if something is bound, it is always automatically bound to (at the very least) it's concrete type) AND by the interface type.

I'll try and see if there are some better groupings and names for these method - lmk if you have ideas for short and concise intentful namings.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BindInterfaceImplementationAsSingle seems like a mouthful too.
BindInterfaceAsSingle?

where TImplementation : class, TInterface
where TInterface : class
{
LazyBind(typeof(TImplementation), typeof(TInterface));
}

public void BindAsSingle<TImplementation, TInterface, TInterface2>()
where TImplementation : class, TInterface, TInterface2
where TInterface : class
where TInterface2 : class
{
LazyBind(typeof(TImplementation), typeof(TInterface), typeof(TInterface2));
}

public void BindAsSingle<T>()
where T : class
{
LazyBind(typeof(T));
}

private void LazyBind(Type type, params Type[] typeAliases)
{
var descriptor = new LazyBindDescriptor(type, typeAliases);

foreach (var typeAlias in typeAliases)
{
m_LazyBindDescriptors[typeAlias] = descriptor;
}

m_LazyBindDescriptors[type] = descriptor;
}

private object InstantiateLazyBoundObject(LazyBindDescriptor descriptor)
{
object instance;
if (CachedReflectionUtility.TryGetInjectableConstructor(descriptor.Type, out var constructor))
{
var parameters = GetResolvedInjectionMethodParameters(constructor);
instance = constructor.Invoke(parameters);
}
else
{
instance = Activator.CreateInstance(descriptor.Type);
InjectIn(instance);
}

AddToDisposableGroupIfDisposable(instance);

BindInstanceToType(instance, descriptor.Type);

if (descriptor.InterfaceTypes != null)
{
foreach (var interfaceType in descriptor.InterfaceTypes)
{
BindInstanceToType(instance, interfaceType);
}
}

return instance;
}

private void AddToDisposableGroupIfDisposable(object instance)
{
if (instance is IDisposable disposable)
{
m_DisposableGroup.Add(disposable);
}
}

/// <summary>
/// This method forces the finalization of construction of DI Scope. It would inject all the instances passed to it directly.
/// Objects that were bound by just type will be instantiated on their first use.
/// </summary>
public void FinalizeScopeConstruction()
{
if (m_ScopeConstructionComplete)
{
return;
}

m_ScopeConstructionComplete = true;

var uniqueObjects = new HashSet<object>(m_TypesToInstances.Values);

foreach (var objectToInject in uniqueObjects)
{
InjectIn(objectToInject);
}
}

private object[] GetResolvedInjectionMethodParameters(MethodBase injectionMethod)
{
var parameters = CachedReflectionUtility.GetMethodParameters(injectionMethod);

var paramColleciton = new object[parameters.Length];

for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];

var genericResolveMethod = CachedReflectionUtility.GetTypedResolveMethod(parameter.ParameterType);
var resolved = genericResolveMethod.Invoke(this, null);
paramColleciton[i] = resolved;
}

return paramColleciton;
}

private static class CachedReflectionUtility
{
private static readonly Dictionary<Type, MethodBase> k_CachedInjectableMethods = new Dictionary<Type, MethodBase>();
private static readonly Dictionary<Type, ConstructorInfo> k_CachedInjectableConstructors = new Dictionary<Type, ConstructorInfo>();
private static readonly Dictionary<MethodBase, ParameterInfo[]> k_CachedMethodParameters = new Dictionary<MethodBase, ParameterInfo[]>();
private static readonly Dictionary<Type, MethodInfo> k_CachedResolveMethods = new Dictionary<Type, MethodInfo>();
private static readonly Type k_InjectAttributeType = typeof(Inject);
private static readonly HashSet<Type> k_ProcessedTypes = new HashSet<Type>();
private static MethodInfo k_ResolveMethod;

public static bool TryGetInjectableConstructor(Type type, out ConstructorInfo method)
{
CacheTypeMethods(type);
return k_CachedInjectableConstructors.TryGetValue(type, out method);
}

private static void CacheTypeMethods(Type type)
{
if (k_ProcessedTypes.Contains(type))
{
return;
}

var constructors = type.GetConstructors();
foreach (var constructorInfo in constructors)
{
var foundInjectionSite = constructorInfo.IsDefined(k_InjectAttributeType);
if (foundInjectionSite)
{
k_CachedInjectableConstructors[type] = constructorInfo;
var methodParameters = constructorInfo.GetParameters();
k_CachedMethodParameters[constructorInfo] = methodParameters;
break;
}
}

var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

foreach (var methodInfo in methods)
{
var foundInjectionSite = methodInfo.IsDefined(k_InjectAttributeType);
if (foundInjectionSite)
{
k_CachedInjectableMethods[type] = methodInfo;
var methodParameters = methodInfo.GetParameters();
k_CachedMethodParameters[methodInfo] = methodParameters;
break;
}
}


k_ProcessedTypes.Add(type);
}

public static bool TryGetInjectableMethod(Type type, out MethodBase method)
{
CacheTypeMethods(type);
return k_CachedInjectableMethods.TryGetValue(type, out method);
}

public static ParameterInfo[] GetMethodParameters(MethodBase injectionMethod)
{
return k_CachedMethodParameters[injectionMethod];
}

public static MethodInfo GetTypedResolveMethod(Type parameterType)
{
if (!k_CachedResolveMethods.TryGetValue(parameterType, out var resolveMethod))
{
if (k_ResolveMethod == null)
{
k_ResolveMethod = typeof(DIScope).GetMethod("Resolve");
}

resolveMethod = k_ResolveMethod.MakeGenericMethod(parameterType);
k_CachedResolveMethods[parameterType] = resolveMethod;
}

return resolveMethod;
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading