ASP.NET Core 1.0 - Dependency Injection - What it is, and what it is not

Since its inception, ASP.NET 5 (now known as ASP.NET Core 1.0) has had the concept of Dependency Injection (DI) baked into its foundation. Where previous iterations of MVC supported this mechanism, it was always value-added. Also, at that stage, it wasn't DI, it was a Service Locator pattern, of which you could plug in a compatible IoC container to make it DI.

With the ability to redesign the entire stack from the ground up, the team took the approach of building in support for DI as a first-class feature. This was a natural extension of the desire to break up the stack into a set of composable NuGet packages - they needed a way to bring a whole host of components together - built around a set of known abstractions. These components needed to be testable too.

The built-in container represents a baseline set of functionality required to support ASP.NET Core 1.0. It only supports scenarios that are used by the framework - it is very focused on what it needs to do. With that in mind, you won't find it supporting some advanced concepts such as mutable containers, named child scopes, etc. found in other containers. That being said, ASP.NET Core is designed to allow you to plug in an alternative container that supports that additional functionality.

Foundations - IServiceProvider

The IServiceProvider interface has been kicking around since .NET 1.1 and is used by a variety of components throughout the Desktop CLR (.NET Framework) for providing a mechanism through which to resolve service instances. This includes the classic HttpContext (but don't try to resolve your IoC components through it - you can only return a few types, like HttpRequest, HttpResponse, etc. - It's not hooked up to your IoC container).

For the DI story in ASP.NET Core - the team have re-used this pre-existing interface, but it now is the service locator abstraction for the entire ASP.NET Core stack. In that sense, it sort of fills the role of the CommonServiceLocator for ASP.NET Core - to plug any other IoC/DI system into ASP.NET Core - you have to implement this standard interface. The stack uses this interface for resolving its types - and this means that although the framework has built-in support through their own container, you can easily plug in any other container - as long as they bring their implementation of IServiceProvider along for the ride. Sort of...

There is actually another contract that will need to be implemented to make an ASP.NET Core compatible container - IServiceScopeFactory. This interface is used for provisioning a new lifetime scope (in terms of built-in DI). For our built-in story, this is provided out of the box - and it is through this mechanism that Request-scoped services are resolved.

ServiceDescriptor and IServiceCollection

The ServiceDescriptor type (Microsoft.Extensions.DependencyInjection.Abstractions package) provides a container-agnostic approach to describing your services and their lifetimes. Generally, you take the approach of using a set of extension methods over IServiceCollection, such as:

services.AddTransient<IMyService, MyService>();  

These are wrappers around calls to ServiceDescriptor.[Instance|Transient|Scoped|Singleton] You can easily use ServiceDescriptor directly:

var descriptor = ServiceDescriptor.Transient<IMyService, MyService>();  

An IServiceCollection is a mutable collection of ServiceDescriptor instances.

Under the hood of the ServiceProvider

The built-in container is an internal implementation named ServiceProvider found in the Microsoft.Extensions.DependencyInjection package. An extension method of IServiceCollection is provided to initialise a new instance of it:

public static IServiceProvider BuildServiceProvider(this IServiceCollection services)  
    return new ServiceProvider(services);

When you initialise a service provider with an IServiceCollection instance, it creates a new root container. It contains an instance of ServiceTable which contains the blueprints for creating instances of services in their required lifetime. Through the use of IServiceScopeFactory it is also possible to initialise a new instance of ServiceProvider using the existing container as the root. The important thing about this, is they share the same ServiceTable instance - which means it is not designed to allow modifications to service registrations in child scopes. The idea is you configure your container once - reducing the set of moving parts.

A ServiceTable represents one or more IService instances, whereby an IService is a binding between a type, a ServiceLifetime and an IServiceCallsite, the latter of which actually realizes the instance of the service type. When a call to GetService is received, it follows the following steps to obtain the implementation instance:

  1. Look in the cache of realized-services to look for a delegate used to obtain the instance.
  2. If one does not exist - go through the ServiceTable and find the IServiceCallsite instance.
  3. Create a delegate used to obtain the instance through the IServiceCallsite
  4. Cache the delegate for future calls.

On the first call for a service instance, the container uses a reflection-based approach for obtaining the instance, but for any subsequent calls may result in the container opting to generate an expression tree, which compiles down to a delegate for future calls. This is to optimise occurrences where a component may be requested multiple times, depending on your chosen ServiceLifetime.

Optimizations for IEnumerable<T> services

The built-in container supports IEnumerable<T> directly, and it optimizes discovering T instances by chaining IService entries together through the IService.Next property. This means when the container is realizing an instance of IEnumerable<T>, it can move through the ServiceTable in a linked-list fashion to obtain the IService instances quickly.

Use in ASP.NET Core

The out-of-the-box experience provides the built-in set of DI components. The default convention is to simply use the Startup.ConfigureServices method to apply your service registrations:

public void ConfigureServices(IServiceCollection services)  
    services.AddTransient<IMyService, MyService>();

The framework will take care of calling services.BuildServiceProvider() after this call has completed.

Replacing the built-in container

This same mechanism for registering services can be used for returning a custom IServiceProvider:

public IServiceProvider ConfigureServices(IServiceCollection service)  
    services.AddTransient<IMyService, MyService>();

    return services.BuildServiceProvider();

That final call return services.BuildServiceProvider() could easily be replaced with other containers, such as Autofac and perhaps Ninject. After ASP.NET Core RTWs (but hopefully before!), I would expect to see most if not all of the popular IoC containers to implement a compatible IServiceProvider, allowing you to use the container of your choice, if the built-in container does not fit your requirements.

Use outside of ASP.NET Core

It is entirely possible to use the ASP.NET Core built-in DI container outside of ASP.NET Core - this is actually signified by the fact the abstractions and implementations aren't actually part of the Microsof.AspNetCore namespace. Like many of these utility services (such as FileProviders, etc.) they can be used independently.

Creating a container manually

You can easily create your own container using the same mechanism, the IServiceCollection. The Microsoft.Extensions.DependencyInjection.ServiceCollection implementation needs to be spun up and some registrations need to be added:

var services = new ServiceCollection();

services.AddTransient<IMyService, MyService>();  
services.AddScoped<IMyOtherService, MyOtherService>();  

You can then build your container through the BuildServiceProvider extension method:

var container = services.BuildServiceProvider();

var myService = container.GetService<IMyService>();  
var myOtherService = container.GetService<IMyOtherService>();  

To create a child scope, you can resolve the scope factory:

var scope = container.GetService<IServiceScopeFactory>();  
var scopedContainer = scope.ServiceProvider;

var myOtherServiceScoped = scopedContainer.GetService<IMyOtherService>();  

Finishing up

I hope this post gives you more of an in-depth look the built-in container - what it is, and how it works. Don't forget to check out the aspnet/DependencyInjection repo on GitHub.

comments powered by Disqus