Injecting an abstraction in the controller – Dependency Injection

In this last controller, we leverage the SOLID principles, constructor injection, and, inherently, the Strategy pattern to build a controller that we can change from the outside. All we have to do to make the code flexible is inject the interface instead of its implementation, like this:

using Microsoft.AspNetCore.Mvc;
using Strategy.Services;
namespace Strategy.Controllers;
[Route(“travel/[controller]”)]
[ApiController]
public class InjectAbstractionLocationsController : ControllerBase
{
    private readonly ILocationService _locationService;
    public InjectAbstractionLocationsController(ILocationService locationService)
    {
        _locationService = locationService;
    }
    [HttpGet]
    public async Task<IEnumerable<LocationSummary>> GetAsync(CancellationToken cancellationToken)
    {
        var locations = await _locationService.FetchAllAsync(cancellationToken);
        return locations.Select(l => new LocationSummary(l.Id, l.Name));
    }
}

The highlighted lines showcase the changes. Injecting the ILocationService interface lets us control if we inject an instance of the InMemoryLocationService class, the SqlLocationService class, or any other implementation we’d like.This is the most flexible possibility we can get.Advantages:

  • Using constructor injection allows changing the dependency in one place, and all the methods get it (assuming we have more than one method).
  • Injecting the ILocationService interface allows us to inject any of its implementations without changing the code.
  • Because of the ILocationService interface, the controller is loosely coupled with its dependencies.

Disadvantages:

  • Understanding what objects the controller uses is harder since the dependencies are resolved at runtime. However, this forces us to program against an interface instead (a good thing).

Let’s have a look at this flexibility in action.

Constructing the InjectAbstractionLocationsController

I created a few xUnit tests to explore the possibilities, making it easy to create classes manually.

I used Moq to mock implementations. If you are unfamiliar with Moq and want to learn more, I left a link in the Further Reading section.

Two of the tests refers to the following member, a static Location object:

public static Location ExpectedLocation { get; }
    = new Location(11, “Montréal”, “CA”);

The test cases are not to assess the correctness of our code but to explore how easy it is to compose the controller differently. Let’s explore the first test case.

Mock_the_IDatabase

The first is an integration test that injects an instance of the SqlLocationService class into the controller and mocks the database. The fake database returns a collection of one item. That item is the Location instance referenced by the ExpectedLocation property. Here’s that code:

var databaseMock = new Mock<IDatabase>();
databaseMock.Setup(x => x.ReadManyAsync<Location>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
    .ReturnsAsync(() => new Location[] { ExpectedLocation })
;
var sqlLocationService = new SqlLocationService(
    databaseMock.Object);
var sqlController = new InjectAbstractionLocationsController(
    sqlLocationService);

The preceding code shows how we can control the dependency we inject into the classes because of how the InjectAbstractionLocationsController was designed. We can’t say the same about the four other controller versions.Next, we call the GetAsync method to verify that everything works as expected:

var result = await sqlController.GetAsync(CancellationToken.None);

Finally, let’s verify we received that collection of one object:

Assert.Collection(result,
    location =>
    {
        Assert.Equal(ExpectedLocation.Id, location.Id);
        Assert.Equal(ExpectedLocation.Name, location.Name);
    }
);

Optionally, or instead, we could validate the service called the database mock, like this:

databaseMock.Verify(x => x
    .ReadManyAsync<Location>(
        It.IsAny<string>(),
        It.IsAny<CancellationToken>()
    ),
    Times.Once()
);

There are a lot of useful features in the Moq library.

Validating that the code was correct is not important for this example. The key is to understand the composition of the controller, which the following diagram represents:

 Figure 8.1: Composition of the controller in a test that mocks the IDatabase interfaceFigure 8.1: Composition of the controller in a test that mocks the IDatabase interface 

As we can see from the diagram, the classes depend on interfaces, and we inject implementations when building them. The next two tests are simpler than this, only depending on the ILocationService. Let’s explore the second one.

Leave a Reply

Your email address will not be published. Required fields are marked *



         


          Terms of Use | Accessibility Privacy