Skip to content

Instantly share code, notes, and snippets.

@chkn
Last active August 29, 2015 14:01
Show Gist options
  • Save chkn/6238f11c23d16cfc48c3 to your computer and use it in GitHub Desktop.
Save chkn/6238f11c23d16cfc48c3 to your computer and use it in GitHub Desktop.
A simple stab at a dependency injector
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))
{
}
}
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