Click here to Skip to main content
15,867,935 members
Articles / Programming Languages / C#
Article

Introducing the LinFu Framework, Part IV: Simple.IOC – The Five Minute Inversion of Control Container

Rate me:
Please Sign up or sign in to vote.
4.89/5 (25 votes)
15 Nov 2007LGPL312 min read 74K   626   41   19
A fully functional, yet minimalistic inversion of control container

Introduction

In this installment of the series, I'll show you how you can use LinFu's Simple.IOC container to quickly add Dependency Injection capabilities to your application. I'll also discuss some of the common features found in most inversion-of-control containers and I will show you how Simple.IOC can perform those same features without forcing you to decipher its object model (or lack thereof).

Note: If you're wondering what the entire LinFu framework is about, click here.

Background

When I discovered how to write LinFu.DesignByContract1 about three years ago, I quickly realized that I needed to find a way to wrap every object that I created into a contract that LinFu.DesignByContract1 could enforce. The problem was that the only way to tie an object to a contract was to wrap it with an interceptor using LinFu.DynamicProxy upon object instantiation.

The thought of having to clutter my code with direct references to ProxyFactory and LinFu.DesignByContract didn't sound too appealing. I needed a better way to abstract the details of object construction away from the client code. The client should have no knowledge about the existence of LinFu.DesignByContract and all object instantiations should be centralized into a single source.

For me, using an inversion-of-control container was the best way to meet these requirements. I therefore set out to find a library that I could easily use without having to spend more than five minutes reading its documentation or being forced to spend even more time trying to figure out its object model. More importantly, I needed a container so simple that it required little to no configuration. The container should be able to assemble itself without requiring me to write code to wire all the dependencies together. It also shouldn't force me to spend time trying to learn someone else's XML syntax.

What About…?

I looked at various IOC containers such as Castle's Windsor container, Spring.NET and others, but they didn't meet my needs for simplicity. They all had an impressive number of features, but I knew that I wasn't going to use all of their respective feature sets. I also knew that I didn't have the time to write that many configuration files to get my application up and running. Since none of those containers fit my needs, I decided to write my own. That's how Simple.IOC came into existence.

Features and Usage

In the following sections, I'll briefly discuss the features commonly found in an IOC container and I'll show you how easy it is to implement the same features using LinFu. Before I talk about how to use Simple.IOC's features, however, we're going to need a model to use with the container. Here it is:

An Example Model

Note the following example model, in pseudo-code:

A vehicle has an engine.
A vehicle also has a driver, which is a person.
Vehicles can either move or park.
An engine can either start or stop.
A person has a name and an age.

Using this description, one can glean an object model that results in the following code:

C#
public interface IVehicle
{
    void Move();
    void Park();
    IPerson Driver { get; set; }
    IEngine Engine { get; set; }
}

public interface IEngine
{
    void Start();
    void Stop();
}

public interface IPerson
{
    string Name { get; set; }
    int Age { get; set; }
}

Interfaces?

The first thing that you might notice here is that I'm actually using interfaces for the model rather than using either an abstract base class or a concrete class. Using interfaces allows you to easily use other parts of the LinFu framework (in libraries such as DynamicProxy or DynamicObject) and it gives the Simple.IOC container the most amount of flexibility when dealing with your type instances. In any case, this model should be sufficient for this discussion, so let's move on to the next section.

Object Instantiation

One of the most important features that any IOC container can provide is the ability to separate the details of object construction from its actual usage. For the Simple.IOC container, accessing an IVehicle instance is as easy as:

C#
SimpleContainer container = new SimpleContainer();
// (Load the container's dependencies somewhere here..)

IVehicle vehicle = container.GetService<IVehicle>();

// Do something useful with the vehicle
Vehicle.Move();

In this example, the client code knows nothing about the concrete class that implements the IVehicle interface. The client assumes that the container is capable of providing an IVehicle implementation, but at this point we haven't injected any dependencies into the container yet. If that's the case, then how do you inject services into the container?

Dependency Injection

In order to ensure that there is a working implementation of the IVehicle interface, we need to make sure that the container can create instances of IVehicle by providing a concrete class implementation that the container can use. Assuming that I have the following Car class defined in a DLL named CarLibrary.dll:

C#
[Implements(typeof(IVehicle), LifecycleType.OncePerRequest)]
public class Car : IVehicle
{
   private IEngine _engine;
   private IPerson _person;
   
   // Note: The loader can only work with default constructors
   public Car()
   {
   }
   public IEngine Engine
{
      get { return _engine;  }
      set { _engine = value; }
   }
   public IPerson Driver
   {
      get { return _person;  }
      set { _person = value; }
   }
   public void Move()
   {  
      if (_engine == null || _person == null)
         return;

      _engine.Start();
      Console.WriteLine("{0} says: I'm moving!", _person.Name);
   }
   public void Park()
   {
      if (_engine == null || _person == null)
         return;

      _engine.Stop();
      Console.WriteLine("{0} says: I'm parked!" , _person.Name);
   }   
}

Most inversion-of-control containers would use an external XML file to describe what should be injected into the container. That's not the case with LinFu. LinFu, in contrast, only requires a single attribute declaration declared on each concrete class to determine which dependencies should be injected into the container. Since we are only loading CarLibrary.dll, loading that assembly is as easy as:

C#
string directory = AppDomain.CurrentDomain.BaseDirectory;
IContainer container = new SimpleContainer();
Loader loader = new Loader(container);

// Load CarLibrary.dll; If you need load
// all the libaries in a directory, use "*.dll" instead
loader.LoadDirectory(directory, "CarLibrary.dll");

The loader will scan the CarLibrary.dll assembly for any types that have ImplementsAttribute defined and it will inject each of those types into the container. All you have to do is define ImplementsAttribute in your class and the loader will handle the rest. The first attribute parameter describes the interface type that your class will be implementing, while the second parameter denotes the lifecycle of the type that you wish to inject. While the first parameter is self-explanatory, the lifecycle type needs a bit more explanation.

Lifecycle Management

Like other IOC containers, LinFu supports the following lifecycle types:

Once per Request

Every time a client requests a service instance from the container, the container will create a brand new instance for your client to use. No two instances that are instantiated will ever be the same reference. In the previous example, we specified LifecycleType.OncePerRequest on the Car class to indicate that every vehicle created from that container will be a new and separate vehicle.

Once per Thread

In contrast to LifecycleType.OncePerRequest, LifecycleType.OncePerThread tells the loader to create one instance of your target type for every thread that requests an instance of an IVehicle type. This is effectively a thread-wide singleton. The corresponding declaration would be:

C#
[Implements(typeof(IVehicle), LifecycleType.OncePerThread)]
...

If you need an application-wide singleton, on the other hand, then the next option might be what you're looking for.

Singleton Instancing

With the LifecycleType.Singleton option, no matter how many times your client code might request an instance of a particular interface, once that instance has been created by the container, you will always get the same interface instance every time the GetService() method is called. This might be useful if you have a shared component that needs to be accessed from anywhere in your application. The equivalent declaration would be:

C#
[Implements(typeof(IVehicle), LifecycleType.Singleton)]
...

Property Setter Injection

The next thing that you might have noticed is that IVehicle was defined with two respective properties named Driver and Engine, yet none of the examples show you how to make Simple.IOC inject those dependencies into the type automatically. This is because, by default, Simple.IOC lets concrete types inject their own dependencies from the container by having them implement the IInitialize interface:

C#
public interface IInitialize
{
    void Initialize(IContainer container);
}

When an object is created by the container, it checks if the newly-created instance implements IInitialize. If that object implements IInitialize, the container effectively "introduces" itself to that object through the container parameter, allowing that object to initialize itself with any services that the container might already have:

C#
// Note: the differences are highlighted in bold

[Implements(typeof(IVehicle), LifecycleType.OncePerRequest)]
public class Car : IVehicle, IInitialize
{
   // … Code omitted for brevity
   public void Initialize(IContainer container)
   {
      _engine = container.GetService<IEngine>();
      _person = container.GetService<IPerson>();
   }
}

Since each type is responsible for injecting its own property dependencies from the container, the container doesn't have to know anything about injecting those property dependencies into each type. This makes the code for Simple.IOC incredibly easy to maintain. It also allows you to ensure that a container has all the dependencies your components require upon initialization.

Rolling Your Own Property Injector

If, for some reason, Simple.IOC's default property injection behavior doesn't meet your needs, you can always create your own property injector using the IPropertyInjector interface:

C#
public interface IPropertyInjector
{
   bool CanInject(object instance, IContainer sourceContainer);
   void InjectProperties(object instance, IContainer sourceContainer);
}

For example, Simple.IOC uses the following implementation of IPropertyInjector by default:

C#
public class DefaultPropertyInjector : IPropertyInjector
{
   public bool CanInject(object instance, IContainer sourceContainer)
   {
      return instance is IInitialize;
   }
   public void InjectProperties(object instance, IContainer sourceContainer)
   {
      if (!(instance is IInitialize))
         return;

      IInitialize init = instance as IInitialize;
      init.Initialize(sourceContainer);
   }
}

As you can see here, there's nothing special about DefaultPropertyInjector, aside from its simplicity. Once you have written your own custom implementation of IPropertyInjector, all you need to do is add it to the container's PropertyInjectors collection:

C#
MyCustomInjector injector = new MyCustomInjector();
container.PropertyInjectors.add(injector);

Every time the container creates a new object, it asks every IPropertyInjector instance in that collection if it can inject dependencies into that type by using the CanInject() method. Once a property injector tells the container that it can inject properties into the target type, the container calls the InjectProperties() method, leaving the injector to perform the property injection on its own.

Constructor Injection

Unlike other containers, Simple.IOC doesn't support the traditional form of constructor injection. This is because it creates an unnecessary coupling between object instantiation and object initialization. The container should know nothing about the types it creates and it should know nothing about the constructors which that type might contain. The next question you might be asking is, "If the container can only create objects with default constructors, how do I create types that don't have a default constructor?"

Creating Your Own Factory

As it turns out, Simple.IOC makes it easy to create objects that don't have a default constructor. Let's suppose, for example, that the Car class was modified to only have the following constructor:

C#
public Car(IEngine engine, IPerson driver);

...and let's further suppose that every time there was a request for an IVehicle instance, we wanted to use whatever IEngine and IPerson implementations the container had at the time the object was created.

A Layer of Indirection

In order to create your own factory, all you have to do is implement the IFactory(Of T) interface and define FactoryAttribute on your factory class:

C#
[Factory(typeof(IVehicle))]
public class CarFactory : IFactory<IVehicle>
{
   public IVehicle CreateInstance(IContainer container)
   {
      // Get an instance of the engine
      // and the driver
      IEngine engine = container.GetService<IEngine>();
      IPerson driver = container.GetService<IPerson>();
      Car newCar = new Car(engine, driver);

      return newCar;
   }
}

The only thing left to do at this point is load your custom factory into the container. Fortunately, there's no additional code that you have to write in order to make this happen. Once you call the initial Loader.LoadDirectory() method, all dependencies (including custom factories) are loaded into the container automatically. All you have to do is make sure that your custom CarFactoryassembly is placed in the same directory as the one specified in the Loader.LoadDirectory() call. The loader will take care of the rest of the details for you.

Method Call Interception

Like other containers, Simple.IOC has features that allow you to intercept method calls made to instances that the container creates. In order to intercept instances given by the container, all you have to do is implement the ITypeInjector interface:

C#
public interface ITypeInjector
{
   bool CanInject(Type serviceType, object instance);
   object Inject(Type serviceType, object instance);
}

The CanInject() method will tell the container whether or not you can provide an interceptor (presumably a proxy) for that given service type. The Inject() method will replace the service instance with an interceptor itself. Once you have written your own custom implementation of ITypeInjector, all you have to do is add it to the TypeInjectors collection that is defined on the container instance:

C#
ITypeInjector yourInjector = new SomeTypeInjector();
Container.TypeInjectors.Add(yourInjector);

With this approach, transparently adding additional behavior to your interface instances becomes practically trivial when combined with LinFu.DynamicProxy. When used with LinFu.DynamicProxy, adding another set of features to an existing interface implementation only requires you to add another interceptor. I'll leave it to the reader to decide what to do with this feature. The possibilities are endless.

Extensibility

Nearly every IOC container library has at least one or two features that allow it to extend its feature set using plug-ins. In LinFu's case, the Simple.IOC container supports plug-ins through the use of the IContainerPlugin interface:

C#
public interface IContainerPlugin
{
   void BeginLoad(IContainer container);
   void EndLoad(IContainer container);
}

The BeginLoad and EndLoad methods will be called before and after the loader loads the list of types into the container. To implement your own plug-in, all you would have to do is write something similar to the following:

C#
[ContainerPlugin]
public class SamplePlugin : IContainerPlugin
{
   void BeginLoad(IContainer container)
   {
      // Do something useful here
      Console.WriteLine("Load started");
   }
   void EndLoad(IContainer container)
   {
      // Do something useful here too
      Console.WriteLine("Load completed");
   }
}

…and just as it's done with the custom CarFactory implementation, you don't need to add any additional code to load this into the container. The loader will detect the ContainerPluginAttribute declared on SamplePlugin and it will load it into the container once Loader.LoadDirectory() is called.

Points of Interest

Inversion of Control, Implement Thyself

Probably one of the most interesting things that you might notice about the Simple.IOC container is that the container itself was actually built using inversion-of-control principles. It relies heavily on interfaces to do most of its work and it truly knows nothing about the classes that implement those interfaces.

Choosing Configuration through Reflection Instead of XML

While XML is (by definition) extensible and self-describing, it seemed like some developers were reinventing the wheel by trying to describe dependencies that were already contained in every .NET assembly ever compiled. For me, it made more sense to query the existing metadata embedded in an assembly rather than parse an external XML file that had more or less the same information. Thus, I chose to skip implementing an XML-based configuration.

Coming Up in the Next Article

In Part V of this series, I'll show you how you can use LinFu.DesignByContract2 to add language-independent Design by Contract features to your favorite .NET language without having to switch to Eiffel or Eiffel.NET. LinFu.DesignByContract2 allows you to define type contracts in the following fashion:

C#
[ContractFor(typeof(IDbConnection))]
public interface IConnectionContract
{
    [RequireConnectionStringNotEmpty] void Open();
}

RequireConnectionStringNotEmptyAttributeis actually a precondition that is defined as:

C#
[AttributeUsage(AttributeTargets.Method)]
public class RequireConnectionStringNotEmptyAttribute : Attribute, 
    IPrecondition
{
   public bool Check(object target, InvocationInfo info)
   {
      IDbConnection connection = target as IDbConnection;
      
      // If it's a null object, return true and
      // skip the check
      if (connection == null)
          return true;

      return !string.IsNullOrEmpty(connection.ConnectionString);
   }
   public void ShowError(TextWriter output, object traget, InvocationInfo)
   {
      Output.WriteLine("The connection string cannot be null or empty!");
   }
   public bool AppliesTo(object target, InvocationInfo info)
   {
      // Perform this check only on IDbConnection objects
      IDbConnection connection = target as IDbConnection;
      if (connection == null)
          return false;

      return true;
   } 
   public void Catch(Exception ex)
   {
      // If this precondition throws an error,
      // ignore it
   }
}

To make this even more interesting, we're going to be using the contract in VB.NET:

VB
Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports LinFu.DesignByContract2.Injectors
Imports Simple.IoC
Imports Simple.IoC.Loaders

Module Module1
    Sub Main()
        Dim container As New SimpleContainer()
        Dim loader As New Loader(container)

        Dim directory As String = AppDomain.CurrentDomain.BaseDirectory

        ' Explicitly load the contract loader dll
        loader.LoadDirectory(directory, _
            "LinFu.DesignByContract2.Injectors.dll")

        ' Load everything else
        loader.LoadDirectory(directory, "*.dll")

        ' Load the sample IConnection contract that was written in C#
        Dim contractLoader As IContractLoader = _
            container.GetService(Of IContractLoader)()
        contractLoader.LoadDirectory(directory, "SampleContracts.dll")

        ' Manually add a service for IDbConnection
        container.AddService(Of IDbConnection)(New SqlConnection())

        Dim connection As IDbConnection = _
            container.GetService(Of IDbConnection)()

        ' The code will fail at this point since there 
        ' is no connection string defined
        connection.Open()

    End Sub

End Module

Believe it or not, that last call to connection.Open() will never be executed. Instead, the contract verifier will throw the following exception:

Screenshot - exception.png

The error message displayed in the exception should look familiar; it's the same one that we defined in the example above. Think about that for one second. LinFu.DesignByContract allows you to pass contracts around as simple DLL files. Unlike Eiffel, the contracts can actually be swapped between languages and passed around as reusable libraries. The onus is on you to decide what to do with this feature. Suffice it to say that after using LinFu.DbC2, you might never develop the same way again.

Nearly Transparent

Aside from the container setup code in the VB.NET example above, the client is completely oblivious to the existence of a contract. Adding new contracts is as easy as copying new contract libraries to the application directory. Once those contracts have been placed into that directory, the only thing left to do is tell the ContractLoader instance to scan the directory again. So, for those of you who are waiting to have DbC implemented in your favorite .NET language but are still waiting for Microsoft to implement it, there's no need to worry. All you have to do is to wait for Part V of this series and I'll show you how LinFu does language-independent Design by Contract. Stay tuned!

History

  • 15 November, 2007 -- Original version posted

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Software Developer (Senior) Readify
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAdd services as types? Pin
Jakob Borg5-Sep-08 1:00
Jakob Borg5-Sep-08 1:00 
AnswerRe: Add services as types? Pin
Philip Laureano5-Sep-08 2:07
Philip Laureano5-Sep-08 2:07 
AnswerRe: Add services as types? Pin
Jakob Borg5-Sep-08 2:18
Jakob Borg5-Sep-08 2:18 
NewsLinFu IoC 2.0 out in the repository! Pin
Philip Laureano29-Aug-08 3:10
Philip Laureano29-Aug-08 3:10 
Generalboostrapping Pin
the_trickster29-Aug-08 0:21
the_trickster29-Aug-08 0:21 
GeneralRe: boostrapping Pin
Philip Laureano29-Aug-08 2:50
Philip Laureano29-Aug-08 2:50 
GeneralPart V Preview Pin
Philip Laureano7-Dec-07 4:52
Philip Laureano7-Dec-07 4:52 
GeneralA few questions Pin
seesharper25-Nov-07 23:13
seesharper25-Nov-07 23:13 
GeneralRe: A few questions Pin
Philip Laureano26-Nov-07 3:36
Philip Laureano26-Nov-07 3:36 
GeneralExceptions Pin
Daniel Smolka20-Nov-07 6:16
Daniel Smolka20-Nov-07 6:16 
Hi Philip,
I think the container.GetService method should throw an exception when it cannot find the implementation of the requested interface, not just return null. The current behaviour leads to NullReference exceptions without any description if there is any problem.

What happens if you try to add two different implementation of the same interface?

Thanks,
Dan
GeneralRe: Exceptions Pin
Daniel Smolka20-Nov-07 6:22
Daniel Smolka20-Nov-07 6:22 
GeneralRe: Exceptions Pin
Philip Laureano20-Nov-07 13:30
Philip Laureano20-Nov-07 13:30 
GeneralRe: Exceptions Pin
Daniel Smolka20-Nov-07 23:55
Daniel Smolka20-Nov-07 23:55 
GeneralNew Project Page + Forums Pin
Philip Laureano15-Nov-07 12:07
Philip Laureano15-Nov-07 12:07 
GeneralThanks, yet again Pin
Pete O'Hanlon15-Nov-07 9:43
subeditorPete O'Hanlon15-Nov-07 9:43 
GeneralRe: Thanks, yet again Pin
Philip Laureano15-Nov-07 12:04
Philip Laureano15-Nov-07 12:04 
GeneralRe: Thanks, yet again Pin
Philip Laureano4-Dec-07 14:58
Philip Laureano4-Dec-07 14:58 
GeneralRe: Thanks, yet again Pin
Pete O'Hanlon6-Dec-07 10:13
subeditorPete O'Hanlon6-Dec-07 10:13 
GeneralRe: Thanks, yet again Pin
Pete O'Hanlon7-Dec-07 4:27
subeditorPete O'Hanlon7-Dec-07 4:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.