Click here to Skip to main content
15,892,293 members
Articles / Programming Languages / C#

Inversion of Control Containers – Best Practices

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
2 Sep 2011LGPL33 min read 20.5K   5  
A set of best practices helping you to get the most out of your container

Disclaimer: I’ve used IoC containers for a couple of years now and also made a couple of my own (just to learn, nothing released). I’ve also consumed loads of documentation in the form of blogs and StackOverflow questions. And I thought that I should share what I’ve learned. In other words: These are my very own subjective best practices.

An IoC Container is Not a Service Locator

One of the first mistakes that you’ll make is to register the container in the container or create a singleton to get access to it. It basically means that you can do something like:

C#
public class MyService
{
    MyService(IServiceResolver myContainer)
    {
    }

    public void DoSomething()
    {
        var service = _container.Resolve<ISomeService>();
        service.DoMore();
    }
}

Do not do that. Service location is another pattern that you get as a “bonus” when you use an IoC container. Some even call Service Location as an anti-pattern. Google it.

The most important reason is that you hide a dependency that you won’t discover until you forget to register IAnotherService. What if the method is only used during edge cases? Then the dependency won't be discovered until that edge case is fulfilled.

Using the constructor to inject dependencies will also help you discover violations to the Single Responsibility principle. Having a lot of dependencies might suggest that you need to break your class into several smaller classes.

There are some cases when you think that you really need to use the container in your class. Example:

C#
public class YourService
{
    public YourService(IContainer container)
    {
    }

    public void YourMethod()
    {
        var user = _container.Get<IUser>();
    }
}

The proper solution is to register a factory in the container:

C#
public class YourService
{
    public YourService(IDomainModelFactory factory)
    {
    }

    public void YourMethod()
    {
        var user = _factory.Create<IUser>();
    }
}

The Smaller Interface, the Better

Interfaces are not classes. There is nothing saying that there should be a 1-1 mapping between them. In my opinion, it’s far better to create several small interfaces and let the same class implement them than creating a larger one having all the features that the class has.

Do design the interfaces before you design the classes. The reason is that if you do the opposite, you’ll usually end up with one interface per class.

Smaller interfaces also make it easier to let your application grow since you can move smaller parts of the implementation (as in creating a new class for one of the interfaces).

Don’t Choose a Longer Lifetime to Get Better Performance

This is a mistake that I’ve done dozens of times. Object creation will usually not be a problem when you are running your application.

The advantage with short lifetimes is that it gets easier to handle context sensitive dependencies such as database connections.

Using a longer lifetime (which lives longer than the mentioned context sensitive dependencies) usually means that you create the same kind of service location implementation or custom factories.

Hence you get code like this:

C#
public class UserService : IUserService
{
    public void Add()
    {
        var (var uow = UnitOfWork.create())
        {
            // do stuff.
            uow.Save();
        }
    }
}

instead of:

C#
public class UserService : IUserService
{
    public UserService(IUnitOfWork uow)
    {
    }

    public void Add()
    {
        // do stuff with the unit of work here.
    }
}

What’s the problem with the first approach? Well. It’s usually the caller that knows what should be done in a unit of work, not the service itself. I’ve seen people create complex Unit Of Work solutions to get around that problem. The other simpler approach is to use TransactionScope.

Both of those solutions are either slower or more complex than services with shorter lifetimes. Save the optimizations to when object instantiation has been proven to be a problem.

Don’t Mix Lifetimes

Mixing lifetimes can lead to undesired effects if you are not careful. Especially when projects have begun to grow and getting more complex dependency graphs.

Example:

C#
// EF4 context, scoped lifetime
class Dbcontext : IDbContext
{
}

// short lifetime, takes db context as a dependency
class Service1 : IService1
{
}

// Single instance, keeps a cache etc.
class Service2 : IService2
{
}

Service2 takes Service1 as a dependency which in turn requires a dbcontext. Let’s say that the dbcontext has an idle timeout which closes the connection after a while. That would make Service1 fail and also Service2 which depends on it.

Simply be careful when using different lifetimes.

Try to Avoid Primitives as Constructor Parameters

Try to avoid using primitives as constructor parameters (for instance, a string). It’s better to take in object since it makes extension (subclassing) easier.

Hey, I need my primitives you say. Take this example:

C#
public class MyRepository
{
    public MyRepository(string connectionString)
    {
    }
}

Well. Your class breaks SRP. It acts as a connection factory and a repository. Break the factory part out:

C#
public class MyRepository
{
    public MyRepository(IConnectionFactory factory)
    {
    }
}

Avoid Named Dependencies

Named dependencies are as bad as magic strings. Don’t use them.

If you need different implementations, simply create different interfaces such as IAccountDataSource and IBillingDataSource instead of just taking in IDataSource.

In my opinion, this is a possible violation of Liskov's Substitution principle.

License

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


Written By
Founder 1TCompany AB
Sweden Sweden

Comments and Discussions

 
-- There are no messages in this forum --