This article describes how to extend the Simple Injector with convension based configuration for primitive constructor arguments.
When working with dependency injection, services (classes that contain behavior) depend on other services. The general idea is to inject those services into the constructor of the 'parent' service. Primitive types are no services, since they contain no behavior, and I normally advice not to mix primitive types and services in a single constructor. My advice would normally be:
- Extract and group the primitives in their own 'configuration' type and inject that type into the service, or
- Move those primitives to properties and use property injection.
I find property injection nice, since those primitives are almost always system configuration values and removing them from the constructor (and thus separating them from the required service dependencies) seems very clean.
Property injection however, is supposed to be used for optional dependencies, which means that not injecting such dependency should allow the system to keep running. A connection string however, is hardly ever optional, since without a connection string, it will be impossible to connect to the database. But since I don't really see those configuration values as 'real' dependencies, I personally don't mind using property injection.
Mixing primitives and services in the constructor however, can have a benefit, as explained by Mark Seemann in his blog post about Primitive Dependencies. In that post, Mark shows how to use convention over configuration on primitive dependencies. For instance, by naming a string dependency 'xxxConnectionString', we can load the value by name 'xxx' directly from the <connectionStrings> section of the application's configuration file. Or an primitive dependency, who's name ends with 'AppSettings', can be retrieved directly from the <appSettings> section.
Personally, I'm not sure whether I would like these types of conventions, because the name of the value in the configuration file, will be coupled to your code. Though, I must admit that it can make the container’s configuration simpler, since you won't have to create a new configuration type, use property injection, or fallback to using a lambda expression in registering the type. So let's see how we can implement such convention over configuration feature in the Simple Injector.
Simple Injector contains extension points for changing the way constructor injection works. By default, Simple Injector disallows registering and injecting value types and strings (which is a good default). The trick is to change the constructor parameter verification behavior (defined by the IConstructorVerificationBehavior interface) and the constructor injection behavior (defined by the IConstructorInjectionBehavior).
By replacing the default implementations of these abstractions, we can change Simple Injector to work the way we want, which is to allow convention over configuration.
Let's start by defining an abstraction for conventions on constructor parameters:
public interface IParameterConvention
{
bool CanResolve(ParameterInfo parameter);
Expression BuildExpression(ParameterInfo parameter);
}
This interface implements the tester-doer pattern. We can ask the convention whether it can resolve the supplied parameter, and if it can, BuildExpression allows us to create an Expression object that defines the constructor argument. Simple Injector works with expression trees under the covers, which allows it to compile delegates with performance that is very close to newing types up manually. By letting a convention return an Expression, we will have best performance, and most flexibility in what and how a parameter must be injected.
Mark Seemann uses a convention for connection strings and app settings. Let's stick with that example and those two conventions. Let's start with the ConnectionStringsConvention:
public class ConnectionStringsConvention : IParameterConvention
{
private const string ConnectionStringPostFix =
"ConnectionString";
[DebuggerStepThrough]
public bool CanResolve(ParameterInfo parameter)
{
bool resolvable =
parameter.ParameterType == typeof(string) &&
parameter.Name.EndsWith(ConnectionStringPostFix) &&
parameter.Name.LastIndexOf(ConnectionStringPostFix) > 0;
if (resolvable)
{
this.VerifyConfigurationFile(parameter);
}
return resolvable;
}
[DebuggerStepThrough]
public Expression BuildExpression(ParameterInfo parameter)
{
var constr = this.GetConnectionString(parameter);
return Expression.Constant(constr, typeof(string));
}
[DebuggerStepThrough]
private void VerifyConfigurationFile(ParameterInfo parameter)
{
this.GetConnectionString(parameter);
}
[DebuggerStepThrough]
private string GetConnectionString(ParameterInfo parameter)
{
string name = parameter.Name.Substring(0,
parameter.Name.LastIndexOf(ConnectionStringPostFix));
ConnectionStringSettings settings =
ConfigurationManager.ConnectionStrings[name];
if (settings == null)
{
throw new ActivationException(
"No connection string with name '" + name +
"' could be found in the application's " +
"configuration file.");
}
return settings.ConnectionString;
}
}
This ConnectionStringsConvention does a few interesting things. Its CanResolve method checks to see if the supplied parameter is of type string and its name ends with 'ConnectionString'. If not, CanResolve returns false immediately, which means that we will fall back on Simple Injector’s default validation behavior. If the parameter matches, CanResolve will check if the value can be found in the <connectionStrings> section of the application's configuration file. An exception will be thrown when this is not the case. The CanResolve will get called during the registration process, and throwing an exception therefore allows us to let the application fail immediately when an invalid registration is made.
Compared to the CanResolve, the BuildExpression method pretty simple. It retrieves the connection string value from the configuration file, wraps it in an expression and returns that expression. Since the configuration file can't change during the lifetime of an application (changes either have no effect, or in case of a web application, will cause the application to be restarted), it would be useless to reread the value every time a new instance of the depending type is created. The value is constant, and we can safely return a ConstantExpression. This also yields the best performance.
The AppSettingsConvention looks similar to the previous ConnectionStringsConvention. It too checks to see if the value exists in the configuration file. However, while the ConnectionStringsConvention would only deal with strings, the AppSettingsConvention can work with strings and any arbitrary value type, that can be converted from a string (using.NET’s built-in TypeConverter system):
public class AppSettingsConvention : IParameterConvention
{
private const string AppSettingsPostFix = "AppSetting";
[DebuggerStepThrough]
public bool CanResolve(ParameterInfo parameter)
{
Type type = parameter.ParameterType;
bool resolvable =
(type.IsValueType || type == typeof(string)) &&
parameter.Name.EndsWith(AppSettingsPostFix) &&
parameter.Name.LastIndexOf(AppSettingsPostFix) > 0;
if (resolvable)
{
this.VerifyConfigurationFile(parameter);
}
return resolvable;
}
[DebuggerStepThrough]
public Expression BuildExpression(ParameterInfo parameter)
{
object valueToInject = this.GetAppSettingValue(parameter);
return Expression.Constant(valueToInject,
parameter.ParameterType);
}
[DebuggerStepThrough]
private void VerifyConfigurationFile(ParameterInfo parameter)
{
this.GetAppSettingValue(parameter);
}
[DebuggerStepThrough]
private object GetAppSettingValue(ParameterInfo parameter)
{
string key = parameter.Name.Substring(0,
parameter.Name.LastIndexOf(AppSettingsPostFix));
string configurationValue =
ConfigurationManager.AppSettings[key];
if (configurationValue == null)
{
throw new ActivationException(
"No app setting with key '" + key + "' " +
"could be found in the application's " +
"configuration file.");
}
TypeConverter converter = TypeDescriptor.GetConverter(
parameter.ParameterType);
return converter.ConvertFromString(null,
CultureInfo.InvariantCulture, configurationValue);
}
}
Now we've got two IParameterConvention implementations, we need to allow plugging these implementations in the Simple Injector auto-wiring pipeline. We need create both a IConstructorVerificationBehavior and a IConstructorInjectionBehavior implementation:
internal class ConventionConstructorVerificationBehavior
: IConstructorVerificationBehavior
{
private IConstructorVerificationBehavior decorated;
private IParameterConvention convention;
public ConventionConstructorVerificationBehavior(
IConstructorVerificationBehavior decorated,
IParameterConvention convention)
{
this.decorated = decorated;
this.convention = convention;
}
public void Verify(ParameterInfo parameter)
{
if (!this.convention.CanResolve(parameter))
{
this.decorated.Verify(parameter);
}
}
}
This ConventionConstructorVerificationBehavior is a decorator. It extends the container's original behavior with convention support. By extending the original behavior, it allows us to apply multiple conventions, or even mix it with other plug-ins that changed the default behavior of the container. The IConstructorInjectionBehavior implementation looks very alike:
internal class ConventionConstructorInjectionBehavior
: IConstructorInjectionBehavior
{
private IConstructorInjectionBehavior decorated;
private IParameterConvention convention;
public ConventionConstructorInjectionBehavior(
IConstructorInjectionBehavior decorated,
IParameterConvention convention)
{
this.decorated = decorated;
this.convention = convention;
}
public Expression BuildParameterExpression(
ParameterInfo parameter)
{
if (!this.convention.CanResolve(parameter))
{
return this.decorated
.BuildParameterExpression(parameter);
}
return this.convention.BuildExpression(parameter);
}
}
Just one thing is missing, and that is a convenient extension method, that makes registering a new IParameterConvention simple:
public static void RegisterParameterConvention(
this ContainerOptions options,
IParameterConvention convention)
{
options.ConstructorVerificationBehavior =
new ConventionConstructorVerificationBehavior(
options.ConstructorVerificationBehavior,
convention);
options.ConstructorInjectionBehavior =
new ConventionConstructorInjectionBehavior(
options.ConstructorInjectionBehavior,
convention);
}
This extension method works over the ContainerOptions class and replaces the option's original ConstructorVerificationBehavior and ConstructorInjectionBehavior with our specially crafted versions, while wrapping the original implementations. With this in place, we can use these extensions as follows:
var container = new Container();
// Add the parameter convensions:
container.Options.RegisterParameterConvention(
new ConnectionStringsConvention());
container.Options.RegisterParameterConvention(
new AppSettingsConvention());
// Registrations here
container.Register<IDbContext, MyDbContext>();
And there you have it. Convention support for primitive dependencies with the Simple Injector.
Happy injecting!
Republished from .NET Junkie [7 clicks].
Read the original version here [1 clicks].