Tales from the Trenches: One User's Story - Customizing Unity
On this page: | Download: |
---|---|
Case study provided by Dan Piessens |
Case study provided by Dan Piessens
My use of Unity started with attending the first Unity workshop hosted by the p&p group post 1.0 release. Admittedly, those were the early days of Dependency Injection and Inversion of Control (IoC) and I was completely off base as to my understanding of the concepts. However, as members of the Unity team began asking people in the room for their use cases, I began to understand how Unity was not only a well-designed IoC container, but extremely flexible and lightweight in its implementation.
My first stab at customizing Unity was to have an ILogger interface in our application that automatically set its logger name to the class it was being injected into. I looked at the extension interface; it looked simple enough, just have a class implement the extension base class and you’re good to go right? Well it’s a little more complicated than that.
First, let’s take a look at what we wanted the logger to do. Most loggers require something to identify where a log message is coming from such as a category, a source, or a name. More often than not, a fully qualified class name is used to enable a user to then filter by namespace. Since the value of the category is static, after you have injected the logger, it becomes redundant to include it on every log line or to resolve it on every call; therefore, factoring it into dependency injection makes life easier for the developer. An example of this would be as follows:
class MyClass
{
private readonly ILogger _logger;
public MyClass(ILogger logger)
{
_logger = logger;
}
public string SayHello(string name)
{
// Logger name here will be 'MyApplication.MyClass'
_logger.LogDebug("Saying Hello to {0}", name);
return string.Format("Hello {0}", name);
}
}
The first critical thing to understand is that Unity includes a sequence of build stages that are similar to a road map: each stage in this pipeline facilitates Unity in building the object that needs to be resolved. Some pipeline stages run each time an object is constructed, others only run the first time the container needs to build an object. Because of this, you need to plan how your customization fits in to this build pipeline, and which stages you need to modify to create the instance you need. There was some trial and error but in the end, it was fairly easy to modify the correct stages for the custom logger.
The next step is to think about how your extension will store or discover the additional data it needs; most of this revolves around the build context. The build context, which is similar to a SQL query plan, provides information to the pipeline on subsequent builds of the object and indicates what information the pipeline can be cached and what it needs to determine on each build. The Unity team made this straightforward with the build context so it’s not nearly as error prone as with some other IoC containers.
For this extension, I created the LoggerNameResolverPolicy. This policy is ultimately what constructs and injects the logger into the target class. It also stores the logger name, which in this case is the parent class name. Now, you might ask why we called it the “parent?” One if the tricky points to grasp is that the build plan is actually for the logger, not the class that you are injecting the logger into. The policy class looks like this:
public sealed class LoggerNameResolverPolicy : IDependencyResolverPolicy
{
private readonly Type _type;
private readonly string _name;
private readonly string _parentClassName;
public LoggerNameResolverPolicy(Type type, string name,
string parentClassName)
{
_type = type;
_name = name;
_parentClassName = parentClassName;
}
public object Resolve(IBuilderContext context)
{
var lifetimeManager = new ContainerControlledLifetimeManager();
lifetimeManager.SetValue(_parentClassName);
var loggerNameKey =
new NamedTypeBuildKey(typeof(string), "loggerName");
//Create the build context for the logger
var newKey = new NamedTypeBuildKey(_type, _name);
//Register the item as a transient policy
context.Policies.Set<IBuildKeyMappingPolicy>(
new BuildKeyMappingPolicy(loggerNameKey), loggerNameKey);
context.Policies.Set<ILifetimePolicy>(
lifetimeManager, loggerNameKey);
context.Lifetime.Add(lifetimeManager);
try
{
return context.NewBuildUp(newKey);
}
finally
{
context.Lifetime.Remove(lifetimeManager);
context.Policies.Clear<IBuildKeyMappingPolicy>(loggerNameKey);
context.Policies.Clear<ILifetimePolicy>(loggerNameKey);
}
}
}
I could refactor this implementation of the resolver policy class to use the parameter override support in Unity 2.0 and 3.0, but this code worked with Unity 1.0 and still works today. I wanted to show how even though Unity has evolved, older plugins such as this continue to work. If you’re wondering what the code is doing, it is using the concept of a “transient policy” in Unity. The policy only exists while the container is building the object and is then it is disposed. The interesting part is how the parent policy is cached with the object so whether you need one instance or a thousand, the container only creates the policy once for the class.
The next step was to capture the logger name whenever a property or constructor parameter asks for it. This was fairly simple as Unity has specific policies that run whenever it resolves a property or constructor argument (methods too but we don’t use method resolution). These are represented by interfaces IPropertySelectorPolicy and IConstructorSelectorPolicy. There are also abstract implementations that are generally very useful. In our case however, we had the need to create resolver implementations that used type metadata in several places and wanted to abstract out that portions of the property or constructor selection policy with our own that passed in the buildContext variable and the constructing type. The following code example shows how we overrode the default constructor selector policy to suit our needs. The primary modification is in the CreateResolver method, passing in the build context to look for our own policies and using the build key to capture the type being created.
public class ContainerConstructorSelectorPolicy : IConstructorSelectorPolicy
{
public virtual SelectedConstructor SelectConstructor(
IBuilderContext context, IPolicyList resolverPolicyDestination)
{
// Same as default implementation...
}
private static SelectedConstructor CreateSelectedConstructor(
IBuilderContext context, ConstructorInfo ctor,
IPolicyList resolverPolicyDestination)
{
var result = new SelectedConstructor(ctor);
foreach (var param in ctor.GetParameters())
{
var key = Guid.NewGuid().ToString();
var policy = CreateResolver(context, param);
resolverPolicyDestination.Set(policy, key);
DependencyResolverTrackerPolicy.TrackKey(context.PersistentPolicies,
context.BuildKey, key);
result.AddParameterKey(key);
}
return result;
}
private static IDependencyResolverPolicy CreateResolver(
IBuilderContext context, ParameterInfo parameterInfo)
{
var policy = context.Policies
.Get<IParameterDependencyResolver>(
new NamedTypeBuildKey(parameterInfo.ParameterType));
IDependencyResolverPolicy resolverPolicy = null;
if (policy != null)
{
resolverPolicy = policy.CreateResolver(context.BuildKey.Type,
parameterInfo);
}
return resolverPolicy ?? new FixedTypeResolverPolicy(
parameterInfo.ParameterType);
}
private static ConstructorInfo FindInjectionConstructor(Type typeToConstruct)
{
// Same as default implementation...
}
private static ConstructorInfo FindLongestConstructor(Type typeToConstruct)
{
// Same as default implementation...
}
}
Once we had these main build policies in place, we needed to implement our new interfaces IPropertyDependencyResolver and IParameterDependencyResolver. The signatures are almost identical, except that one passes in parameter arguments and the other passes in constructor arguments. For simplicity, I’ll only show the constructor resolver, but the pattern is the same for a parameter resolver or even a method resolver if you choose to support them.
public class LoggerNameConstuctorParameterPolicy : IParameterDependencyResolver
{
public IDependencyResolverPolicy CreateResolver(
Type currentType, ParameterInfo param)
{
return new LoggerNameResolverPolicy(param.ParameterType, null,
currentType.FullName);
}
}
The final module wire up is easy; in fact, most of the items don’t need additional configuration. We had a few errors on the first try, but the debugging experience made those errors very descriptive. The fact that Unity is open source also helped greatly, there were a few times when I peeked at the default implementation code to get hints about the right way to construct things. The Unity guides that ship with this release will also prove to be a big help with customization. The code to create the extension itself simply involved registering the policies into the parameter and constructor discovery stages and indicating that this should occur for any ILogger item. It also included a section to override the default constructor selector and property selector policies with our custom ones. In the end we moved this code to a separate extension so it could be better reused. The final registration code looked like this:
public class LoggerNameExtension : UnityContainerExtension
{
protected override void Initialize()
{
// Override base Unity policies.
Context.Policies.ClearDefault<IConstructorSelectorPolicy>();
Context.Policies.SetDefault<IConstructorSelectorPolicy>(
new ContainerConstructorSelectorPolicy());
Context.Policies.SetDefault<IPropertySelectorPolicy>(
new ContainerPropertySelectorPolicy());
// Set logging specific policies
var buildKey = new NamedTypeBuildKey(typeof(ILogger));
Context.Policies.Set<IParameterDependencyResolver>(
new LoggerNameConstuctorParameterPolicy(), buildKey);
Context.Policies.Set<IPropertyDependencyResolver>(
new LoggerNamePropertyPolicy(), buildKey);
}
}
Emboldened by our initial success, my team and I went on to create several more Unity extensions; some added additional dependency resolution attribute options, others effectively created specialized factory instances of objects. Over the past few years, many of the extensions were retired as Unity itself expanded to support factory methods, alternate registration attributes, and most recently Lazy<T> item support and registration by convention. The only real customization needed today is for scenarios such as the logger, where metadata is pulled from the code that calls it.