Updated on

Dependency injection in ASP.NET Core means a class asks for the services it needs, usually in its constructor, and the framework’s built-in container creates them and passes them in.

In this article, we’re going to talk about dependency injection, one of the most frequently used design patterns out there. There are many tutorials about dependency injection, but in this one, we’re going to focus on understanding why the dependency injection is used and how to use it properly in ASP.NET Core.

To download the source code for this article, you can visit our GitHub repository.

So, let’s start.

What Is Dependency Injection?

Dependency Injection (often called just DI) is a software design pattern that helps us create loosely coupled applications. It is an implementation of the Inversion of Control (IoC) principle, and the Dependency Inversion Principle (D in SOLID).

The Inversion of Control principle is a design pattern in which the control flow of the application is reversed. We can use it to our benefit to decouple the parts of our application. This might sound a bit odd, but we’ll see how it works in a moment.

Dependency Injection is just one way of implementing the Inversion of Control principle. Inversion of Control can be achieved via other means like delegates, events, or other design patterns like template method for example.

The Inversion of Control also states that objects should not create other objects they depend on. These should be injected via external service or container. ASP.NET Core has one built in.

You can read more about the Dependency Inversion Principle in our SOLID principles series.

Normal Flow vs Inverted Flow

In a more graphical way, here’s how the typical workflow looks like:

Compile time and run time: Class A references Class B, which references Class C, and control flows the same way

In a lot of applications, the compile-time dependencies flow in the same direction as the runtime execution. This is wrong because dependencies should be oriented towards the abstraction and not implementation. Class A is referencing a method from Class B, and Class B is referencing the method from Class C. This creates a direct dependency graph that should be avoided at all costs.

This can be corrected by using interfaces, or rather coding to interfaces:

At compile time, Class A references Interface B, Class B implements Interface B and references Interface C, and Class C implements Interface C. At run time, control still flows from Class A to Class B to Class C.

This time around, Class A calls a method from an abstraction, Interface B, that Class B implements. Similarly, Class B calls a method from an Interface C instead of Class C directly.

Thus, Class B is no longer a dependency of Class A, and Class A and Class B remain loosely coupled. Class B depends on the interface controlled by Class A and therefore the compile-time dependencies are inverted. At the same time, runtime flow remains the same.

Inversion of Control with the Constructor Injection

Constructor injection is by far the most used type of Dependency Injection. Dependency injection is built in ASP.NET Core, and we’re encouraged by the framework to use it. Actually, we might even find it hard not since it’s deeply integrated into the framework.

But let’s put all this theory to practice. We are going to create an example of a control flow without a DI first to see how it looks like.

Our starting point is a new ASP.NET Core MVC project:

dotnet new mvc -n DependencyInjectionDemo
cd DependencyInjectionDemo

We’ll create a simple PlayerGenerator for a potential game. Let’s create a simple model first. We can create a Player class in the Models folder and some enums to determine the gender and the hair color of that player:

namespace DependencyInjectionDemo.Models;

public class Player
{
    public required string Name { get; set; }
    public Gender Gender { get; set; }
    public int Age { get; set; }
    public HairColor HairColor { get; set; }
    public int Strength { get; set; }
    public required string Race { get; set; }
}

public enum Gender
{
    Male = 0,
    Female = 1
}

public enum HairColor
{
    Blue = 0,
    Black = 1,
    Brown = 2,
    Green = 3,
    Pink = 4,
    White = 5,
    Orange = 6
}

We mark Name and Race as required, so every new player gets them, and the compiler doesn’t warn us about nulls.

Now, we need to create a PlayerGenerator service in the Services folder:

using DependencyInjectionDemo.Models;

namespace DependencyInjectionDemo.Services;

public class PlayerGenerator
{
    private readonly string[] _maleNames = ["Jon Irenicus", "Kagain", "Minsc", "Xzar", "Drizzt Do'Urden", "Sarevok", "Elminster"];
    private readonly string[] _femaleNames = ["Imoen", "Jaheira", "Dynaheir", "Branwen", "Bodhi", "Shar-Teel", "Safana"];

    public Player CreateNewPlayer()
    {
        string playerName;
        var random = Random.Shared;

        var playerNameIndex = random.Next(7);
        var playerGenderIndex = random.Next(2);
        var playerHairColorIndex = random.Next(7);
        var playerAge = random.Next(18, 100);
        var strength = random.Next(8, 18);

        if (playerGenderIndex == 0)
            playerName = _maleNames[playerNameIndex];
        else
            playerName = _femaleNames[playerNameIndex];

        return new Player
        {
            Name = playerName,
            Gender = (Gender)playerGenderIndex,
            HairColor = (HairColor)playerHairColorIndex,
            Age = playerAge,
            Strength = strength,
            Race = "Human"
        };
    }
}

This piece of code creates a random male or female human player character and uses some randomly generated values to determine character details. It’s a really simple generator that has only one method, and its only purpose is to create new players.

Now we can add a GameController to the Controllers folder, with an Index() method. Mind you, this is the version we’re going to fix:

using DependencyInjectionDemo.Services;
using Microsoft.AspNetCore.Mvc;

namespace DependencyInjectionDemo.Controllers;

public class GameController : Controller
{
    public IActionResult Index()
    {
        var playerGenerator = new PlayerGenerator();
        var newPlayer = playerGenerator.CreateNewPlayer();

        return View(newPlayer);
    }
}

We’re simply creating a new instance of the PlayerGenerator and calling the CreateNewPlayer() method to create a new player and send it to the view.

The only thing remaining is to create the Index view in the Views/Game folder to show our new player details:

@model Player
@{
    ViewData["Title"] = "Game Page";
}

<div>
    <h2>New Player Info</h2>
    <p>Name: @Model.Name</p>
    <p>Gender: @Model.Gender</p>
    <p>Race: @Model.Race</p>
    <p>Age: @Model.Age</p>
    <p>Hair Color: @Model.HairColor</p>
    <p>Strength: @Model.Strength</p>
</div>

Now if we run the application with dotnet run and navigate to /Game, we should get a randomly created player character:

The Game page of the DependencyInjectionDemo app shows New Player Info: Name Drizzt Do'Urden, Gender Male, Race Human, and a random age, hair color and strength.

Excellent. Drizzt Do’Urden is ready to play his turn.

The Problem(s)

Since our controller is directly creating a PlayerGenerator instance, we’ve created several huge problems.

Our controller is no longer just the controller, so we’ve violated the Single Responsibility Principle. Controllers should not create instances of the classes they depend on.

We’ve also produced a tightly coupled source code. Say for example we want to change the PlayerGenerator class with another one, like BetterPlayerGenerator class which creates players in a completely different way. We would need to change our controller too:

public IActionResult Index()
{
    var playerGenerator = new BetterPlayerGenerator();
    var newPlayer = playerGenerator.CreateNewPlayer();

    return View(newPlayer);
}

This is not ideal, and you see how this can propagate through the many different layers of applications easily.

We’ve also made the GameController harder to test. Since the PlayerGenerator is created inside the Index() method, we can’t mock it. This would make our unit tests unreliable. We aren’t able to test the unit in this case, so the tests wouldn’t even be the real unit tests. This would make them more like integration tests since we would be testing the integration of PlayerGenerator and GameController.

Lastly, we’re creating dirty and unreadable code in the long run. Imagine if every dependency in the project is created this way. We would have a lot of redundant code, and we would have to change an entire application whenever the slightest change in logic is made.

So what can we do to improve this situation?

We can use dependency injection.

Let’s see how.

Making the Solution More Flexible with Dependency Injection

First, we need to create an interface IPlayerGenerator. We can do that manually, or we can go to PlayerGenerator class and press Ctrl + . on the class name and click on the Extract Interface. The latter approach is faster, and it automatically adds the interface to our class:

public class PlayerGenerator : IPlayerGenerator
{
...
}

The BetterPlayerGenerator class doesn’t exist yet, so let’s create it in the Services folder and add the interface manually:

using DependencyInjectionDemo.Models;

namespace DependencyInjectionDemo.Services;

public class BetterPlayerGenerator : IPlayerGenerator
{
    public Player CreateNewPlayer()
    {
        return new Player
        {
            Name = "Minsc",
            Gender = Gender.Male,
            HairColor = HairColor.Black,
            Age = 35,
            Strength = 18,
            Race = "Human"
        };
    }
}

Its logic is completely different: it always creates Minsc, at full strength, which makes him easy to spot on the page later.

And the interface is generated in the Services folder:

using DependencyInjectionDemo.Models;

namespace DependencyInjectionDemo.Services;

public interface IPlayerGenerator
{
    Player CreateNewPlayer();
}

Now both our classes implement the IPlayerGenerator interface.

Now we need to go to our GameController and use the Dependency Injection to add our dependency:

using DependencyInjectionDemo.Services;
using Microsoft.AspNetCore.Mvc;

namespace DependencyInjectionDemo.Controllers;

public class GameController : Controller
{
    private readonly ILogger<GameController> _logger;
    private readonly IPlayerGenerator _playerGenerator;

    public GameController(ILogger<GameController> logger, IPlayerGenerator playerGenerator)
    {
        _logger = logger;
        _playerGenerator = playerGenerator;
    }

    public IActionResult Index()
    {
        var newPlayer = _playerGenerator.CreateNewPlayer();
        _logger.LogInformation("Created a new player: {Name}", newPlayer.Name);

        return View(newPlayer);
    }
}

We’ve added our dependency via constructor injection, and we’ve assigned it to a private readonly field. This is a good practice, as we’re preventing accidental assignment anywhere else outside the constructor.

Now we can simply call the _playerGenerator.CreateNewPlayer() method in the Index() method.

We also ask for an ILogger<GameController> to log the name of every new player. We don’t have to register the logger, because ASP.NET Core registers logging for us.

Great, but what happens now if we run the application?

The developer exception page reports an InvalidOperationException: the container can't resolve IPlayerGenerator while MVC creates GameController.

We get a nice big InvalidOperationException, stating that the service cannot be resolved.

But why does this happen?

It happens because we haven’t registered our service yet with our dependency injection container. It’s only logical because the container is not aware of which interface resolves to which class, yet.

The same exception has a few other causes, too, and our guide to the unable to resolve service for type error covers them.

Let’s see how we can register our service.

Registering the Service with Dependency Injection Container

In order to get rid of that InvalidOperationException, we need to register our service.

We can do that in the Program.cs file, by adding a single line before builder.Build(), and a using directive for our Services namespace at the top:

using DependencyInjectionDemo.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddScoped<IPlayerGenerator, PlayerGenerator>();

builder.Services.AddControllersWithViews();

var app = builder.Build();

We’ve added the PlayerGenerator service to the service collection with AddScoped() method.

In short, scoped lifetime means that the instance is created on every request. For now, let’s leave it like this, but we’ll talk more about dependency injection lifetimes.

The important thing is that the service registration is necessary and that we can add it as a transient, scoped or singleton service to our IServiceCollection.

Notice that Program.cs is now the only file in our app that knows which class stands behind IPlayerGenerator. The controller asks for the interface, and Program.cs decides what it gets. That’s what we call the composition root.

GameController references the IPlayerGenerator interface, and PlayerGenerator implements it

If we run the application again, it should work just fine. And our logger reports every new player in the console:

info: DependencyInjectionDemo.Controllers.GameController[0]
      Created a new player: Xzar

How Does Dependency Injection Help?

So what happens now if we want to introduce the BetterPlayerGenerator class now in our controller?

We don’t need to change a thing in the GameController itself!

We just need to change the registration in Program.cs:

builder.Services.AddScoped<IPlayerGenerator, BetterPlayerGenerator>();

That’s it!

Now, no matter how many times we refresh /Game, we get Minsc:

info: DependencyInjectionDemo.Controllers.GameController[0]
      Created a new player: Minsc

Although the BetterPlayerGenerator has a completely different player generation logic, it doesn’t affect our controller.

That’s a much better way of doing it!

So we’ve completed several goals with this approach:

  • Made the code cleaner
  • Decoupled controller from the service implementation
  • Made the controller easier to test
  • Created a more robust foundation for our solution (using abstractions over concrete classes)

Different Service Registration Lifetimes

As we’ve already mentioned, there are three different registration lifetimes we can choose from when registering our services.

These are:

  • Transient: We can use this for lightweight, stateless services. Each time the service is requested from the container, the new instance is created
  • Scoped: The instance of the service is created once per request and within that request (scope) it is reused
  • Singleton: The instance is created only once, and the whole application shares it

We’ll talk more about these DI lifetimes.

What Happens When a Singleton Depends on a Scoped Service?

Say we want a player of the day, one player that everybody gets until the app restarts. That sounds like a job for a singleton. Let’s add a PlayerOfTheDay class to the Services folder, but don’t keep this version, because it’s wrong:

using DependencyInjectionDemo.Models;

namespace DependencyInjectionDemo.Services;

public class PlayerOfTheDay
{
    public PlayerOfTheDay(IPlayerGenerator playerGenerator)
    {
        Player = playerGenerator.CreateNewPlayer();
    }

    public Player Player { get; }
}

And let’s register it as a singleton in Program.cs, right below our generator:

builder.Services.AddSingleton<PlayerOfTheDay>();

Now, if we run the application, it doesn’t even start:

Unhandled exception. System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: DependencyInjectionDemo.Services.PlayerOfTheDay Lifetime: Singleton ImplementationType: DependencyInjectionDemo.Services.PlayerOfTheDay': Cannot consume scoped service 'DependencyInjectionDemo.Services.IPlayerGenerator' from singleton 'DependencyInjectionDemo.Services.PlayerOfTheDay'.)

Why?

A singleton lives as long as the application, but our generator is scoped, so it should live only as long as one request. If the container let this through, the generator would be stuck inside the singleton for good. Microsoft’s dependency injection guidelines call this a captive dependency, a case “where a longer-lived service holds a shorter-lived service captive”.

Our generator has no state, so nothing bad would happen here. But put an EF Core DbContext in its place, keep it in a field, and all your requests would share one context, which isn’t thread safe.

We see the error only because dotnet run starts the app in the Development environment. There, ASP.NET Core turns on two checks of its container, ValidateScopes and ValidateOnBuild, and together they catch the mistake at startup. In every other environment, both are off, and the same code starts without a word.

If we want these checks everywhere, we can turn them on in Program.cs, before builder.Build():

builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true;
    options.ValidateOnBuild = true;
});

So how do we fix our PlayerOfTheDay? When a singleton needs a scoped service, it can create a scope of its own. Let’s change the class:

using DependencyInjectionDemo.Models;

namespace DependencyInjectionDemo.Services;

public class PlayerOfTheDay
{
    public PlayerOfTheDay(IServiceScopeFactory scopeFactory)
    {
        using var scope = scopeFactory.CreateScope();
        var playerGenerator = scope.ServiceProvider.GetRequiredService<IPlayerGenerator>();

        Player = playerGenerator.CreateNewPlayer();
    }

    public Player Player { get; }
}

This time, we ask for IServiceScopeFactory instead of the generator. The container registers it as a singleton, so it’s safe to keep in one. Then, we create a scope, get our generator from it and create the player. When the constructor ends, using disposes the scope, along with any disposable service it created.

If we run the application now, it starts just fine.

Conclusion

In this article, we’ve covered concepts like Dependency Injection, Inversion of Control, and Dependency Inversion principle. We’ve learned how they are related and how the control over creating dependencies can be inverted by using dependency injection.

We’ve also created a small example to see how we can apply dependency injection in ASP.NET Core application, and we’ve seen how beneficial this approach can be. We’ve registered our services in Program.cs, our composition root, and seen how the container, in Development, stops a singleton from holding a scoped service.

To learn more about the different dependency injection lifecycles, check out our article on dependency injection lifetimes.

Tested with .NET 10.0.12.