Archives 08/13/2024

Project – Strategy – Dependency Injection

In the Strategy project, we delve into various methods of injecting dependencies, transitioning from the Control Freak approach to a SOLID one. Through this exploration, we evaluate the advantages and drawbacks of each technique.The project takes the form of a travel agency’s location API, initially returning only hardcoded cities. We’ve implemented the same endpoint five times across different controllers to facilitate comparison and trace the progression. Each controller comes in pair except for one. The pairs comprise a base controller that uses an in-memory service (dev) and an updated controller that simulates a SQL database (production). Here’s the breakdown of each controller:

  • The ControlFreakLocationsController instantiates the InMemoryLocationService class using the new keyword.
  • The ControlFreakUpdatedLocationsController instantiates the SqlLocationService class and its dependency using the new keyword.
  • The InjectImplementationLocationsController leverages constructor injection to get an instance of the InMemoryLocationService class from the container.
  • The InjectImplementationUpdatedLocationsController leverages constructor injection to get an instance of the SqlLocationService class from the container.
  • The InjectAbstractionLocationsController leverages dependency injection and interfaces to let its consumers change its behavior at runtime.

The controllers share the same building blocks; let’s start there.

Shared building blocks

The Location data structure is the following:

namespace Strategy.Models;
public record class Location(int Id, string Name, string CountryCode);

The LocationSummary DTO returned by the controller is the following:

namespace Strategy.Controllers;
public record class LocationSummary(int Id, string Name);

The service interface is the following and has only one method that returns one or more Location objects:

using Strategy.Models;
namespace Strategy.Services;
public interface ILocationService
{
    Task<IEnumerable<Location>> FetchAllAsync(CancellationToken cancellationToken);
}

The two implementations of this interface are an in-memory version to use when developing and a SQL version to use when deploying (let’s call this production to keep it simple).The in-memory service returns a predefined list of cities:

using Strategy.Models;
namespace Strategy.Services;
public class InMemoryLocationService : ILocationService
{
    public async Task<IEnumerable<Location>> FetchAllAsync(CancellationToken cancellationToken)
    {
        await Task.Delay(Random.Shared.Next(1, 100), cancellationToken);
        return new Location[] {
            new Location(1, “Paris”, “FR”),
            new Location(2, “New York City”, “US”),
            new Location(3, “Tokyo”, “JP”),
            new Location(4, “Rome”, “IT”),
            new Location(5, “Sydney”, “AU”),
            new Location(6, “Cape Town”, “ZA”),
            new Location(7, “Istanbul”, “TR”),
            new Location(8, “Bangkok”, “TH”),
            new Location(9, “Rio de Janeiro”, “BR”),
            new Location(10, “Toronto”, “CA”),
        };
    }
}

The SQL implementation uses an IDatabase interface to access the data:

using Strategy.Data;
using Strategy.Models;
namespace Strategy.Services;
public class SqlLocationService : ILocationService
{
    private readonly IDatabase _database;
    public SqlLocationService(IDatabase database) {
        _database = database;
    }
    public Task<IEnumerable<Location>> FetchAllAsync(CancellationToken cancellationToken) {
        return _database.ReadManyAsync<Location>(
            “SELECT * FROM Location”,
            cancellationToken
        );
    }
}

That database access interface is simply the following:

namespace Strategy.Data;
public interface IDatabase
{
    Task<IEnumerable<T>> ReadManyAsync<T>(string sql, CancellationToken cancellationToken);
}

In the project itself, the IDatabase interface has only the NotImplementedDatabase implementation, which throws a NotImplementedException when its ReadManyAsync method is called:

namespace Strategy.Data;
public class NotImplementedDatabase : IDatabase
{
    public Task<IEnumerable<T>> ReadManyAsync<T>(string sql, CancellationToken cancellationToken)
        => throw new NotImplementedException();
}

Since the goal is not learning database access, I mocked that part in a test case in a xUnit test using the controller and the SqlLocationService class.

With those shared pieces, we can start with the first two controllers.