Greg Beech's Website

Write your own configurable service container, Part 1

In my last entry I described how to extend the ASP.NET cache to allow it to be hosted out-of-process as a distributed cache, and it turned out that the hardest part of it would be to change your existing code to use it rather than the in-process cache if you weren't using a configurable service container. If you aren't sure what a service container is or why you would use one, read Martin Fowler's article for some background.

I've used off-the-shelf service containers such as Spring.NET before, however they are generally very complex with about a million layers of hierarchy and base classes with names like AbstractAutowireCapableObjectFactory which makes it incredibly difficult to understand and fix any issues you come across. But more importantly, as with many third party components, they support a lot of features you don't care about and don't support a lot of the features you do want. A couple of the ones I wanted were:

  1. Lazy loading of singleton services so that I could use one configuration file for all the services, and our different applications wouldn't care whether they had references to the assemblies unless they needed to actually load the service. This makes maintenance simpler as you can have a single service configuration file describing all services rather than a separate one for each application that needs to access then.
  2. Configurable wiring up of events, so that arbitrary events from a service can be handled either synchronously or asynchronously by other services. This allows us to do things like auditing and email notifications without polluting the code of services dealing with particular activities and thus violating the single responsibility principle (in essence it allows a declarative chain of responsibility).

This series presents some of the fundamental ideas and code needed to write a configurable service container, but without being prescriptive because your requirements are probably different to mine, and indeed on other projects with different architectures my own requirements might differ again. Although we're rewriting functionality that already exists in many different forms, the beauty of this is that you'll understand it completely and be able to extend it and branch it and fix it to meet your exact needs and quality standards without any third party dependencies; Not Invented Here syndrome is not always a bad thing.

To start off with, lets set out the framework for a very simple container that supports only constructor injection (as that's my preferred model to ensure that services are fully instantiated) and singleton service instances (which is more efficient that constructing new services each time). We'll build on this over the next few entries until we have something that's genuinely useful and should cover most scenarios you'll need a service container for.

Firstly we need to define to the container what the service hierarchy looks like. Below are three classes defining a constructor argument which refers to a service to be passed in at a given argument index, a service class with a name, type, and collection of constructor arguments, and a keyed collection of services so we can find them by name as an O(1) operation. I've used the C# 3.0 auto-property syntax to keep the classes concise.

public class ConstructorArg
{
    public int Index { get; set; }
    public string ServiceRef { get; set; }
}

public class Service
{
    public Service()
    {
        this.Args = new Collection<ConstructorArg>();
    }

    public string Name { get; set; }
    public string Type { get; set; }
    public Collection<ConstructorArg> Args { get; private set; }
}

public class ServiceCollection : KeyedCollection<string, Service>
{
    protected override string GetKeyForItem(Service item)
    {
        return item.Name;
    }
}

Next we need the framework for our container. Because we don't want to dictate how the service configuration classes must be loaded, for example they could be represented as XML or in a database, we'll make it abstract and take the configuration as a constructor argument. The class also contains a dictionary of singleton services that will be populated as they are loaded, keyed by the name of the service

public abstract class RuntimeServiceContainer
{
    private readonly ServiceCollection config;
    private readonly IDictionary<string, object> singletons;

    protected RuntimeServiceContainer(ServiceCollection config)
    {
        this.singletons = new Dictionary<string, object>();
        this.config = config;
    }

    public object GetService(string name)
    {
        if (this.singletons.ContainsKey(name))
        {
            return this.singletons[name];
        }
        else
        {
            return this.LoadService(name);
        }
    }

    public T GetService<T>(string name)
    {
        return (T)this.GetService(name);
    }

    private object LoadService(string name)
    {
        lock (this.services)
        {
            return this.LoadServiceHierarchy(name, new Stack<string>());
        }
    }

    private object LoadServiceHierarchy(string name, Stack<string> constructing)
    {
        throw new NotImplementedException();
    }
}

To access the services there is a GetService method, which also has a generic overload that casts the service to the required return type for caller convenience. These check whether there is already a singleton service by that name, and if not pass through to the LoadService method, which contains the first bits of interesting code.

It may seem odd to lock all services while loading one which means that other requests for services may be blocked, however it makes things much easier to manage as the loading process may create other singletons and otherwise we'd be messing around with reader/writer locks during the loading process. In practice if all the services are singletons then they won't spend a significant portion of the application's lifetime being loaded so it isn't worth optimising this locking; if we allow non-singleton services as well then we may have to revisit it.

The other thing that may appear odd is the stack being passed to LoadServiceHierarchy. Because the services may refer to other services, which may themselves refer to other services, LoadServiceHierarchy will be a recursive method and so we need to keep track of which services are currently under construction to ensure we don't get circular references resulting in a stack overflow. Each time the method is entered we'll check that the service is not already on the stack, push the name of the service onto the stack, and then pop it off at the end of the method. By asserting that the popped value is the name of the service we can also ensure that we have managed the recursion correctly.

In the next entry we'll implement the LoadServiceHierarchy method and create a concrete implementation of the container that can load the service configuration from XML, so we'll have a working service container which is actually sufficient for the majority of people's requirements.


Posted Dec 26 2007, 11:55 AM by Greg Beech

Comments

Greg Beech's Tech Blog wrote Write your own configurable service container, Part 2
on 12-27-2007 11:01 AM

In the first entry in this series I outlined why it might be better to write your own service container

Add a Comment

(required)  
(optional)
(required)  
Remember Me?

Enter the numbers above:
Copyright (C) Greg Beech. All rights reserved.
Powered by Community Server (Non-Commercial Edition), by Telligent Systems