For the first time I’ve used DI container 5 years ago (it was Spring for Java). During the first experience I was greatly impressed with the required style of design: component needs to do only what outer world expect from him. All other things, like creating or locating dependencies should be put outside the component. This is quite obvious but abidance of these simple rules brings a number of valuable benefits:
- If component does only what it is responsible to do then the code decreases in size and become easier to understand
- All dependencies of component can be determined from its public interface (through properties, constructor parameters etc)
- Level of potential reusability increases due to low coupling
- Testability increases especially with application of tools like RhinoMocks and Moq
Unity is a simple and lightweight DI container for .NET. It allows defining interconnections between components both in XML and using API of IUnityContainer. In this post I’d like to show the idea how configuration mechanism can be extended using possibilities of C# 3.0.
Sample classes and interfaces:public interface IMyComponent
{
void Run();
}
public interface ILogger
{
void LogInfo(string format, params object[] args);
}
public class MyComponent : IMyComponent
{
public string Text { get; set; }
public ILogger Logger { get; set; }
public void Run()
{
Logger.LogInfo(Text);
}
}
public class ConsoleLogger : ILogger
{
public void LogInfo(string format, params object[] args)
{
Console.WriteLine(format, args);
}
}
As we can see MyComponent class is decoupled from concrete implementation of ILogger so is can be tested with ILogger stub, reused with other type of logger and so on. Let’s create a code that will wire up all these components together.
1. Create UnityContainer and register all types in it
var container = new UnityContainer()
.RegisterType<IMyComponent, MyComponent>(
new InjectionMember[]
{
new InjectionProperty("Logger", new ResolvedParameter(typeof (ILogger),"Logger")),
new InjectionProperty("Text", "DataSource=...")
}
)
.RegisterType<ILogger, ConsoleLogger>("Logger");
For this sample I've used beta release of Unity – Feb 2010, in the older versions configuration of injected members is available only through calling ConfigureInjectionFor with InjectedMembers extension.
2. Instantiate MyComponent and run operation
var myComponent = container.Resolve<IMyComponent>();
myComponent.Run();
Line "DataSource=…" should appear in console.
Everything seems to be fine, but… IMHO this version of code has few flaws. First: property is referred via string name and this is very error prone. One occasional rename operation and BOOM!!! Second inconvenience is that property type should be set explicitly. This is annoying and unsafe approach, developer can change type of property but DI container will still try to inject dependent object relying on the old type. Let's add that both of mentioned errors are invisible to compiler and reveal itself only in runtime.
Time to make some improvements. C# 3.0 already provides ways to refer type members in type-safe way via using expression trees. We can apply them both to define target property and to get its type. This version will be much more resistant to errors because of compiler control.
In the beginning - some auxiliary types
/// <summary>
/// Accumulator of type-related settings
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ITypeConfigurator<T>
{
ITypeConfigurator<T> SetName(string name);
ITypeConfigurator<T> SetLifetimeManager(LifetimeManager lifetimeManager);
ITypeConfigurator<T> SetResolvedProperty<TRes>(Expression<Func<T, TRes>> expr);
ITypeConfigurator<T> SetResolvedProperty<TRes>(Expression<Func<T, TRes>> expr, string objectName);
ITypeConfigurator<T> SetValueProperty<TRes>(Expression<Func<T, TRes>> expr, TRes value);
}
public class TypeConfigurator<T> : ITypeConfigurator<T>
{
public TypeConfigurator()
{
InjectionMembers = new List<InjectionMember>();
LifetimeManager = new ContainerControlledLifetimeManager(); // default value
}
public string Name { get; private set; }
public LifetimeManager LifetimeManager { get; private set; }
public List<InjectionMember> InjectionMembers { get; private set; }
ITypeConfigurator<T> ITypeConfigurator<T>.SetName(string name)
{
Name = name;
return this;
}
ITypeConfigurator<T> ITypeConfigurator<T>.SetLifetimeManager(LifetimeManager lifetimeManager)
{
LifetimeManager = lifetimeManager;
return this;
}
ITypeConfigurator<T> ITypeConfigurator<T>.SetResolvedProperty<TRes>(Expression<Func<T, TRes>> expr)
{
return AddResolvedProperty(expr, null);
}
ITypeConfigurator<T> ITypeConfigurator<T>.SetResolvedProperty<TRes>(Expression<Func<T, TRes>> expr, string objectName)
{
return AddResolvedProperty(expr, objectName);
}
private ITypeConfigurator<T> AddResolvedProperty<TRes>(Expression<Func<T, TRes>> expr, string objectName)
{
var property = GetPropertyInfo(expr);
var propertyValue = string.IsNullOrEmpty(objectName)
? new ResolvedParameter<TRes>()
: new ResolvedParameter<TRes>(objectName);
InjectionMembers.Add(new InjectionProperty(property.Name, propertyValue));
return this;
}
ITypeConfigurator<T> ITypeConfigurator<T>.SetValueProperty<TRes>(Expression<Func<T, TRes>> expr, TRes value)
{
var property = GetPropertyInfo(expr);
InjectionMembers.Add(new InjectionProperty(property.Name, value));
return this;
}
private static PropertyInfo GetPropertyInfo(LambdaExpression expr)
{
var memberExpression = expr.Body as MemberExpression;
if (memberExpression == null)
throw new ArgumentException("Simple member expression expected");
var propertyInfo = memberExpression.Member as PropertyInfo;
if (propertyInfo == null)
throw new ArgumentException("Property access expression expected");
return propertyInfo;
}
}
Entry point to improved configuration mechanism can be defined as an extension method for IUnityContainer interface.
public static class UnityContainerExtensions
{
/// <summary>
/// Gathers type information and applies it to provided container.
/// </summary>
/// <remarks>
/// I have used delegate <see cref="configurator"/> in interface for simplification
/// because otherwise I need to create bunch of overloads
/// RegisterType(name, SetLifetimeManager...)
/// RegisterType(name, ...)
/// RegisterType(...)
/// RegisterType(SetLifetimeManager)
/// In current version in case of need it is possible to set SetLifetimeManager and SetName with <see cref="ITypeConfigurator{T}.SetName"/>
/// or <see cref="ITypeConfigurator{T}.SetLifetimeManager"/> methods.
/// </remarks>
public static IUnityContainer RegisterType<TFrom, TTo>(
this IUnityContainer container,
Action<ITypeConfigurator<TTo>> configurator
) where TTo : TFrom
{
var typeConfigurator = new TypeConfigurator<TTo>();
// configurator will accumulate all settings defined by user
configurator(typeConfigurator);
// collected settings are applied to actual container
return container.RegisterType<TFrom, TTo>(
typeConfigurator.Name,
typeConfigurator.LifetimeManager,
typeConfigurator.InjectionMembers.ToArray()
);
}
}
And finally, advanced configurator in action:
var container = new UnityContainer()
.RegisterType<IMyComponent, MyComponent>(
c =>
c
.SetResolvedProperty(_ => _.Logger, "Logger")
.SetValueProperty(_ => _.Text, "DataSource=..")
)
.RegisterType<ILogger, ConsoleLogger>("Logger");
var myComponent = container.Resolve<IMyComponent>();
myComponent.Run();
Summary: unnecesary stuff removed, error-resistance: +10, additional skills: compile-time checks over property names, automatic inference of property types
Комментариев нет:
Отправить комментарий