Decoupling Application Code from IoC Implementation
Background:
While working on my pet project, using my favourite dependency injection framework, StructureMap, I came to a realization. If I ever decide to change my dependency injection framework for something else, such as Ninject, my code is tightly coupled to StructureMap. The change would force me to update a bunch of code throughout my numerous application layers.
Solution:
To decouple my code from StructureMap, I created a base class IocHelper with the most important functions that I use.
- AddConfiguration – For telling the IoC framework what class I want it to instantiate for a particular requested type.
- GetInstance – For asking the IoC framework for an instantiated object of a requested type or class.
Here is the complete IocHelper base class:
public abstract class IocHelper
{
protected IocHelper()
{
RegistryItemList = new List<RegistryItem>();
}
/// <summary>
/// Factory method to Creates an instance of StructureMapIocHelper.
/// </summary>
/// <returns></returns>
public static IocHelper Create()
{
//Instantiate and return the implementation you choose to use for your IOC
return new StructureMapIocHelper();
}
/// <summary>
/// Information about an item that we are going to register with Map
/// </summary>
protected class RegistryItem
{
public Type InterfaceType { get; set; }
public Type ConcreteType { get; set; }
public CacheEnum CacheStrategy { get; set; }
public Func<object> RunAction { get; set; }
}
protected List<RegistryItem> RegistryItemList { get; set; }
public enum CacheEnum
{
/// <summary>
/// A single instance will be created for each HttpContext. Caches the instances in the HttpContext.Items collection if it exists, otherwise uses ThreadLocal storage.
/// </summary>
HybridCached,
/// <summary>
/// A new instance will be created for each request.
/// </summary>
PerRequest,
/// <summary>
/// A single instance will be shared across all requests
/// </summary>
Singleton
}
/// <summary>
/// Adds a configuration mapping for an Interface class
/// </summary>
/// <typeparam name="TInterface">The interface type</typeparam>
/// <typeparam name="TConcrete">The concrete class type to inject</typeparam>
/// <param name="cacheEnum">How the object will be cached</param>
public void AddConfiguration<TInterface, TConcrete>(CacheEnum cacheEnum)
{
AddConfiguration(typeof(TInterface), typeof(TConcrete), cacheEnum);
}
/// <summary>
/// Adds a configuration mapping for an Interface class
/// </summary>
/// <param name="forInterface">The interface type</param>
/// <param name="theConcreteTypeIs">The concrete class type to inject</param>
/// <param name="cacheStrategy">How the object will be cached</param>
public void AddConfiguration(Type forInterface, Type theConcreteTypeIs, CacheEnum cacheStrategy)
{
if (!forInterface.IsInterface)
throw new InvalidDataException("First parameter to AddConfiguration must be an Interface");
if (!theConcreteTypeIs.IsClass)
throw new InvalidDataException("Second parameter to AddConfiguration must be a Class");
RegistryItemList.Add(new RegistryItem
{
InterfaceType = forInterface,
ConcreteType = theConcreteTypeIs,
CacheStrategy = cacheStrategy
});
}
/// <summary>
/// Adds a configuration mapping for a concrete class
/// </summary>
/// <param name="forClass">The type of the class.</param>
/// <param name="actionFunction">A delegate function that instantiates the class</param>
/// <param name="cacheStrategy">how the object will be cached</param>
public void AddConfiguration(Type forClass, Func<object> actionFunction, CacheEnum cacheStrategy)
{
if (!forClass.IsClass)
throw new InvalidDataException("First parameter to AddConfiguration must be a Class");
RegistryItemList.Add(new RegistryItem
{
ConcreteType = forClass,
CacheStrategy = cacheStrategy,
RunAction = actionFunction
});
}
/// <summary>
/// Adds a configuration mapping for a concrete class
/// </summary>
/// <typeparam name="TClass">The type of the class.</typeparam>
/// <param name="actionFunction">A delegate function that instantiates the class</param>
/// <param name="cacheStrategy">how the object will be cached</param>
public void AddConfiguration<TClass>(Func<object> actionFunction, CacheEnum cacheStrategy)
{
AddConfiguration(typeof(TClass), actionFunction, cacheStrategy);
}
/// <summary>
/// Commits the configuration to make all of the added configurations effective.
/// </summary>
public abstract void CommitConfiguration();
/// <summary>
/// Gets the instance of a particular class based on map configurations.
/// </summary>
/// <typeparam name="T">The type to cast the output to</typeparam>
/// <param name="requestedType">Type of the requested object.</param>
/// <returns>instantiated object that was requested</returns>
public abstract T GetInstance<T>(Type requestedType);
/// <summary>
/// Gets the instance of a particular class based on map configurations.
/// </summary>
/// <typeparam name="T">The type requested object</typeparam>
/// <returns> instantiated object that was requested</returns>
public abstract T GetInstance<T>();
}
Then I created a derived class from IocHelper to create the StructureMap implementation of my IocHelper. So far I have only created a StructureMap implementation, but next I plan on creating one for Ninject and others as well.
Here is the complete StructureMapIocHelper implementation class:
/// <summary>
/// IOC Helper Implementation for StructureMap v2.6.1
/// </summary>
public class StructureMapIocHelper : IocHelper
{
/// <summary>
/// Structure Map Registry class that will handle the registration of items on instantiation.
/// </summary>
protected class RegistryClass : Registry
{
/// <summary>
/// Initializes a new instance of the <see cref="RegistryClass"/> class.
/// </summary>
/// <param name="registryItems">Tequires a list of registry items which will be used for mapping upon instantiation.</param>
public RegistryClass(IEnumerable<RegistryItem> registryItems)
{
foreach (var item in registryItems)
{
var registryItem = item;
if (registryItem.RunAction != null)
{
//Register Concrete types with Structure Map
For(registryItem.ConcreteType).LifecycleIs(GetCacheStrategy(registryItem.CacheStrategy)).Use(registryItem.RunAction());
}
else
{
//Register Interface types with Structure Map
For(registryItem.InterfaceType).LifecycleIs(GetCacheStrategy(registryItem.CacheStrategy)).Use(registryItem.ConcreteType);
}
}
}
/// <summary>
/// Conver the cache strategy to StructureMap specific enumerations.
/// </summary>
/// <param name="cacheEnum">The cache enum.</param>
/// <returns></returns>
public InstanceScope GetCacheStrategy(CacheEnum cacheEnum)
{
switch (cacheEnum)
{
case CacheEnum.HybridCached:
return InstanceScope.Hybrid; //A single instance will be created for each HttpContext. Caches the instances in the HttpContext.Items collection if it exists, otherwise uses ThreadLocal storage.
case CacheEnum.PerRequest:
return InstanceScope.PerRequest; //A new instance will be created for each request.
case CacheEnum.Singleton:
return InstanceScope.Singleton; //A single instance will be shared across all requests
default:
throw new NotImplementedException("{0} not implemented for StructureMapIocHelper.RegistryClass");
}
}
}
public override void CommitConfiguration()
{
var reg = new RegistryClass(RegistryItemList);
ObjectFactory.Configure(x => x.AddRegistry(reg));
}
public override T GetInstance<T>(Type requestedType)
{
return (T)ObjectFactory.GetInstance(requestedType);
}
public override T GetInstance<T>()
{
return GetInstance<T>(typeof (T));
}
}
I know that some people use their IoC in much more complex ways. I think this solution could be modified to accommodate those as well. But for most people who use IoC in its basic form it should be good enough.
Now to demonstrate how this IoC wrapper is used in code. Here is an example of my “Boot Strap” class that takes care of configuring my dependencies:
public static class BootStrap
{
public static void BootIOC()
{
/*
Get an instance of the IoC ipmlementation
In this case it will get the StructureMapIocHelper
*/
var registry = IocHelper.Create();
//Typical Interface to concrete class mappings
registry.AddConfiguration<ITimeFactory, TimeFactory>(IocHelper.CacheEnum.HybridCached);
registry.AddConfiguration<IUserRepository,UserRepository>(IocHelper.CacheEnum.PerRequest);
registry.AddConfiguration<IFactory,Factory>(IocHelper.CacheEnum.HybridCached);
/*
Example of configuring classes that don't have interfaces.
A delegate is passed in as the first parameter,
which tells it how to instantiate the object.
*/
registry.AddConfiguration<UnitOfWork>(() => new UnitOfWork(), IocHelper.CacheEnum.HybridCached);
//Commit this configuration to the IoC implementation and make it active
registry.CommitConfiguration();
}
}
Once the mappings have been configured like above, you can use the following code to ask the IoC container to instantiate an object for you. For example, to ask for an instance of ITimeFactory you can do the following:
IocHelper.Create().GetInstance<ITimeFactory>();
Roberto Sebestyen
You might want to check out the CommonServiceLocator project on codeplex. It provides a common interface for IoC containers Windsor, StructureMap, Ninject, Unity, Spring.Net, AutoFac, MEF, and LinFu.
@Chris Hefley
Thanks for that tip Chris. I’ll definitely check it out! I figured something like that already exists, however I wanted to experiment and see how I could achieve this result myself. As much as I like external libraries, saving me from re-inventing the wheel, trying it myself teaches me a thing or two.