Primitives are also dependencies

There are tons of examples of how Dependency Injection (DI) can be used to decouple clients and services. When the subject is DI, the focus tends to be heavily on the Liskov Substitution Principle (LSP), so most people think about dependencies as polymorphic types (interfaces or abstract base classes). Primitive types like strings and integers tend to be ignored or discouraged. It doesn't help that most DI Containers need extra help to deal with such values.

Primitives are dependencies, too. It doesn't really matter whether or not they are polymorphic. In the end, a dependency is something that the client depends on - hence the name. It doesn't really matter whether the dependency is an interface, a class or a primitive type. In most object-oriented languages, everything is an object - even integers and booleans (although boxing occurs).

There are several ways to inject dependencies into clients. My book describes a set of patterns including Constructor Injection and Property Injection. It's important to keep in mind that ultimately, the reason why Constructor Injection should be your preferred DI pattern has nothing to do with polymorphism. It has to do with protecting the invariants of the class.

Therefore, if the class in question requires a primitive value in order to work, that is a dependency too. Primitive constructor arguments can be mixed with polymorphic arguments. There's really no difference.

Example: a chart reader #

Imagine that you're building a service which provides Top 40 music chart data. There's a ChartController which relies on an IChartReader:

public class ChartController
{
    private readonly IChartReader chartReader;
 
    public ChartController(IChartReader chartReader)
    {
        if (chartReader == null)
            throw new ArgumentNullException("chartReader");
 
        this.chartReader = chartReader;
    }
 
    // ...
}

One implementation of IChartReader is based on a database, so it requires a connection string (a primitive). It also requires a configuration value which establishes the size of the chart:

public class DbChartReader : IChartReader
{
    private readonly int top;
    private readonly string chartConnectionString;
 
    public DbChartReader(int top, string chartConnectionString)
    {
        if (top <= 0)
            throw new ArgumentOutOfRangeException(
                "top",
                "Only positive numbers allowed.");
        if (chartConnectionString == null)
            throw new ArgumentNullException("chartConnectionString");
 
        this.top = top;
        this.chartConnectionString = chartConnectionString;
    }
 
    // ...
}

When top has the value 40, the chart is a Top 40 chart; when the value is 10 it's a Top 10 chart; etc.

Unit testing #

Obviously, a class like DbChartReader is easy to wire up in a unit test:

[Fact]
public void UnitTestingExample()
{
    var sut = new DbChartReader(
        top: 10,
        chartConnectionString: "localhost;foo;bar");
 
    // Act goes here...
    // Assert goes here...
}

Hard-coded composition #

When it's time to bootstrap a complete application, one of the advantages of treating primitives as dependencies is that you have many options for how and where you define those values. At the beginning of an application's lifetime, the best option is often to hard-code some or all of the values. This is as easy to do with primitive dependencies as with polymorphic dependencies:

var controller = new ChartController(
    new DbChartReader(
        top: 40,
        chartConnectionString: "foo"));

This code is part of the application's Composition Root.

Configuration-based composition #

If the time ever comes to move the arms of the the Configuration Complexity Clock towards using the configuration system, that's easy to do too:

var topString = ConfigurationManager.AppSettings["top"];
var top = int.Parse(topString);
 
var chartConnectionString = ConfigurationManager
    .ConnectionStrings["chart"].ConnectionString;
 
var controller = new ChartController(
    new DbChartReader(
        top,
        chartConnectionString));

This is still part of the Composition Root.

Wiring a DI Container with primitives #

Most DI Containers need a little help with primitives. You can configure components with primitives, but you often need to be quite explicit about it. Here's an example of configuring Castle Windsor:

container.Register(Component
    .For<ChartController>());
container.Register(Component
    .For<IChartReader>()
    .ImplementedBy<DbChartReader>()
    .DependsOn(
        Dependency.OnAppSettingsValue("top"),
        Dependency.OnValue<string>(
            ConfigurationManager.ConnectionStrings["chart"]
                .ConnectionString)));

This configures the ChartController type exactly like the previous example, but it's actually more complicated now, and you even lost the feedback from the compiler. That's not good, but you can do better.

Conventions for primitives #

A DI Container like Castle Windsor enables you define your own conventions. How about these conventions?

  • If a dependency is a string and it ends with "ConnectionString", the part of the name before "ConnectionString" is the name of an entry in the app.config's connectionStrings element.
  • If a dependency is a primitive (e.g. an integer) the name of the constructor argument is the key to the appSettings entry.

That would be really nice because it means that you can keep on evolving you application by adding code, and it just works. Need a connection string to the 'artist database'? Just add a constructor argument called "artistConnectionString" and a corresponding artist connection string in your app.config.

Here's how those conventions could be configured with Castle Windsor:

container.Register(Classes
    .FromAssemblyInDirectory(new AssemblyFilter(".")
        .FilterByName(an => an.Name.StartsWith("Ploeh")))
    .Pick()
    .WithServiceAllInterfaces());
 
container.Kernel.Resolver.AddSubResolver(
    new ConnectionStringConventions());
container.Kernel.Resolver.AddSubResolver(
    new AppSettingsConvention());            

The Register call scans all appropriate assemblies in the application's root and registers all components according to the interfaces they implement, while the two sub-resolvers each implement one of the conventions described above.

public class ConnectionStringConventions : ISubDependencyResolver
{
    public bool CanResolve(
        CreationContext context,
        ISubDependencyResolver contextHandlerResolver,
        ComponentModel model,
        DependencyModel dependency)
    {
        return dependency.TargetType == typeof(string)
            && dependency.DependencyKey.EndsWith("ConnectionString");
    }
 
    public object Resolve(
        CreationContext context,
        ISubDependencyResolver contextHandlerResolver,
        ComponentModel model,
        DependencyModel dependency)
    {
        var name = dependency.DependencyKey.Replace("ConnectionString", "");
        return ConfigurationManager.ConnectionStrings[name].ConnectionString;
    }
}

The CanResolve method ensures that the Resolve method is only invoked for string dependencies with names ending with "ConnectionString". If that's the case, the connection string is simply read from the app.config file according to the name.

public class AppSettingsConvention : ISubDependencyResolver
{
    public bool CanResolve(
        CreationContext context,
        ISubDependencyResolver contextHandlerResolver,
        ComponentModel model,
        DependencyModel dependency)
    {
        return dependency.TargetType == typeof(int); // or bool, Guid, etc.
    }
 
    public object Resolve(
        CreationContext context,
        ISubDependencyResolver contextHandlerResolver,
        ComponentModel model,
        DependencyModel dependency)
    {
        var appSettingsKey = dependency.DependencyKey;
        var s = ConfigurationManager.AppSettings[appSettingsKey];
        return Convert.ChangeType(s, dependency.TargetType);
    }
}

This other convention can be used to trigger on primitive dependencies. Since this is a bit of demo code, it only triggers on integers, but I'm sure you'll be able to figure out how to make it trigger on other types as well.

Using convention-based techniques like these can turn a DI Container into a very powerful piece of infrastructure. It just sit there, and it just works, and rarely do you have to touch it. As long as all developers follow the conventions, things just work.


Comments

Convention over configuration - A bit magical, but Nice!
2012-07-02 11:18 UTC
Marcus #
Great post!
2012-07-04 11:42 UTC
Gary McLean Hall #
Do you know if Unity supports this sort of convention?
2012-07-05 20:29 UTC
It's been almost two years since I last worked with Unity, so I don't know - I can't remember all the details of its API. However, I remember Unity as being quite extensible, actually, so I wouldn't be surprised if you could do something like this.

Basically, Unity has very little in terms of Fluent APIs, convention over configuration, etc. but what it does have is a very open architecture. This means it's almost always possible to write a bit of Reflection code to configure Unity by convention.

FWIW, there's a chapter in my book about Unity and its extensibility mechanisms, but to be fair, it doesn't cover exactly this scenario.
2012-07-06 06:16 UTC
Gary McLean Hall #
I haven't gotten that far in your book, yet ;)

It appears it is possible, and that someone has created an extension for convention:

http://aspiringcraftsman.com/2009/06/13/convention-based-registration-extension/
2012-07-06 14:34 UTC
It seems like a over-engineering to me. Make things simple like in "Wiring a DI Container with primitives" is the best option for me. The other options below seems confuse and unnecessary to me.
2012-09-08 22:31 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 02 July 2012 09:26:30 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 02 July 2012 09:26:30 UTC