NInject vs. StructureMap vs. I Made This
As I've mentioned before my team is working on a large scale ASP.NET MVC application and we're using dependency injection to decouple our data access layer from the application. Initially we were using NInject as our IoC tool of choice. It's lightweight and has a really nice configuration DSL and it's pretty fast.
The problem arose when trying to get NInject's scope caching to work. It has this nifty feature where it will cache the objects it creates and you could specify the scope of that caching...for example SingletonBehavior, or OnPerSessionBehavior. That's great but I couldn't make it work, regardless of the configuration it would always return a brand new object every time.
So then I went and checked out StructureMap. It's a little more complex to get set up and I personally don't think it's registration DSL is as nice as NInject's but it works. It has a similar caching feature where you can specify an Instance Scope of Application, Session, Request, Thread, etc. Same problem though, no matter how I configured it the caching was always at the Application level. So every session in the app would get back the same objects...not very secure on the data front.
Now, I am completely open to the possibility that I don't know enough about either of these tools and that the roadblocks I ran into were user error. However, I realized I was spending way too much time on something that is essentially a very simple problem. Here's what I needed out of an IoC tool:
1. Constructor Injection - I don't really care about property or method injection, just constructors
2. Cache the Output at Session Level
3. Easy Registration with Constructor Args
So the following is what I came up with to solve the problem:
And here's how we register the bindings:
And lastly, here's how you request an object:
So, it may be ridiculous to roll-your-own on the IoC container but as you can see it was pretty simple to get what I needed. It's very solution specific but what's wrong with that. In this instance it's pretty easy to swap out for something more feature rich if necessary but for now...I call YAGNI on all that.
namespace DI {
internal class InjectionBinder {
public Type Abstract { get; set; }
public Type Concrete { get; set; }
public object[] CtorArgs { get; set; }
}
///
/// A lightweight dependency injection tool that supports constructor injection and HttpContext.Session caching.
///
public static class Injector {
private const string SessionBindingKey = "__InjectorBindings__";
private const string SessionCacheKey = "__InjectorCache__";
private static List _bindings {
get {
if (HttpContext.Current.Session[SessionBindingKey] == null)
HttpContext.Current.Session[SessionBindingKey] = new List();
return (List)HttpContext.Current.Session[SessionBindingKey];
}
set { HttpContext.Current.Session[SessionBindingKey] = value; }
}
private static List> _cache {
get {
if (HttpContext.Current.Session[SessionCacheKey] == null)
HttpContext.Current.Session[SessionCacheKey] = new List>();
return (List>)HttpContext.Current.Session[SessionCacheKey];
}
set { HttpContext.Current.Session[SessionCacheKey] = value; }
}
///
/// Returns a concrete instance of the specified abstract type using the provided bindings and
/// attempts to satisfy that type's constructor arguments.
///
/// The abstract type of the object to initialize.
///
public static object GetInstance(Type abstractType) {
// Return the item from the cache if it's in there
var cache = _cache.ToDictionary(o => o.Key, o => o.Value);
if (cache.ContainsKey(abstractType))
return cache[abstractType];
// If the type is in the bindings, return the concrete
var foundBinding = _bindings.Find(b => b.Abstract == abstractType);
if (foundBinding != null)
return Activator.CreateInstance(foundBinding.Concrete, foundBinding.CtorArgs);
object obj = null;
var ctors = abstractType.GetConstructors().ToList();
// If the type has no constructor, then just create one
if (ctors.Count == 0)
return Activator.CreateInstance(abstractType);
// If there are constructors, find one which takes arguments of the types we have binders for
ctors.ForEach(c => {
var prms = c.GetParameters().ToList();
var args = new List();
var haveBindings = true;
prms.ForEach(p => {
var binding = _bindings.Find(b => b.Abstract == p.ParameterType);
if (binding == null) { haveBindings = false; }
else { args.Add(Activator.CreateInstance(binding.Concrete, binding.CtorArgs)); }
});
if (haveBindings) { obj = Activator.CreateInstance(abstractType, args.ToArray()); }
});
return obj;
}
///
/// Returns a concrete instance of the specified abstract type using the provided bindings and
/// attempts to satisfy that type's constructor arguments.
///
/// The abstract type of the object to initialize.
///
public static TAbstract GetInstance() where TAbstract : class {
return GetInstance(typeof (TAbstract)) as TAbstract;
}
///
/// Creates a binding rule between an abstract type and a concrete type.
///
/// The abstract type for the binding.
/// The concrete type for the binding
/// The list of constructor arguments to use when initializing the instance of the concrete type.
public static void Bind(object[] ctorArgs) {
var binding = _bindings.Find(b => b.Abstract == typeof(TAbstract));
if (binding != null) {
binding.Concrete = typeof(TConcrete);
binding.CtorArgs = ctorArgs;
} else {
_bindings.Add(new InjectionBinder {
Abstract = typeof(TAbstract),
Concrete = typeof(TConcrete),
CtorArgs = ctorArgs
});
}
}
///
/// Clears all Injector binding rules and empties the internal cache.
///
public static void Kill() {
HttpContext.Current.Session[SessionBindingKey] = null;
HttpContext.Current.Session[SessionCacheKey] = null;
}
}
}private static void BindInjectionContainer() {
Injector.Kill(); // Clears out previously created bindings and empties the cache
var currentClient = new Client(); // Doesn't matter...just an example
var currentUser = new User();
var currentCulture = "en-US";
Injector.Bind(new object[] { currentClient, currentCulture });
Injector.Bind(new object[] { currentClient, currentCulture, currentUser });
Injector.Bind(new object[] {});
}var secRepo = Injector.GetInstance();