ASP.NET Core Dependency Injection: What is the IServiceCollection?

If you’ve built applications using ASP.NET Core then you’ve most likely used the built-in dependency injection container from Microsoft.Extensions.DependencyInjection. This package provides an implementation of the corresponding abstractions found in Microsoft.Extensions.DependencyInjection.Abstractions.

In this post, I wanted to take a deeper look at the first concept from the Microsoft Dependency Injection (DI) container, the IServiceCollection hopefully demystifying it a little.

 

Dependency Injection in ASP.NET Core Pluralsight Course Image
If you find the information in this post useful and want to learn more about dependency injection, you may enjoy my course on Pluralsight – “Dependency Injection in ASP.NET Core

What is Dependency Injection and a DI Container?

Before moving on, I want to clarify as briefly as possible what we mean by dependency injection and why it can be useful. If you’re new to these concepts, this should provide just enough information to consume the rest of this post.

The Microsoft dependency injection container is simply a set of classes, combined into a library that creates objects required by your application code. Take the following code…

public class ClassA
{
	public void DoWork() 
	{
		var b = new ClassB();
		b.DoStuff();
	}
}

public class ClassB
{
	public void DoStuff()
	{
		// Imagine implementation
	}
}

ClassA depends directly on ClassB and within it’s DoWork method, it creates a new instance before calling its DoStuff method. We can apply a couple of common principles to improve this code…

public class ClassA
{
	private readonly ClassB _dependency;

	public ClassA(ClassB classB) => _dependency = classB;

	public void DoWork() => _dependency.DoStuff();
}

public class ClassB : IThing
{
	public void DoStuff()
	{
		// Imagine implementation
	}
}

Firstly, we no longer use the new keyword to instantiate an instance of ClassB. Instead, we have specified the type it depends on within the constructor. An instance of ClassB must be provided when that constructor is called. This applies a form of Inversion of Control (IoC) since ClassA is no longer in control of creating an instance of ClassB.

We can apply a further principle to this code…

public interface IThing
{
	public void DoStuff();
}

public class ClassA
{
	private readonly IThing _dependency;

	public ClassA(IThing thing) => _dependency = thing;

	public void DoWork() => _dependency.DoStuff();
}

public class ClassB : IThing
{
	public void DoStuff()
	{
		// Imagine implementation
	}
}

We have now applied the dependency inversion principle from SOLID. We no longer depend on a concrete implementation. Instead, we depend upon the IThing abstraction. We allow the caller of this constructor to pass in any valid implementation of this interface.

In application code, we can satisfy this manually with the following code.

class Program
{
	static void Main(string[] args)
	{
		IThing thing = new ClassB();
		ClassA classA = new ClassA(thing);
		classA.DoWork();
	}
}

We must first create an instance of ClassB, which now implements the IThing interface. We can create an instance of ClassA, passing in ClassB as the first and only argument. For these two classes, it’s pretty straightforward to handle this dependency injection manually. As applications grow, we are likely to have tens, even hundreds or thousands of types, many of which may have deeper dependency graphs.

This is where dependency injection (DI) containers come to the rescue. We configure the container so that it knows which types to use and it then is responsible for automating the creation of objects, often referred to as services.

Registering Services

The first place you will usually interact with the Microsoft dependency injection container is within the Startup class of ASP.NET Core applications. Here you use the ConfigureServices method to register services with the container. The ConfigureServices method is called early in the application hosting lifetime. It has one parameter, an IServiceCollection, which is provided when the hosting library initialises the ASP.NET Core application.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
		// Register services with DI
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
	}
}

To simplify things as much as possible, I’m going to take ASP.NET Core out of the mix for my sample code. Yes, you can indeed use the DI library independently of ASP.NET Core! We’ll use a basic console application to experiment with the IServiceCollection.

In future posts in this series, we’ll continue to use this app to work with the IServiceProvider. While we can and will add these features to a raw console application, a better practice for production apps is to use a worker service. Worker services include not only the DI container but also the application hosting, configuration and options too. Since we’re not pushing this code to production, we’ll sidestep that recommendation.

After creating a new Console application, our first job is to add a package reference to include the Microsoft.Extensions.DependencyInjection NuGet package

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
  </ItemGroup>

</Project>

At the time of writing, 5.0.1 is the latest released version, so that’s the one I’ll use.

We’re now nearly ready to begin registering our dependencies. To do that, we’ll need an IServiceCollection to work with. Let’s take a look at the IServiceCollection definition.

public interface IServiceCollection : IList<ServiceDescriptor>
{
}

IServiceCollection does not define any of its own members but derives from IList<ServiceDescriptor>.

The Microsoft.Extensions.DepenencyInjection package includes the default implementation, which is the ServiceCollection class.

public class ServiceCollection : IServiceCollection
{
	private readonly List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>();
	
	public int Count => _descriptors.Count;
	
	public bool IsReadOnly => false;
	
	public ServiceDescriptor this[int index]
	{
		get
		{
			return _descriptors[index];
		}
		set
		{
			_descriptors[index] = value;
		}
	}
	
	// MORE CODE OMITTED FOR BREVITY
}

The ServiceCollection implementation uses a private field to store a List<ServiceDescriptor> and pretty much just wraps this, exposing the methods required to implement IList<T>. We can see from this exploration that at its core, we would expect ServiceDescriptors (we’ll come to these below) to be added to the list, as we register services. We’ll learn how this takes place behind the scenes as we progress through this post.

Let’s start by creating an instance of ServiceCollection and registering two services for our application.

static void Main(string[] args)
{
	var serviceCollection = new ServiceCollection();

	serviceCollection.AddSingleton<ClassA>();
	serviceCollection.AddSingleton<IThing, ClassB>();

	Console.WriteLine("Done");
}

In the preceding code, we’ve registered two services using the AddSingleton method. This is not a method defined by the IServiceCollection interface, nor is it present on the ServiceCollection type. It’s an extension method for IServiceCollection which lives in a class called ServiceCollectionServiceExtensions. We’ll learn how this method results in a registered service very soon. Before we move on, it’s worth recapping the concept of service lifetimes.

Controlling Service Lifetimes

The Microsoft dependency injection framework allows us to register services using one of three lifetimes. The lifetime controls how often the dependency injection container will create new instances of the service. In the sample code, we’ve used the Singleton lifetime for both of our services.

Singleton services are created once per dependency injection container. We’ll have a single DI container (ServiceProvider) for our application once we complete the code. This means that only one concrete instance of each service is expected. The instances will be created on-demand and will live for as long as the DI container lives. In our application, that will be until the process exits. 

An advantage of using Singleton services is that we avoid allocating numerous instances of the same thing. This is both more efficient and slightly more performant. A key consideration though is whether a single instance can be safely shared across threads. For this sample, we’ll have a single thread running the code, so this will be fine. In ASP.NET Core applications, singleton types may be in use across multiple threads simultaneously to accommodate handling the requests concurrently. What this ultimately means is that it cannot store non-thread-safe state in singleton services. Our sample classes do nothing and are stateless, so we’re all good here.

The second lifetime option is transient. This is on the opposite end of the spectrum from the singleton lifetime. Using the transient lifetime results in a new instance of the service being returned from the DI container each time it is resolved. This means that instances of this type are never shared by classes which depend on them, and therefore don’t need to consider thread-safety. Of course, you still need to avoid sharing the resolved instance across threads in your own code. From the perspective of ASP.NET Core, each request may resolve zero, one or many instances of the service to satisfy all dependents.

The final lifetime is a little special. The scoped lifetime exists in a kind of middle ground between singleton and transient. The type is always freshly created per scope, but within a scope, the same instance is reused. This leads to the question, what is a scope?

What is a Scope?

A scope is used to define a logical boundary for resolved services. You can manually create a scope; whenever you see fit. When used with ASP.NET Core, a scope is created for you for each new request that is received. This allows the sharing of an instance of a service within the lifetime of a request. The best example of this is probably the DbContext when using Entity Framework Core. The context is registered as scoped and so that same context is reused for a request. This is generally safe and allows context state changes to be shared within any services involved in request handling.

Outside of ASP.NET Core, the use of scopes is less common. One possible use case is to separate concurrent workloads so that each uses their own scope. For example, we may have a queue processing worker service where we parallelise processing of messages. In this case, each concurrent message processing Task may use a scope to isolate its services from the other Tasks. This could be particularly useful of the messages are written to a database via Entity Framework as we can perhaps reuse the DbContext per scope, which avoids the concurrent Tasks modifying the same context instance.

In our simple example, ClassA depends on and an IThing and holds an instance in its private field. This is an excellent example of where lifetimes need to be considered concerning dependencies. If ClassB is registered with a short, transient lifetime, we should generally not register ClassA with a longer lifetime. If we do, we must accept that the transient instance provided to ClassA by the DI container, will then be captured by it, and live for the lifetime of that ClassA instance.

What Happens when we Register Services?

In our sample, we’ve registered two services.

serviceCollection.AddSingleton<ClassA>();
serviceCollection.AddSingleton<IThing, ClassB>();

The first line adds a service registration with the singleton lifetime. Here we have registered ClassA as both the service type (upon which other types can depend) and the implementation type. The second registration is more common and registers our ClassB Type to implement the IThing service type. Both are singletons in this application.

In the first registration, AddSingleton<TService> is an extension method on IServiceCollection where the generic argument must be a class. This method forwards onto AddSingleton(Type serviceType). This method then, calls down into AddSingleton(Type serviceType, Type implementationType) passing the same Type for both arguments.

In the second registration, AddSingleton<TService, Timplementation> also adds a constraint requiring that TService be a class (or interface) and that TImplementation is a class, which inherits from that interface. This method ultimately calls down into the same AddSingleton(Type serviceType, Type implementationType) passing in the corresponding types.

Here is the final AddSingleton method:

public static IServiceCollection AddSingleton(
	this IServiceCollection services,
	Type serviceType,
	Type implementationType)
{
	if (services == null)
	{
		throw new ArgumentNullException(nameof(services));
	}
	if (serviceType == null)
	{
		throw new ArgumentNullException(nameof(serviceType));
	}
	if (implementationType == null)
	{
		throw new ArgumentNullException(nameof(implementationType));
	}
	return Add(services, serviceType, implementationType, ServiceLifetime.Singleton);
}

We can see that AddSingleton calls into the private Add method, passing the Singleton enum value as the final argument.

Let’s look at how the Add method works:

private static IServiceCollection Add(
	IServiceCollection collection,
	Type serviceType,
	Type implementationType,
	ServiceLifetime lifetime)
{
	var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);
	collection.Add(descriptor);
	return collection;
}

It creates a new ServiceDescriptor instance, passing in the service type, an implementation type (which may be the same as the service type) and the lifetime. It then calls the Add method on the IServiceCollection which adds the descriptor to its list.

Earlier, we learned that IServiceCollection essentially wraps a List<ServiceDescriptor>. The ServiceDescriptor class is pretty basic and represents a registered service including its service type, implementation, and lifetime. It exposes these as read-only properties.

There are two properties which all service descriptors will have set by the constructor are the ServiceType and the Lifetime (ServiceLifetime). They will also have one (and only one) of the following properties:

  • An ImplementationType (Type?) representing the type which will be resolved whenever an instance of the serviceType is required.
  • An ImplementationInstance (object?) which is an actual instance of the serviceType which already exists.
  • An ImplementationFactory (Func<IServiceProvider, object>?) which is simply a Func that given an IServiceProvider can return an object which can be implicitly converted to the serviceType.

At this stage, we are merely building up the collection of service descriptors which describe each service that may need to be resolved within our application code. Later we will use this list to build a service provider capable of providing resolved instances. We’ll cover that in a future blog post.

Other Registration Techniques

For completeness, I want to highlight that there are other ways to register services that align with the properties we may optionally set on the ServiceDescriptor. I’ve covered these in more detail in some of my prior blog posts but let’s review them here too.

Implementation Factory

For certain edge cases, the creation of services may require some additional behaviour. Perhaps the type we need is best created via a builder class, and we wish to allow the DI container to use the same approach. In this case, we can use an overload of AddSingleton, which accepts a Func<IServiceProvider, object>. This function is called at the time services are being resolved from the IServiceProvider. The function has access to that IServiceProvider to resolve any services it needs manually. It must then eventually return an object of the type specified by the generic TService argument.

serviceCollection.AddSingleton(sp =>
{
	// Not production code
	var thing = sp.GetRequiredService<IThing>();
	return new ClassA(thing);
});

This preceding code is not something you’d do in production as it’s best to let the container create the instance via its constructor, resolving the dependencies from its other services. However, it does illustrate that we can take control when we need to.

A second use case for this technique is when you wish to conditionally decide on an instance to return based on application configuration. You could access the IConfiguration from the container, read a value, and then decide about the instance you return.

You can read more about this approach in ASP.NET Core Dependency Injection – Registering Implementations Using Delegates

Instance Registration

An additional choice for singleton registrations is to provide an instance directly in the registration. A reference to this is then stored in the service descriptor.

var myInstance = new ClassB();
serviceCollection.AddSingleton<IThing>(myInstance);

In the preceding code, we already have an instance of ClassB, and we have chosen to register that instance into the container. This single instance will now be used whenever a service depends on the IThing interface.

I’ve rarely used this registration method, but it might be useful to have an expensive type instantiated early in your application startup code and continue using that instance, rather than allowing the container to create its own.

Working with Service Descriptors Directly

Finally, it’s interesting to know that we can also manually define a ServiceDescriptor which we add directly to the IServiceCollection.

var descriptor = new ServiceDescriptor(typeof(IThing), typeof(ClassB), ServiceLifetime.Singleton);
serviceCollection.Add(descriptor);

In this code, we circumvent the helper methods by defining the services descriptor and calling the Add method.

Summary

In this post, we’ve recapped some of the core pieces of knowledge required to understand DI in .NET. We’ve seen that it’s possible to begin using the Microsoft DI container outside of ASP.NET Core by creating a ServiceCollection in our code. We’ll use that in a future post to build a service provider and resolve some services. We’ve learned how the standard AddXYZ extension methods on the IServiceCollection work and that they ultimately create a ServiceDescriptor that is added to the list which the ServiceCollection wraps. 

Finally, we reviewed some other ways to register services and manually used a ServiceDescriptor in our application code. Join me in the next post in this series which looks at how the IServiceCollection is used to build an IServiceProvider. 

The code for this example can be found up on GitHub.

In the meantime, if you have Pluralsight access, you can find my complete DI course over on Pluralsight.

Related Content:


Have you enjoyed this post and found it useful? If so, please consider supporting me:

Buy me a coffeeBuy me a coffee Donate with PayPal

Steve Gordon

Steve Gordon is a Pluralsight author, 6x Microsoft MVP, and a .NET engineer at Elastic where he maintains the .NET APM agent and related libraries. Steve is passionate about community and all things .NET related, having worked with ASP.NET for over 21 years. Steve enjoys sharing his knowledge through his blog, in videos and by presenting talks at user groups and conferences. Steve is excited to participate in the active .NET community and founded .NET South East, a .NET Meetup group based in Brighton. He enjoys contributing to and maintaining OSS projects. You can find Steve on most social media platforms as @stevejgordon