Development With A Dot

Blog on development in general, and specifically on .NET

Sponsors

News

My Friends

My Links

Permanent Posts

Portuguese Communities

Unity – Part 5: Injecting Values

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.

Comments

No Comments