Dependency Injection in .NET Core
.NET Core and ASP.NET have been around for about two years, and with it came a new modern and multi-platform framework built from the groud up.
One of the design principles of ASP.NET Core was embracing dependency injection. It not only provides the necessary hooks and infrastructure to inject your dependencies, it is also the way framework objects are composed and wired together.
ASP.NET Core ships with a simple built-in dependency injection container. Not only that, but the container itself is another dependency abstracted behind the IServiceProvider interface, easily allowing for authors of fully-featured containers to be compatible with the framework and for users to replace the built-in container.
Brief introduction to Dependency Injection
If you are reading this then you most probably have come across dependency injection already. but let's recap to establish the context. This might also help new joiners to follow along.
Dependency Injection is nothing more than a pattern where classes or modules declare their dependencies for others to fulfil at runtime.
In object-oriented languages this means classes won’t instantiate themselves, instead they will simply declare them for someone else to instantiate and provide (or inject). Constructor injection is the most common way of doing so, but is not the only one as you can also consider injecting using a property setter or as a method argument.
Let’s see some code using constructor injection:
class Greeter
{
private readonly IMessageWriter writer;
public Greeter (IMessageWriter writer)
{
this.writer = writer;
}
public void Greet()
{
writer.Write("Hello World!");
}
}
interface IMessageWriter
{
void Write(string message);
}
The Greeter class needs an instance of IMessageWriter but instead of creating an instance itself, it simply declares what it needs in its constructor and relies on someone else to provide that at runtime. So rather than doing this.writer = new MessageWriter() in its constructor, the dependency is injected as a constructor parameter.
See also how the dependency is declared using the IMessageWriter interface rather than a specific implementation class. This is important since now our Greeter class doesn’t depend on a specific writer. Any writer that implements such interface will work.
What we gain by following dependency injection is loosely coupled code. And loosely coupled code is key in achieving some desirable properties in your code:
- Extensibility - makes is possible to add new functionality by simply providing a different implementation of the interface and provides a natural hook for patterns like decorators, adapters, composites, facades, etc
- Testability - Code can be independently tested, and test mocks injected during tests.
- Maintainability - Having code which is both extensible and testable goes at great lengths towards enabling maintainability!
Dependency Injection is also a key tool that helps you adhere to the SOLID principles (Yes, dependency injection is not one of the principles, but a tool that allows you to follow them!) Since they are all related to loosely coupled code, dependency injection helps in some form with all of them, but it is key in two of them:
- Open-Closed principle. The interfaces and declaration of dependencies provide a natural hook to add new functionality to the system without modifying the existing code.
- Dependency Inversion. Your higher-level modules (the ones implementing your important core business logic) can declare and control the interfaces they need, for lower level modules to implement.
One thing we haven’t seen in this example is the concrete implementation of IMessageWriter or where the actual instances are created. That is because from the point of view of the Greeter class, it’s someone else’s problem! This might feel hard to let go at first, so let’s complete the example assuming we are writing a simple hello world console application:
static void Main(string[] args)
{
var greeter = new Greeter(new MessageWriter());
greeter.Greet();
Console.ReadKey();
}
class MessageWriter: IMessageWriter
{
public void Write(string message) => Console.WriteLine(message);
}
Consider carefully the first line of the Program’s Main method:
var greeter = new Greeter(new MessageWriter());
This is considered our composition root, the place where we cannot keep declaring dependencies anymore and instead needs to create concrete instances. In such a simple example with only two objects involved, we have been able to manually do this. In more complex applications, the composition root might need to create and compose complex object graphs which is why we use dependency injection containers.
While dependency injection doesn’t require the usage of containers, managing the dependencies manually on any non-trivial application would be a huge headache. A container basically acts as a catalogue of all our dependencies, allowing the composition root to simply ask for a resolved object.
Of course, containers provide many other desirable features when following dependency injection like lifetime management, convention-based registration or interceptors to name a few.
That's it for the introduction, if you want to read more on the subject, there are plenty of resources out there, of which I will highlight Mark Seamann’s great book Dependency Injection in .NET (second edition in the works)
The ASP.NET Core built-in container
From version 2.1 onwards of the ASP.NET Core framework it was designed with dependency injection being one of the most important considiretions.
- The framework comes with its own simple dependency injection container which provides basic functionality for registering and resolving dependencies
- All the framework dependencies like routing, loggers, configuration, etc use dependency injection and are registered themselves within the dependency injection container
Open an existing ASP.NET Core application or create a new one, then take another look at the ConfigureServices method of the Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
The interface IServiceCollection simply represents the catalogue of dependencies, which you will see referred to as services within the ASP.NET Core code and documentation:
public interface IServiceCollection:
IList<ServiceDescriptor>,
ICollection<ServiceDescriptor>,
IEnumerable<ServiceDescriptor>, IEnumerable
{
}
This catalogue is simply a map between target dependencies like IArticlesRepository and the classes that actually implement those dependencies like ArticlesRepository.
Several extension methods defined in the Microsoft.Extensions.DependencyInjection namespace provide all the methods necessary to register the dependencies (or services) within an IServiceCollection like AddTransient, AddSingleton, etc.
Let’s keep exploring how the framework itself register the dependencies. It has become idiomatic to create extension methods that register all the dependencies required for a given feature with a single call. That is exactly what is going on with the AddMvc() call.
Consider the following code:
// Create and configure an Autofac container
var builder = new ContainerBuilder();
// Call builder methods to register dependencies
builder.RegisterControllers(typeof(MvcApplication).Assembly);
…
// Set the dependency resolver to be Autofac.
var container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
Where each of these are extension methods themselves that add the dependencies required for each of those subsections of the MVC framework. If we dig more through the source code of AddMvcCore we will reach 100+ lines of registering services like:
services.TryAddSingleton<MvcMarkerService, MvcMarkerService>();
services.TryAddSingleton<ITypeActivatorCache, TypeActivatorCache>();
services.TryAddSingleton<IUrlHelperFactory, UrlHelperFactory>();
So as you can see, the framework really does register all its dependencies within the IServiceCollection. But that is just one part of the puzzle. In order to get a working application, dependencies also need to be resolved.
That is where the IServiceProvider comes into play, which in itself is a really simple interface that allows you to resolve a dependency:
public interface IServiceProvider
{
object GetService(Type serviceType);
}
Complimented by very useful extensions that provide a simpler API as in GetRequiredService():
public static class ServiceProviderServiceExtensions
{
public static IServiceScope CreateScope(this IServiceProvider provider);
public static object GetRequiredService(this IServiceProvider provider, Type serviceType);
public static T GetRequiredService<T>(this IServiceProvider provider);
public static T GetService<T>(this IServiceProvider provider);
public static IEnumerable<T> GetServices<T>(this IServiceProvider provider);
public static IEnumerable<object> GetServices(this IServiceProvider provider, Type serviceType);
}
However, you would very rarely need to directly use the IServiceProvider directly. Instead you just need to make sure to use dependency injection, declaring dependencies in the constructor of your class and that your dependencies are registered within the IServiceCollection. The framework will then be able to resolve and inject your dependencies at runtime.
The built-in container is nothing more than the implementation of IServiceProvider in a way that understands the catalogue of dependencies in a given IServiceCollection!
Registering your own dependencies
private readonly IArticlesRepository articlesRepository;
public ArticlesController(IArticlesRepository articlesRepository)
{
this.articlesRepository = articlesRepository;
}
By virtue of the routes and controller convention, the MVC framework will find your controller class. However, it won’t know how to resolve the IArticlesRepository dependency that is required by its constructor. This means we need to ensure this is added to the IServiceCollection catalogue of dependencies! The easiest way is to update the ConfigureServices of your Startup class and let the framework know about the dependency:
// Inject an ArticlesRepository whenever an IArticlesRepository is needed
services.AddTransient<IArticlesRepository, ArticlesRepository>();
That’s it, now the MVC framework can ask the IServiceProvider for an instance of your controller, along with an instance of the repository.
At this point you might be wondering what happens if ArticlesRepository itself has some dependency. Nothing changes, you just need to make sure the new dependency is registered within the IServiceCollection and the container will be able to recursively resolve the dependencies, building the object graph.
Conslusion
Dependency Injection is a critical pattern that every developer writing object-oriented code should learn. But how easy it is to put it into practice depends a lot on the framework you are using.
As we have seen through the article, ASP.NET Core has been designed with dependency injection in mind and even its own simple container out of the box. This means there are no obstacles to use dependency injection and create loosely coupled code from day one.
Not only that, dependency injection itself has been abstracted by the framework in a way that makes replacing the built-in container, seamless. Provided your container of choice has implemented the required interfaces, you can replace the built-in container in a few minutes.
If it wasn’t already, this should make dependency injection second nature for ASP.NET Core developers!