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.
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.
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.
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.
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.
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.
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.
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.