Unity – Part 5: Injecting Values
- by Ricardo Peres
Introduction This is the fifth post on Unity. You can find the introductory post here, the second post, on dependency injection here, a third one on Aspect Oriented Programming (AOP) here and the latest so far, on writing custom extensions, here. This time we will talk about injecting simple values. An Inversion of Control (IoC) / Dependency Injector (DI) container like Unity can be used for things other than injecting complex class dependencies. It can also be used for setting property values or method/constructor parameters whenever a class is built. The main difference is that these values do not have a lifetime manager associated with them and do not come from the regular IoC registration store. Unlike, for instance, MEF, Unity won’t let you register as a dependency a string or an integer, so you have to take a different approach, which I will describe in this post. Scenario Let’s imagine we have a base interface that describes a logger – the same as in previous examples: 1: public interface ILogger
2: {
3: void Log(String message);
4: }
And a concrete implementation that writes to a file:
1: public class FileLogger : ILogger
2: {
3: public String Filename
4: {
5: get;
6: set;
7: }
8:
9: #region ILogger Members
10:
11: public void Log(String message)
12: {
13: using (Stream file = File.OpenWrite(this.Filename))
14: {
15: Byte[] data = Encoding.Default.GetBytes(message);
16:
17: file.Write(data, 0, data.Length);
18: }
19: }
20:
21: #endregion
22: }
And let’s say we want the Filename property to come from the application settings (appSettings) section on the Web/App.config file.
As usual with Unity, there is an extensibility point that allows us to automatically do this, both with code configuration or statically on the configuration file.
Extending Injection
We start by implementing a class that will retrieve a value from the appSettings by inheriting from ValueElement:
1: sealed class AppSettingsParameterValueElement : ValueElement, IDependencyResolverPolicy
2: {
3: #region Private methods
4: private Object CreateInstance(Type parameterType)
5: {
6: Object configurationValue = ConfigurationManager.AppSettings[this.AppSettingsKey];
7:
8: if (parameterType != typeof(String))
9: {
10: TypeConverter typeConverter = this.GetTypeConverter(parameterType);
11:
12: configurationValue = typeConverter.ConvertFromInvariantString(configurationValue as String);
13: }
14:
15: return (configurationValue);
16: }
17: #endregion
18:
19: #region Private methods
20: private TypeConverter GetTypeConverter(Type parameterType)
21: {
22: if (String.IsNullOrEmpty(this.TypeConverterTypeName) == false)
23: {
24: return (Activator.CreateInstance(TypeResolver.ResolveType(this.TypeConverterTypeName)) as TypeConverter);
25: }
26: else
27: {
28: return (TypeDescriptor.GetConverter(parameterType));
29: }
30: }
31: #endregion
32:
33: #region Public override methods
34: public override InjectionParameterValue GetInjectionParameterValue(IUnityContainer container, Type parameterType)
35: {
36: Object value = this.CreateInstance(parameterType);
37: return (new InjectionParameter(parameterType, value));
38: }
39: #endregion
40:
41: #region IDependencyResolverPolicy Members
42:
43: public Object Resolve(IBuilderContext context)
44: {
45: Type parameterType = null;
46:
47: if (context.CurrentOperation is ResolvingPropertyValueOperation)
48: {
49: ResolvingPropertyValueOperation op = (context.CurrentOperation as ResolvingPropertyValueOperation);
50: PropertyInfo prop = op.TypeBeingConstructed.GetProperty(op.PropertyName);
51: parameterType = prop.PropertyType;
52: }
53: else if (context.CurrentOperation is ConstructorArgumentResolveOperation)
54: {
55: ConstructorArgumentResolveOperation op = (context.CurrentOperation as ConstructorArgumentResolveOperation);
56: String args = op.ConstructorSignature.Split('(')[1].Split(')')[0];
57: Type[] types = args.Split(',').Select(a => Type.GetType(a.Split(' ')[0])).ToArray();
58: ConstructorInfo ctor = op.TypeBeingConstructed.GetConstructor(types);
59: parameterType = ctor.GetParameters().Where(p => p.Name == op.ParameterName).Single().ParameterType;
60: }
61: else if (context.CurrentOperation is MethodArgumentResolveOperation)
62: {
63: MethodArgumentResolveOperation op = (context.CurrentOperation as MethodArgumentResolveOperation);
64: String methodName = op.MethodSignature.Split('(')[0].Split(' ')[1];
65: String args = op.MethodSignature.Split('(')[1].Split(')')[0];
66: Type[] types = args.Split(',').Select(a => Type.GetType(a.Split(' ')[0])).ToArray();
67: MethodInfo method = op.TypeBeingConstructed.GetMethod(methodName, types);
68: parameterType = method.GetParameters().Where(p => p.Name == op.ParameterName).Single().ParameterType;
69: }
70:
71: return (this.CreateInstance(parameterType));
72: }
73:
74: #endregion
75:
76: #region Public properties
77: [ConfigurationProperty("appSettingsKey", IsRequired = true)]
78: public String AppSettingsKey
79: {
80: get
81: {
82: return ((String)base["appSettingsKey"]);
83: }
84:
85: set
86: {
87: base["appSettingsKey"] = value;
88: }
89: }
90: #endregion
91: }
As you can see from the implementation of the IDependencyResolverPolicy.Resolve method, this will work in three different scenarios:
When it is applied to a property;
When it is applied to a constructor parameter;
When it is applied to an initialization method.
The implementation will even try to convert the value to its declared destination, for example, if the destination property is an Int32, it will try to convert the appSettings stored string to an Int32.
Injection By Configuration
If we want to configure injection by configuration, we need to implement a custom section extension by inheriting from SectionExtension, and registering our custom element with the name “appSettings”:
1: sealed class AppSettingsParameterInjectionElementExtension : SectionExtension
2: {
3: public override void AddExtensions(SectionExtensionContext context)
4: {
5: context.AddElement<AppSettingsParameterValueElement>("appSettings");
6: }
7: }
And on the configuration file, for setting a property, we use it like this:
1: <appSettings>
2: <add key="LoggerFilename" value="Log.txt"/>
3: </appSettings>
4: <unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
5: <container>
6: <register type="MyNamespace.ILogger, MyAssembly" mapTo="MyNamespace.ConsoleLogger, MyAssembly"/>
7: <register type="MyNamespace.ILogger, MyAssembly" mapTo="MyNamespace.FileLogger, MyAssembly" name="File">
8: <lifetime type="singleton"/>
9: <property name="Filename">
10: <appSettings appSettingsKey="LoggerFilename"/>
11: </property>
12: </register>
13: </container>
14: </unity>
If we would like to inject the value as a constructor parameter, it would be instead:
1: <unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
2: <sectionExtension type="MyNamespace.AppSettingsParameterInjectionElementExtension, MyAssembly" />
3: <container>
4: <register type="MyNamespace.ILogger, MyAssembly" mapTo="MyNamespace.ConsoleLogger, MyAssembly"/>
5: <register type="MyNamespace.ILogger, MyAssembly" mapTo="MyNamespace.FileLogger, MyAssembly" name="File">
6: <lifetime type="singleton"/>
7: <constructor>
8: <param name="filename" type="System.String">
9: <appSettings appSettingsKey="LoggerFilename"/>
10: </param>
11: </constructor>
12: </register>
13: </container>
14: </unity>
Notice the appSettings section, where we add a LoggerFilename entry, which is the same as the one referred by our AppSettingsParameterInjectionElementExtension extension.
For more advanced behavior, you can add a TypeConverterName attribute to the appSettings declaration, where you can pass an assembly qualified name of a class that inherits from TypeConverter. This class will be responsible for converting the appSettings value to a destination type.
Injection By Attribute
If we would like to use attributes instead, we need to create a custom attribute by inheriting from DependencyResolutionAttribute:
1: [Serializable]
2: [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
3: public sealed class AppSettingsDependencyResolutionAttribute : DependencyResolutionAttribute
4: {
5: public AppSettingsDependencyResolutionAttribute(String appSettingsKey)
6: {
7: this.AppSettingsKey = appSettingsKey;
8: }
9:
10: public String TypeConverterTypeName
11: {
12: get;
13: set;
14: }
15:
16: public String AppSettingsKey
17: {
18: get;
19: private set;
20: }
21:
22: public override IDependencyResolverPolicy CreateResolver(Type typeToResolve)
23: {
24: return (new AppSettingsParameterValueElement() { AppSettingsKey = this.AppSettingsKey, TypeConverterTypeName = this.TypeConverterTypeName });
25: }
26: }
As for file configuration, there is a mandatory property for setting the appSettings key and an optional TypeConverterName for setting the name of a TypeConverter.
Both the custom attribute and the custom section return an instance of the injector AppSettingsParameterValueElement that we implemented in the first place. Now, the attribute needs to be placed before the injected class’ Filename property:
1: public class FileLogger : ILogger
2: {
3: [AppSettingsDependencyResolution("LoggerFilename")]
4: public String Filename
5: {
6: get;
7: set;
8: }
9:
10: #region ILogger Members
11:
12: public void Log(String message)
13: {
14: using (Stream file = File.OpenWrite(this.Filename))
15: {
16: Byte[] data = Encoding.Default.GetBytes(message);
17:
18: file.Write(data, 0, data.Length);
19: }
20: }
21:
22: #endregion
23: }
Or, if we wanted to use constructor injection:
1: public class FileLogger : ILogger
2: {
3: public String Filename
4: {
5: get;
6: set;
7: }
8:
9: public FileLogger([AppSettingsDependencyResolution("LoggerFilename")] String filename)
10: {
11: this.Filename = filename;
12: }
13:
14: #region ILogger Members
15:
16: public void Log(String message)
17: {
18: using (Stream file = File.OpenWrite(this.Filename))
19: {
20: Byte[] data = Encoding.Default.GetBytes(message);
21:
22: file.Write(data, 0, data.Length);
23: }
24: }
25:
26: #endregion
27: }
Usage
Just do:
1: ILogger logger = ServiceLocator.Current.GetInstance<ILogger>("File");
And off you go! A simple way do avoid hardcoded values in component registrations. Of course, this same concept can be applied to registry keys, environment values, XML attributes, etc, etc, just change the implementation of the AppSettingsParameterValueElement class.
Next stop: custom lifetime managers.