Last active
August 29, 2015 14:01
-
-
Save chkn/6238f11c23d16cfc48c3 to your computer and use it in GitHub Desktop.
A simple stab at a dependency injector
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Text; | |
using System.Linq; | |
using System.Threading; | |
using System.Reflection; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
public static class DI { | |
public sealed class Context : IDisposable { | |
internal Context parentContext; | |
readonly Dictionary<Type,Func<object>> factory = new Dictionary<Type,Func<object>> (); | |
internal void Register (Type type, Func<object> creator) | |
{ | |
factory [type] = creator; | |
} | |
internal bool CanGetInstance (Type type) | |
{ | |
return factory.ContainsKey (type) || (parentContext != null && parentContext.CanGetInstance (type)); | |
} | |
internal object TryGetInstance (Type type) | |
{ | |
Func<object> creator; | |
var success = factory.TryGetValue (type, out creator) || (parentContext != null && parentContext.factory.TryGetValue (type, out creator)); | |
return success? creator () : null; | |
} | |
public override string ToString () | |
{ | |
var sb = new StringBuilder (); | |
foreach (var kv in factory) { | |
sb.AppendFormat ("{0} => {1}", kv.Key.Name, kv.Value.Target); | |
sb.AppendLine (); | |
} | |
return sb.ToString (); | |
} | |
void IDisposable.Dispose () | |
{ | |
Interlocked.CompareExchange (ref DI.current, parentContext, this); | |
} | |
} | |
static Context current; | |
public static Context Current { | |
get { | |
if (current == null) | |
Interlocked.CompareExchange (ref current, new Context (), null); | |
return current; | |
} | |
set { current = value; } | |
} | |
public static Context Subcontext () | |
{ | |
var subcontext = new Context (); | |
subcontext.parentContext = Interlocked.Exchange (ref current, subcontext); | |
return subcontext; | |
} | |
// Registers all types in the given assembly that implement an | |
// injectable interface.. | |
public static void Select (Assembly assembly) | |
{ | |
if (assembly == null) | |
throw new ArgumentNullException ("assembly"); | |
foreach (var type in assembly.GetTypes ()) | |
Select (type); | |
} | |
public static void Select (object singleton) | |
{ | |
if (singleton == null) | |
throw new ArgumentNullException ("singleton"); | |
Select (singleton.GetType (), singleton.Selector); | |
} | |
public static void Select (Type instanceType) | |
{ | |
if (instanceType == null) | |
throw new ArgumentNullException ("instanceType"); | |
Select (instanceType, instanceType.Selector); | |
} | |
public static void Select (ConstructorInfo constructor) | |
{ | |
if (constructor == null) | |
throw new ArgumentNullException ("constructor"); | |
Select (constructor.DeclaringType, constructor.Selector); | |
} | |
static void Select (Type type, Func<object> creator) | |
{ | |
if (type.IsAbstract || type.IsInterface) | |
throw new ArgumentException ("Must be a concrete type"); | |
Current.Register (type, creator); | |
foreach (var iface in GetInjectableInterfaces (type)) | |
Current.Register (iface, creator); | |
} | |
public static TInstance Inject<TInstance> () | |
{ | |
TInstance result; | |
if (!TryInject (out result)) | |
throw new UnsatisfiedDependencyException (Current, typeof (TInstance)); | |
return result; | |
} | |
public static bool TryInject<TInstance> (out TInstance result) | |
{ | |
var obj = TryInject (typeof (TInstance)); | |
if (obj != null) { | |
result = (TInstance) obj; | |
return true; | |
} | |
result = default (TInstance); | |
return false; | |
} | |
public static object TryInject (Type targetType) | |
{ | |
return Current.TryGetInstance (targetType); | |
} | |
static IEnumerable<Type> GetInjectableInterfaces (Type concrete) | |
{ | |
// We only want interfaces explicitly declared on the type, | |
// not interfaces that "inherit" from other interfaces.. | |
var interfaces = concrete.GetInterfaces (); | |
return interfaces | |
.Except (interfaces.SelectMany (t => t.GetInterfaces ())) | |
.Where (t => t.IsDefined (typeof (InjectableAttribute), false)); | |
} | |
// We use extension methods for selectors because this creates | |
// curried delegates instead of closures.. | |
static object Selector (this object singleton) | |
{ | |
return singleton; | |
} | |
static object Selector (this Type type) | |
{ | |
// Try to generate an instance of the concrete class by injecting any arguments | |
foreach (var ctor in type.GetConstructors (BindingFlags.Public | BindingFlags.Instance)) { | |
var parameters = ctor.GetParameters (); | |
if (parameters.Length == 0) { | |
// fast path | |
Select (ctor); | |
return ctor.Invoke (null); | |
} | |
// try and find a compatible constructor | |
foreach (var param in parameters) { | |
if (!Current.CanGetInstance (param.ParameterType)) | |
goto nextCtor; | |
} | |
Select (ctor); | |
return ctor.Selector (); | |
nextCtor:; | |
} | |
// couldn't find a constructor we could use | |
throw new MissingMemberException (string.Format ("No suitable constructor found for '{0}'. Current selections:{1}{2}", type, Environment.NewLine, Current)); | |
} | |
static object Selector (this ConstructorInfo ctor) | |
{ | |
var parameters = ctor.GetParameters (); | |
var args = new object [parameters.Length]; | |
for (var i = 0; i < parameters.Length; i++) | |
args [i] = Current.TryGetInstance (parameters [i].ParameterType); | |
return ctor.Invoke (args); | |
} | |
} | |
[AttributeUsage (AttributeTargets.Interface)] | |
public class InjectableAttribute : Attribute { | |
} | |
public class UnsatisfiedDependencyException : Exception { | |
public UnsatisfiedDependencyException (DI.Context ambience, Type unsatisfied) | |
: base (string.Format ("No implementation selected for '{0}'. Current selections:{1}{2}", unsatisfied, Environment.NewLine, ambience)) | |
{ | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using NUnit.Framework; | |
[TestFixture] | |
public class DITests { | |
[SetUp] | |
public void SetUp () | |
{ | |
DI.Current = new DI.Context (); | |
} | |
[Test] | |
public void TestSingletonRegistration () | |
{ | |
var singleton = new Impl1 (); | |
DI.Select (singleton); | |
Assert.AreSame (singleton, DI.Inject<IInterface1> ()); | |
} | |
[Test] | |
public void TestInstanceTypeRegistrations () | |
{ | |
DI.Select (typeof (Impl1)); | |
DI.Select (typeof (Impl2)); | |
var instance1 = DI.Inject<IInterface1> (); | |
Assert.IsNotNull (instance1, "#1"); | |
Assert.IsInstanceOfType (typeof (Impl1), instance1, "#2"); | |
var instance2 = DI.Inject<IInterface1> (); | |
Assert.IsNotNull (instance2, "#3"); | |
Assert.IsInstanceOfType (typeof (Impl1), instance2, "#4"); | |
Assert.AreNotSame (instance2, instance1, "#5"); | |
var instance3 = DI.Inject<IInterface2> (); | |
Assert.IsNotNull (instance3, "#6"); | |
Assert.IsInstanceOfType (typeof (Impl2), instance3, "#7"); | |
} | |
[Test] | |
public void TestConstructorArgumentSubcontext () | |
{ | |
Exception ex = null; | |
DI.Select (typeof (Concrete2)); | |
Assert.AreNotEqual ("", DI.Current.ToString (), "#1"); | |
try { | |
DI.Inject<Concrete2> (); | |
} catch (Exception e) { | |
ex = e; | |
} | |
Assert.IsInstanceOfType (typeof (MissingMemberException), ex, "#2"); | |
ex = null; | |
using (DI.Subcontext ()) { | |
DI.Select (typeof (Impl1)); | |
DI.Select (typeof (Concrete1)); | |
Assert.IsNotNull (DI.Inject<Concrete2> (), "#3"); | |
} | |
try { | |
DI.Inject<Concrete2> (); | |
} catch (Exception e) { | |
ex = e; | |
} | |
Assert.IsInstanceOfType (typeof (MissingMemberException), ex, "#4"); | |
} | |
} | |
[Injectable] | |
interface IInterface1 { | |
} | |
[Injectable] | |
interface IInterface2 : IInterface1 { | |
} | |
class Impl1 : IInterface1 { | |
} | |
class Impl2 : IInterface2 { | |
} | |
class Concrete1 { | |
} | |
class Concrete2 { | |
public Concrete2 (Concrete1 c1, IInterface1 i1) | |
{ | |
if (c1 == null) | |
throw new ArgumentNullException ("c1"); | |
if (i1 == null) | |
throw new ArgumentNullException ("i1"); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment