How to implement dependency injection in ASP.NET Core

Published: Friday 26 February 2021

ASP.NET Core has support for the dependency injection (DI) design pattern and we will show how to implement it.

Dependency injection is when we inject an instance of an object into another object that it relies on.

C# coding challenges

C# coding challenges

Our online code editor allows you to compile the answer.

To take an example of video games, we could have a console service and a game service. The console service has a method to get the full properties of a particular console.

If we want to find out which consoles we can play a particular game on, one way of doing it is to inject the console service into the game service.

From there, we can then call a method in the console service to get the properties of the console. This could be returned as part of a method in the game service.

We will have a look at the different service lifetimes that are available in DI, and implement them in ASP.NET Core.

In addition, we will look at different ways in which we can inject dependencies in ASP.NET Core.

Afterwards, we will have a look at a common error that can happen when starting off using DI.

And finally, we will see how DI can be used in a background service.

Resources

To try out this application, download the code example from our code examples section.

As well as that, check our video tutorial where we test out ASP.NET Core's dependency injection.

The Service Lifetimes

In ASP.NET Core, dependency injection is supported by three different service lifetimes.

Singleton

An instance of a singleton lifetime service will last for the lifetime of the application.

So when the application starts, a singleton lifetime service will be initialised once, and the instance will be available until the application stops, or the instance is disposed of.

Scoped

A scoped lifetime service can be defined implicitly or explicitly.

For example, a scoped lifetime is implicitly initialised in ASP.NET Core when a HTTP request is made and is disposed of when a HTTP response has been sent.

A scoped lifetime can also be explicitly initialised. This is useful for things such as background services, where we would need to define what is a scoped lifetime service.

Scoped lifetime services will last for the lifetime of the scope.

Transient

An object using the transient lifetime service gets a new instance created every time it's injected.

So for example, in an MVC application, if we were to inject the same service in a controller, and a view, there would be a separate instance in the controller, compared to the view.

Using the transient lifetime services can be more memory exhaustive as the object needs initialising every time it's injected.

How the different dependency injection service lifetimes work in ASP.NET Core MVC app

How the different dependency injection service lifetimes work in ASP.NET Core MVC app

Using Dependency Injection in ASP.NET Core

Now that we got an overview of what dependency injection is, we are now going to put it into practice in ASP.NET Core.

For this application, we are going to create an ASP.NET Core Web MVC app.

What we are going to do is to set up three services as classes, with each class inheriting an interface.

Each service will represent one of the service lifetime's in dependency injection.

So, each service with be added to DI as a singleton, scoped or transient service lifetime.

// ISingletonService.cs
public interface ISingletonService
{
	string Time { get; set; }
}
// SingletonService.cs
public class SingletonService : ISingletonService
{
	public string Time { get; set; }

	public SingletonService()
	{
		Time = DateTime.UtcNow.ToString("HH:mm:ss.ffffff");
	}
}
// IScopedService.cs
public interface IScopedService
{
	string Time { get; set; }
}
// ScopedService.cs
public class ScopedService : IScopedService
{
	public string Time { get; set; }

	public ScopedService()
	{
		Time = DateTime.UtcNow.ToString("HH:mm:ss.ffffff");
	}
}
// ITransientService.cs
public interface ITransientService
{
	string Time { get; set; }
}
// TransientService.cs
public class TransientService : ITransientService
{
	public string Time { get; set; }

	public TransientService()
	{
		Time = DateTime.UtcNow.ToString("HH:mm:ss.ffffff");
	}
}

Next, we need to add these services to the IServiceCollection instance in the Startup class. This will register the service to be used in DI.

// Startup.cs
public class Startup
{
	...

	// This method gets called by the runtime. Use this method to add services to the container.
	public void ConfigureServices(IServiceCollection services)
	{
		...

		services.AddSingleton<ISingletonService, SingletonService>();
		services.AddScoped<IScopedService, ScopedService>();
		services.AddTransient<ITransientService, TransientService>();
	}

	...
}

Now, to test this, we are going to create a DI controller.

The way dependency injection works is that we can pass in the interface as a parameter in the constructor.

At this point, it will create an instance for each parameter. This will be dependant on whether the object has been initialised, or the object's service lifetime.

When passing the objects into the constructor's parameter, we will define local variables and pass the parameters instances to those variables.

This means we can use the instances across the whole controller.

// DIController.cs
[Route("di")]
public class DIController : Controller
{
	protected readonly ISingletonService _singletonService;
	protected readonly IScopedService _scopedService;
	protected readonly ITransientService _transientService;

	public DIController(ISingletonService singletonService, IScopedService scopedService, ITransientService transientService)
	{
		_singletonService = singletonService;
		_scopedService = scopedService;
		_transientService = transientService;
	}

	public IActionResult Index()
	{
		return null;
	}
}

What Happens When We Initialise Each Service?

When we initialise each service, we set a Time property. This is set to the current time in hours, minutes, seconds and microseconds.

What we are going to do is in the action, we are going to create a new model class, and store the value of the Time property for each of the services.

Afterwards, the action will return a view, and pass an instance of the model we just created.

We will run the test twice to see the differences between the two values.

// DIModel.cs
public class DIModel
{
	public string SingletonTime { get; set; }
	public string ScopedTime { get; set; }
	public string TransientTime { get; set; }
}
// DIController.cs
[Route("di")]
public class DIController : Controller
{
	protected readonly ISingletonService _singletonService;
	protected readonly IScopedService _scopedService;
	protected readonly ITransientService _transientService;

	public DIController(ISingletonService singletonService, IScopedService scopedService, ITransientService transientService)
	{
		_singletonService = singletonService;
		_scopedService = scopedService;
		_transientService = transientService;
	}

	public IActionResult Index()
	{
		var model = new DIModel();
		model.SingletonTime = _singletonService.Time;
		model.ScopedTime = _scopedService.Time;
		model.TransientTime = _transientService.Time;

		return View(model);
	}
}
@{
// Index.cshtml
}
@model DIModel
<h2>Singleton</h2>
<p>Controller Singleton: @Model.SingletonTime</p>
 
<h2>Scoped</h2>
<p>Controller Scoped: @Model.ScopedTime</p>
 
<h2>Transient</h2>
<p>Controller Transient: @Model.TransientTime</p>

Initialise Each Service - The Results

So we've loaded up the page, and refreshed it so we get two sets of results.

How the services get initialised in dependency injection when we load the page

How the services get initialised in dependency injection when we load the page

How the services get initialised in dependency injection when we refresh the page

How the services get initialised in dependency injection when we refresh the page

The singleton service has the same value in the Time property in each instance. This is expected as we have kept the application running for both times we loaded the page.

For the scoped and transient services, it has created new instances for each page load and this is represented by the value of the Time property being displayed.

Inject the Services in a View

Next, we are going to modify the view by injecting the services we created.

This will allow us to compare the value of the Time property for when it's injected in the controller and when it's injected in the view.

@{
// Index.cshtml
}
@using RoundTheCode.Di.Services
@model DIModel
@inject ISingletonService singletonService
@inject IScopedService scopedService
@inject ITransientService transientService
<h2>Singleton</h2>
<p>Controller Singleton: @Model.SingletonTime</p>
<p>View Singleton: @singletonService.Time</p>
 
<h2>Scoped</h2>
<p>Controller Scoped: @Model.ScopedTime</p>
<p>View Scoped: @scopedService.Time</p>
 
<h2>Transient</h2>
<p>Controller Transient: @Model.TransientTime</p>
<p>View Transient: @transientService.Time</p>

Inject the Services in a View - The Results

So we've loaded up the page and got two sets of results.

Using dependency injection in a controller and in a view

Using dependency injection in a controller and in a view

As we would expect, the singleton service has the same value regardless of whether it's injected in the controller and view.

And, it's also the same story with the scoped service. Because the controller and the view are run as part of the same HTTP request in ASP.NET Core, it means that it's using the same scope, hence why the values of the same.

The only difference is with the transient service. As a transient service is always initialised every time it's injected, even if it's part of the same HTTP request. Therefore, the value of the Time property is different in the controller and view.

Another Way of Implementing Dependency Injection

There is another way of injecting dependencies into a class.

Rather than injecting each service one by one, we can just inject the IServiceProvider instance.

We can use this IServiceProvider instance to call each service by calling it's GetRequiredService method, and passing in the type, either as a parameter, or as a generic method.

// DIController.cs
[Route("di")]
public class DIController : Controller
{
	...

	public DIController(IServiceProvider serviceProvider)
	{
		_singletonService = serviceProvider.GetRequiredService<ISingletonService>();
		_scopedService = serviceProvider.GetRequiredService<IScopedService>();
		_transientService = serviceProvider.GetRequiredService<ITransientService>();
	}

	...
}
@{
// Index.cshtml
}
@using RoundTheCode.Di.Services
@using Microsoft.Extensions.DependencyInjection;
@model DIModel
@inject IServiceProvider serviceProvider
@{
	var singletonService = serviceProvider.GetRequiredService<ISingletonService>();
	var scopedService = serviceProvider.GetRequiredService<IScopedService>();
	var transientService = serviceProvider.GetRequiredService<ITransientService>();
}
...

The main advantage of doing it this way is to avoid a large number of dependencies being injected.

Just say a controller had 13 different services injected. We would have to set up a parameter for each service. That would mean passing in 13 parameters into the constructor!

By injecting just the IServiceProvider instance, we can get the required services as and when we need them.

Injecting Services into Other Services

ASP.NET Core allows the ability to inject services into other services.

However, there are a couple of things to be very aware of.

The first is circular dependency. This is where one service is injected into another service, and vice-versa.

For example, with the ISingletonService instance, we could inject the ITransitionService instance.

But we could do it the other way. We could inject the ISingletonService instance into the ITransitionService instance.

This is known as circular dependency, and doing this in ASP.NET Core will give you an error like this:

A circular dependency was detected for the service of type {type}.

The other thing to be aware of is what service lifetimes can be injected.

Services that have been registered to use the transient service lifetime are allowed to be injected into a service that is using the singleton, or scoped lifetime service.

It's a similar story with a scoped service lifetime. This type of service can inject services that are declared as using the singleton or transient lifetime service.

However, it's not quite the same in an object using the singleton lifetime service.

It's fine to inject a service using the transient lifetime service. But, when we inject a service that is defined with the scoped service lifetime, we will get an error similar to this:

 Cannot consume scoped service '{type}' from singleton '{type}'

ASP.NET Core wouldn't know what to do with a scoped service lifetime as it wouldn't know which scope it belongs to.

If we wanted to used a scoped service inside a singleton, we would have to explicitly create a scope within it, and that's something we will look at next.

Define an Explicit Scoped Service

As we mentioned earlier, when a request is made in ASP.NET Core, it implicitly creates a scope.

But there are instances were we would want to explicitly create a scope.

An example of this is when we are running a background task.

For this example, we are going to create a new hosted service.

A hosted service is basically a background service, and there is more information on what hosted services are in our "Using Hosted Services in ASP.NET Core to Create a "Most Viewed" Background Service" article.

For our hosted service, we are going to explicitly create two scopes within it to show how we can go about doing this.

// DIHostedService.cs
public class DIHostedService : IHostedService
{
	protected readonly IServiceProvider _serviceProvider;

	public DIHostedService(IServiceProvider serviceProvider)
	{
		_serviceProvider = serviceProvider;
	}

	public Task StartAsync(CancellationToken cancellationToken)
	{
		using (var scope = _serviceProvider.CreateScope())
		{
			var singletonService = scope.ServiceProvider.GetRequiredService<ISingletonService>();
			var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
			var transientService = scope.ServiceProvider.GetRequiredService<ITransientService>();

			Debug.WriteLine(string.Format("Singleton time is {0}", singletonService.Time));
			Debug.WriteLine(string.Format("Scoped time is {0}", scopedService.Time));
			Debug.WriteLine(string.Format("Transient time is {0}", transientService.Time));
		}

		using (var scope = _serviceProvider.CreateScope())
		{
			var singletonService = scope.ServiceProvider.GetRequiredService<ISingletonService>();
			var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
			var transientService = scope.ServiceProvider.GetRequiredService<ITransientService>();

			Debug.WriteLine(string.Format("Singleton time is {0}", singletonService.Time));
			Debug.WriteLine(string.Format("Scoped time is {0}", scopedService.Time));
			Debug.WriteLine(string.Format("Transient time is {0}", transientService.Time));
		}

		return Task.CompletedTask;
	}

	public Task StopAsync(CancellationToken cancellationToken)
	{
		return Task.CompletedTask;
	}
}

In-turn, we also need to add the hosted service to the IServiceCollection interface.

// Startup.cs
public class Startup
{
	...

	// This method gets called by the runtime. Use this method to add services to the container.
	public void ConfigureServices(IServiceCollection services)
	{
		...
		services.AddHostedService<DIHostedService>();
	}

	...
}

So when we explicitly define a scope, any services that are defined under the scoped service lifetime will be initialised when the scope begins, and will be disposed when the scope ends.

A Common Error

When starting out with dependency injection, an error similar to the following may appear.

No service for type '{type}' has been registered.

The reason for this error is because the service has not been declared inside the IServiceCollection interface.

These services typically get added inside the ConfigureServices method from the Startup class.

Simply adding the service to the IServiceCollection instance in the Startup class will resolve the issue.

Use a Delegate to Create an Instance

For our final look at dependency injection, we are going to use a delegate when declaring a service to the IServiceCollection instance.

Typically, a service that is part of dependency injection will have other dependencies as part of its parameters in the constructor.

But, what if we want to explicitly define one of these parameters?

Well we can use the extension for either the AddSingletonAddScoped, or AddTransient methods inside the IServiceCollection instance. Within these methods, we can use the extension that has a delegate.

For this delegate, it requires the instance of the IServiceParameter interface.

Then we can use the IServiceParameter instance to define each service, and explicitly define the parameters that we need to.

// Startup.cs
public class Startup
{
	...

	// This method gets called by the runtime. Use this method to add services to the container.
	public void ConfigureServices(IServiceCollection services)
	{
		...

		services.AddSingleton<ISingletonService, SingletonService>((serviceProvider) =>
		{
			return new SingletonService(serviceProvider.GetRequiredService<ITransientService>());
		});
		...
	}

	...
}

Other Dependency Injection Packages

This is a summary of dependency injection in ASP.NET Core.

However, if you used dependency injection in .NET Framework projects, you will probably be familiar with Autofac.

Autofac is another package that can be used to implement dependency injection.

Whilst ASP.NET Core's dependency injection is quite simple, Autofac's DI can provide more detail, such as choosing between seven different lifetime scopes.

And, Autofac is also available for ASP.NET Core projects, as well as earlier versions of ASP.NET.

Personally though, I'm happy with the solution that comes with ASP.NET Core. It's very simple and relatively straight forward to implement.