Click here to Skip to main content
15,885,278 members
Articles / Programming Languages / C#

Introducing Hiro, the World's Fastest IOC Container, Part III: The Container Itself

Rate me:
Please Sign up or sign in to vote.
4.80/5 (3 votes)
28 Jul 2009LGPL38 min read 14.7K   8  
How ridiculously simple it is to use Hiro in your own applications without sacrificing speed for simplicity

Introduction

In this final installment of the Hiro series, I'll show you just how ridiculously simple it is to use Hiro in your own applications without sacrificing speed for simplicity. I'll also show you how you can use Hiro's registration conventions so that you will never have to worry about managing any external configuration files or declare any external attributes on your types just so you can get your types registered into a service container.

Simply Fast, and Simply Simple

As I mentioned in my previous posts, Hiro lacks many of the "fat" features that you would normally associate with other IOC containers (including LinFu). At first, that might seem like a disadvantage for potential Hiro users, but it also implies that you won't have to waste any time trying to understand any IOC container features that you probably will never use in most of your applications. Instead, Hiro focuses on doing three things:

  1. Determine the list of available services from any given assembly
  2. Registering those services into a container
  3. Ensuring that the container can create the services that you registered

Unlike other IOC containers, Hiro doesn't need you to use fluent interfaces or attributes to register your types into a container. You don't even need lambda functions or an external XML configuration file to get your services up and running with Hiro. Instead, Hiro uses Convention over Configuration semantics that let you register your services with only a few lines of code. Here's an example:

C#
// Use the loader to scan the target assembly
// for services
var loader = new DependencyMapLoader();

// The loader will handle all the details of loading
// the services into the container for you
DependencyMap map = loader.LoadFromBaseDirectory("SampleDomain.dll");
var container = map.CreateContainer();

// (Do something useful with the container here)
var greeter = container.GetInstance<IGreeter>();

// ...

"Convention Over...what?"

At first glance, it's hard to believe that Hiro can load all the services from a given assembly (or a set of assemblies) using only the code that was given in the previous example. After all, any given service can have nearly an infinite amount of constructor and property injection dependencies, and given the sheer complexity of this task, there must be a set of rules that Hiro follows that somehow makes the registration happen "automagically". At the same time, there might be some end-user developers out there that might want to have strict control over the actual services that will be registered into the compiled container. In other words, how does Hiro know exactly what to register and still be able to let users control what gets registered into a compiled container?

A Few Rules Go A Long Way

Now the reason why Hiro can do so much in so few lines of code is because that it implements a few conventions that typical IOC container users would follow when registering their own types. As long as users follow those conventions, they will never have to worry about manually registering any of their service types with Hiro. For example, let's assume that you had this particular service type declared in a single assembly:

C#
public interface IGreeter
{
    void Greet(string name);
}

Let's also assume that you have three concrete classes that implement the same IGreeter interface:

C#
public class GermanGreeter : IGreeter
{
    public void Greet(string name)
    {
        Console.WriteLine("German: Hallo, {0}!!");
    }
}

public class FrenchGreeter : IGreeter
{
    public void Greet(string name)
    {
        Console.WriteLine("French: Bonjour, {0}!", name);
    }
}

public class Greeter : IGreeter
{
    public void Greet(string name)
    {
        Console.WriteLine("English: Hello, {0}!", name);
    }
}

"Just Give Me the Default Service, Please!"

Based on the examples above, it probably didn't take you very long to infer that the Greeter class is the default implementation for the IGreeter interface. Furthermore, it's probably safe to assume that a default implementation exists if you have an interface named IYourService and if you already have a concrete class named YourService that implements that particular service type. Hiro follows the same convention, so calling container.GetInstance on an IGreeter type will yield a Greeter instance:

C#
// Load the sample assembly
var loader = new DependencyMapLoader();
DependencyMap map = loader.LoadFromBaseDirectory("SampleDomain.dll");
var container = map.CreateContainer();

// Get the default implementation
var greeter = container.GetInstance<IGreeter>();

// Determine the actual type that implements the IGreeter interface
var greeterType = greeter.GetType();

// This expression will return true
Console.WriteLine(greeterType == typeof(Greeter));

As you can see, the example above is fairly self-explanatory. The DependencyMapLoader class will automatically determine that the Greeter type is the default implementation for the IGreeter interface and register it into the DependencyMap. Of course, this approach assumes that you have a class with a type name that matches the service type name; however, there might be cases where you have multiple classes that implement the same interface but don't follow this naming convention. For example, if I have the FrenchGreeter and a GermanGreeter class but don't have a default Greeter class, how does Hiro determine which implementation should be used as the default implementation type?

When All Else Fails, Use the Alphabet

The answer is that by default, Hiro will pick the first implementation type from a list of service implementations that have been sorted alphabetically by type name. In this case, Hiro will choose the FrenchGreeter as the default IGreeter implementation since the FrenchGreeter class name alphabetically appears before the GermanGreeter class name:

C#
// ...

// Get the default implementation
var greeter = container.GetInstance<IGreeter>();

// Determine the actual type that implements the IGreeter interface
var greeterType = greeter.GetType();

// This expression will return true since
// the FrenchGreeter type appears before the GermanGreeter type
Console.WriteLine(greeterType == typeof(FrenchGreeter));

While this approach might be useful for determining (and thus accessing) a default service implementation, there has to be a way to access the other concrete IGreeter implementations that reside in the compiled container. As it turns out, Hiro makes this process just as equally effortless:

C#
// Get the French and German greeters
var frenchGreeter = container.GetInstance<IGreeter>("FrenchGreeter");
var germanGreeter = container.GetInstance<IGreeter>("GermanGreeter");

By default, Hiro registers each service implementation using the class name of each concrete service type. In order to access a particular service implementation, all you need to do is pass the name of the implementing type in the container.GetInstance<T> call, and the container will instantiate the corresponding type for you.

Using Constructor Injection and Property Injection

Now that we have an idea of how Hiro registers service types into a container, the next thing that you might be wondering about is exactly how Hiro injects dependencies into constructors and properties. For example, if I have a class named GreeterHost that takes one IGreeter dependency in its constructor, how do I tell Hiro to use that constructor instead of the default constructor? Let's take a look at the GreeterHost class:

C#
public class GreeterHost
{
  public GreeterHost ()
  {
  }
  public GreeterHost(IGreeter greeter)
  {
     // Do something useful here
  }

  // ...
}

Let's also assume that I manually registered the GreeterHost class with the DependencyMap using the DependencyMap.AddService method. Here's how you get Hiro to perform constructor injection:

C#
// Manually add the GreeterHost to the dependency map
var map = new DependencyMap();
map.AddService<GreeterHost, GreeterHost>();

// Manually add an IGreeter service so that Hiro uses the constructor
// with the IGreeter parameter
map.AddService<IGreeter, Greeter>();

// Compile the container
var container = map.CreateContainer();

// Create the GreeterHost. Hiro will automatically
// inject the IGreeter implementation into the constructor
var greeterHost = container.GetInstance<GreeterHost>();

As you can see from the example above, Hiro automatically used the contents of the dependency map to determine which constructor should be used for instantiating the GreeterHost type. The only thing that I needed to do to perform constructor injection was to make sure that the IGreeter service type was available in the dependency map once the container was compiled. The compiled container, in turn, used the IGreeter dependency to call the correct GreeterHost constructor.

Singletons Galore

NOTE: If you prefer to have your Greeter type registered as a singleton in the dependency map, however, here's how you do it:

C#
// Manually add an IGreeter service so that Hiro uses the constructor
// with the IGreeter parameter
map.AddSingletonService<IGreeter, Greeter>();

Constructor, Inject Thyself

The best part about the constructor injection example with Hiro is the fact that it handles all the gory details of performing constructor injection for you in the most minimal lines of code possible. In this case, the only convention that you have to follow is to make sure that the dependency map contains all the necessary services for Hiro to call the appropriate constructor. What makes this even more interesting is that Hiro can perform automatic constructor injection with constructors that have any arbitrary number of constructor arguments. In other words, if the DependencyMap instance has the constructor dependency, then Hiro can handle the injection.

Property, Inject Thyself Too

For the most part, Hiro's property injection follows some of the same conventions as constructor injection. Much like constructor injection, the only thing you need to do to use property injection with Hiro is to make sure that services required by the target property are already registered with the dependency map once the container is compiled. For example, let's assume that GreeterHost class as an IGreeterProperty named CurrentGreeter:

C#
public class GreeterHost
{
  public IGreeter CurrentGreeter
  {
     get;set;     
  }
}

Here's how you set up Hiro's dependency map to perform property injection:

C#
// Manually add the GreeterHost to the dependency map
var map = new DependencyMap();
map.AddService<GreeterHost, GreeterHost>();

// Manually add an IGreeter service so that Hiro uses the constructor
// with the IGreeter parameter
map.AddService<IGreeter, Greeter>();

The first thing that you might notice is that the setup code for property injection and constructor injection are both identical to each other. In fact, you could even say that I just "copy & pasted" the example from previous examples in this article. Indeed, that was intentional, and the point here is that are only two simple things that you need to do to make Hiro automatically perform property injection on your types:

  1. Ensure that the service types that your properties require are registered with the dependency map.
  2. Ensure that the properties that will be injected are not read-only properties.

In other words, Hiro's property injection conventions follow one of the most basic dependency injection conventions of all:

If you don't want Hiro to inject dependencies into your target property, then make sure the property is a read-only property or the property type doesn't exist in the container as a service type.

All you need to do to disable property injection for the GreeterHost is to prevent a default IGreeter service from ever being registered into the dependency map. You can do this by either constructing the dependency map by hand, or you can attach a service filter to the DependencyMapLoader so that it avoids loading a default IGreeter service into the resulting dependency map:

C#
var loader = new DependencyMapLoader();

// Make sure that no default service is loaded for the IGreeter interface
loader.ServiceFilter = service=>service.ServiceType 
	!= typeof(IGreeter) && string.IsNullOrEmpty(service.ServiceName);

As you can see from the example above, all you need to do to prevent the DependencyMapLoader from registering services is use a simple lambda function, and it can't get any simpler than that.

Next, here's the call that will construct the GreeterHost and perform the property injection operation if you still have the required dependency inside the container:

C#
// This is the only method call you need
// to do property injection
var greeterHost = container.GetInstance<GreeterHost>();

// ...

Fortunately for the end-user developer, the GetInstance method that performs the constructor injection is also the same method that will perform all property injection operations. Hiro uses these simple conventions to make developers' lives easier, and if it can help at least one person out there write better code, then I consider this project to be a success.

You can download Hiro from here.

You can also click here to download the Hiro example solution.

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

 
-- There are no messages in this forum --