Understand dependency injection

Completed

ASP.NET Core apps often need to access the same services across multiple components. For example, several components might need to access a service that fetches data from a database. ASP.NET Core uses a built-in dependency injection (DI) container to manage the services that an app uses.

Dependency injection and Inversion of Control (IoC)

The dependency injection pattern is a form of Inversion of Control (IoC). In the dependency injection pattern, a component receives its dependencies from external sources rather than creating them itself. This pattern decouples the code from the dependency, which makes code easier to test and maintain.

Consider the following Program.cs file:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using MyApp.Services;

var builder = WebApplication.CreateBuilder(args);
    
builder.Services.AddSingleton<PersonService>();
var app = builder.Build();

app.MapGet("/", 
    (PersonService personService) => 
    {
        return $"Hello, {personService.GetPersonName()}!";
    }
);
    
app.Run();

And the following PersonService.cs file:

namespace MyApp.Services;

public class PersonService
{
    public string GetPersonName()
    {
        return "John Doe";
    }
}

To understand the code, start with the highlighted app.MapGet code. This code maps HTTP GET requests for the root URL (/) to a delegate that returns a greeting message. The delegate's signature defines an PersonService parameter named personService. When the app runs and a client requests the root URL, the code inside the delegate depends on the PersonService service to get some text to include in the greeting message.

Where does the delegate get the PersonService service? It's implicitly provided by the service container. The highlighted builder.Services.AddSingleton<PersonService>() line tells the service container to create a new instance of the PersonService class when the app starts, and to provide that instance to any component that needs it.

Any component that needs the PersonService service can declare a parameter of type PersonService in its delegate signature. The service container will automatically provide an instance of the PersonService class when the component is created. The delegate doesn't create the PersonService instance itself, it just uses the instance that the service container provides.

Interfaces and dependency injection

To avoid dependencies on a specific service implementation, you can instead configure a service for a specific interface and then depend just on the interface. This approach gives you the flexibility to swap out the service implementation, which makes the code more testable and easier to maintain.

Consider an interface for the PersonService class:

public interface IPersonService
{
    string GetPersonName();
}

This interface defines the single method, GetPersonName, that returns a string. This PersonService class implements the IPersonService interface:

internal sealed class PersonService : IPersonService
{
    public string GetPersonName()
    {
        return "John Doe";
    }
}

Instead of registering the PersonService class directly, you can register it as an implementation of the IPersonService interface:

var builder = WebApplication.CreateBuilder(args);
    
builder.Services.AddSingleton<IPersonService, PersonService>();
var app = builder.Build();

app.MapGet("/", 
    (IPersonService personService) => 
    {
        return $"Hello, {personService.GetPersonName()}!";
    }
);
    
app.Run();

This example Program.cs differs from the previous example in two ways:

  • The PersonService instance is registered as an implementation of the IPersonService interface (as opposed to registering the PersonService class directly).
  • The delegate signature now expects an IPersonService parameter instead of a PersonService parameter.

When the app runs and a client requests the root URL, the service container provides an instance of the PersonService class because it's registered as the implementation of the IPersonService interface.

Tip

Think of IPersonService as a contract. It defines the methods and properties that an implementation must have. The delegate wants an instance of IPersonService. It doesn't care at all about the underlying implementation, only that the instance has the methods and properties defined in the contract.

Testing with dependency injection

Using interfaces makes it easier to test components in isolation. You can create a mock implementation of the IPersonService interface for testing purposes. When you register the mock implementation in the test, the service container provides the mock implementation to the component being tested.

For example, say that instead of returning a hard-coded string, the GetPersonName method in the PersonService class fetches the name from a database. To test the component that depends on the IPersonService interface, you can create a mock implementation of the IPersonService interface that returns a hard-coded string. The component being tested doesn't know the difference between the real implementation and the mock implementation.

Also suppose your app maps an API endpoint that returns a greeting message. The endpoint depends on the IPersonService interface to get the name of the person to greet. The code that registers the IPersonService service and maps the API endpoint might look like this:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IPersonService, PersonService>();

var app = builder.Build();

app.MapGet("/", (IPersonService personService) =>
{
    return $"Hello, {personService.GetPersonName()}!";
});

app.Run();

This is similar the previous example with IPersonService. The delegate expects an IPersonService parameter, which the service container provides. As mentioned earlier, assume that the PersonService that implements the interface fetches the name of the person to greet from a database.

Now consider the following XUnit test that tests the same API endpoint:

Tip

Don't worry if you're not familiar with XUnit or Moq. Writing unit tests is outside the scope of this module. This example is just to illustrate how dependency injection can be used in testing.

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using MyWebApp;
using System.Net;

public class GreetingApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public GreetingApiTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task GetGreeting_ReturnsExpectedGreeting()
    {
        //Arrange
        var mockPersonService = new Mock<IPersonService>();
        mockPersonService.Setup(service => service.GetPersonName()).Returns("Jane Doe");

        var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddSingleton(mockPersonService.Object);
            });
        }).CreateClient();

        // Act
        var response = await client.GetAsync("/");
        var responseString = await response.Content.ReadAsStringAsync();

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal("Hello, Jane Doe!", responseString);
    }
}

The preceding test:

  • Creates a mock implementation of the IPersonService interface that returns a hard-coded string.
  • Registers the mock implementation with the service container.
  • Creates an HTTP client to make a request to the API endpoint.
  • Asserts that the response from the API endpoint is as expected.

The test doesn't care how the PersonService class gets the name of the person to greet. It only cares that the name is included in the greeting message. The test uses a mock implementation of the IPersonService interface to isolate the component being tested from the real implementation of the service.