Simple Injector is a .NET library that enables developers to apply dependency injection into their code. Moreover, Simple Injector supports different platforms including .NET 4.5 and up, .NET Standard, UWP, Mono, .NET Core, and Xamarin.

To download the source code for this article, you can visit our GitHub repository.

Let’s dig in.

What is Simple Injector?

Simple Injector is a flexible, fast, and easy-to-use dependency injection library for .NET developers. More about Simple Injector can be found on GitHub and their website.

Before we start setting up Simple Injector please refer to our previous articles: Dependency Injection in ASP.NET Core and SOLID Principles in C# – Dependency Inversion Principle to refresh your memory about dependency injection. 

How to Set Up a Simple Injector?

Let’s start by creating a console application using .NET CLI:

dotnet new console --name SimpleInjectorExample

Next, let’s add the SimpleInjector package to our application:

dotnet add package SimpleInjector

Now we can create services and configure Simple Injector. Let’s create ILogger interface:

public interface ILogger
{
    void Information(string message);
}

The ConsoleLogger class implements ILogger :

public class ConsoleLogger : ILogger
{
    public void Information(string message)
    {
        Console.WriteLine($"[ Information: {DateTime.Now} ] {message}");
    }
}

Our services are ready next step is to wire up the Simple Injector container. For this purpose let’s create ContainerManager class:

public class ContainerManager
{
    private static readonly Lazy<Container> _container = new Lazy<Container>(ConfigureServices);
    
    private static Container ConfigureServices()
    {
        var container = new Container();
        container.Register<ILogger, ConsoleLogger>();
        container.Verify();
        return container;
    }

    public static Container Instance { get => _container.Value; }
}

The ConfigureServices() method handles creating and configuring the Simple Injector container. Once the container is configured the next step is to verify it by calling the Verify() method. Verifying the container is an optional step. However, it is good practice to use it as it helps detect container configuration issues at an early stage. At this point, we can use the ContainerManager class to access instances.

Let’s look at the usage example:

static void RunLogger()
{
    var logger = ContainerManager.Instance.GetInstance<ILogger>();
    logger.Information("Hello SimpleInjector");
}

Which Type Mappings are Available in Simple Injector?

In the previous example, we used Register() method to define a one-to-one mapping between the interface ILogger and the implementation class ConsoleLogger. In addition to one-to-one mapping Simple Injector also supports one-to-many object mapping. One-to-many object mapping is a way of defining a set of implementations for a given abstraction.

To better understand let’s look at a code example:

public interface INotification
{
    void Notify(string notification);    
}

The INotification interface is an abstract definition. next, we will create multiple classes that implement INotification interface.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Let’s start by defining EmailNotification class:

public class EmailNotification : INotification
{
    public void Notify(string notification)
    {
        Console.WriteLine($"[ Email Notification : {DateTime.Now} ] {notification}");
    }
}

Similarly, let’s create another class called SMSNotification:

public class SMSNotification : INotification
{
    public void Notify(string notification)
    {
        Console.WriteLine($"[ SMS Notification : {DateTime.Now} ] {notification}");
    }
}

Both SMSNotification and EmailNotification implement the INotification interface. This means we can configure our container to comprise the one-to-many object mapping:

private static Container ConfigureServices()
{
    ...
    container.Collection.Register<INotification>(typeof(EmailNotification), typeof(SMSNotification));
    container.Verify();
    ...
}

To update our container, we had to add one line wiring up before calling the  Verify method. At this point, everything is set and we can request instances of INotification from the container:

static void RunNotificationServices(string notification)
{
    var notificationServices = ContainerManager.Instance.GetAllInstances<INotification>();
    foreach(var service in notificationServices)
    {
        service.Notify(notification);
    }    
}

Inside RunNotificationServices method, When GetAllInstances is invoked it returns IEnumerable<INotification>. And, provides us with all notification service instances. Then we invoked the Notify method by iterating over each instance.

Type Registration Methods in Simple Injector

Simple Injector provides different ways of registering a type. In this section we will look at three commonly used ways of registering types:

  • Manually created instance
  • Instance created using delegate
  • Auto-Wiring

Manually Created Instance

To configure our container with a manually created instance all we have to do is pass an instance when calling RegisterInstance() method:

private static Container ConfigureServices()
{
     ...
     container.RegisterInstance<IUserRepository>(new UserRepository());
     container.Verify();
     ...
}

In the code example above we have configured our container to return a single instance of UserRepository whenever an instance of IUserRepository is requested. It is not good practice to configure containers this way.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Instance Created Using Delegate

In this section, we will update our IUserRepository configuration to use a delegate instead of a manually created instance:

private static Container ConfigureServices()
{
    ...
    container.Register<IUserRepository>(() => new UserRepository());
    container.Verify();
    ...
}

The Register method accepts delegate as an argument which helps us configure our container using a delegate. It is good practice to use this method of wiring containers when working with third-party application components and frameworks.

Auto-Wiring

Simple Injector comprises Auto-Wiring feature which provides the capability to automatically create a type based on type information (constructor arguments). The prerequisites for Simple Injector to provide auto-wiring are:

  • The type must be a concrete implementation
  • There should only be one public constructor(which can be the default constructor). And, all arguments must be resolvable by the constructor
  • Constructor arguments should not be optional

Let’s add auto-wiring to the example code:

public class UserService : IUserService
{
    private readonly IAddressRepository _addressRepository;
    private readonly IUserRepository _userRepository;

    public UserService(IAddressRepository addressRepository, IUserRepository userRepository)
    {
        _addressRepository = addressRepository;
        _userRepository = userRepository;
    }

    public UserDetail GetUserDetail(int userId)
    {
        var user = _userRepository.GetUser(userId);
        var address = _addressRepository.GetUserAddress(userId);
        
        return new(user, address);
    }
}

We have created UserService class that has one public constructor that has two parameters. For Simple Injector to infer the two parameters let’s update our DI container:

private static Container ConfigureServices()
{
    ...
    container.Register<IUserRepository>(() => new UserRepository());
    container.Register<IAddressRepository, AddressRepository>();
    container.Register<IUserService, UserService>();
    container.Verify();
    ... 
}

Now, Simple Injector knows how to resolve both IUserRepository and IAddressRepository instances. Moreover, it can create instances  UserService whenever an instance of IUserService is requested.

Object Lifetime in Simple Injector

Object lifetime determines the number of instances a configured service will have and the lifetime of those instances. By default, Simple Injector provides three dependency injection lifestyles: Transient, Scoped, and Singleton.

If you’re not familiar with dependency injection lifestyles check out our article Dependency Injection Lifetimes in ASP.NET Core to learn more.

In addition to these three lifestyles Simple Injector provides a way to define a custom lifestyle. Defining a custom lifestyle is outside the scope of this article.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Transient

In Simple Injector the default lifestyle is Transient. For transient types, the container creates a corresponding instance every time we request a service. The container does not keep track of transient instances.

Therefore, they are not disposed of by the container. In the previous code examples, the instance lifetime is not explicitly stated, therefor they are configured as Transient:

//Lifetime explicitely set to Transient 
container.Register<IUserService, UserService>(Lifestyle.Transient);

//by default lifetime is set to Transient
container.Register<IAddressRepository, AddressRepository>();

Scoped

The container creates a single instance for every request within the defined scope. And, tracks the instances. After the completion of the scope, the container disposes of the instances.

To change the lifetime of IAddressRepository we change the Lifestyle value and define the scope of lifestyle:

private static Container ConfigureServices()
{
    var container = new Container();
    container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
    ... 
    container.Register<IAddressRepository, AddressRepository>(Lifestyle.Scoped);
    ...
}

Web Request and WCF Operations implement scoping implicitly. However, for this example, we are using a console application. Hence, we must specify the lifestyle of the scope and when we want to request for scope instance we should start and finish the scope.

static void PingUser(int userId)
{
    using var scope = AsyncScopedLifestyle.BeginScope(ContainerManager.Instance);
    var service = ContainerManager.Instance.GetInstance<IUserService>();
    var logger = ContainerManager.Instance.GetInstance<ILogger>();
    
    var detail = service.GetUserDetail(userId);
    var userInfo = $"{detail.User.FirstName} {detail.User.LastName}";
    var address = $" {detail.Address.City} - {detail.Address.Zone}";
    
    logger.Information($"PINGING USER Name: {userInfo}, Address: {address}");
    RunNotificationServices($"Hello {detail.User.FirstName} {detail.User.LastName}");
}

The core implementation of IUserService is dependent on IAddressRepository (has is scoped lifestyle). Since this is a console application we must start the scope manually by invoking BeginScope() method. The scope instance keeps track of the scoped components and prevents the creation of another instance within the scope. At the end of using block the scope and all other disposable instances are automatically disposed of.

Singleton

There will only be one instance of the registered service type. The container tracks the single instance. And, The instance is disposed of after the container is disposed of.

We can change the lifetime simply by changing the Lifestyle value:

container.Register<ILogger, ConsoleLogger>(Lifestyle.Singleton);

It is possible to mix dependencies between components that have different lifetimes. One rule to keep in mind is that a component can only be dependent on components that have equal or longer lifetimes. For example, a service with a Transient lifestyle can depend on services that have scoped or singleton lifestyle but not vice versa. In Simple Injector calling the Verify method after configuring our container helps us detect this type of error early on.

Why Should We Use Simple Injector?

Simple Injector is an easy-to-use open-source dependency injection library with a great community behind it. Simple Injector is highly performant and designed with concurrency in mind which allows it to scale with the number of processors and threads available. Applying dependency injection using Simple Injector is easy as it provides a powerful diagnostic system that helps identify configuration errors at an early stage. Moreover, Simple Injector provides arguably the most advanced support for .NET generics.

There’s an article that shows how different IoC containers compare to each other, so you might want to check it out: IoC Container Benchmark – Performance comparison

Conclusion

In this article, we’ve learned what a Simple Injector is, the different aspects of configuring a Simple Injector container, and the different instance lifetimes supported in Simple Injector.