DTO controller – Model-View-Controller

To solve our problems, we reimplement the controller using DTOs. To make it easier to follow along, here are all the DTOs as a reference:

namespace Shared.DTO;
public record class ContractDetails(
    int Id,
    string Name,
    string Description,
    int StatusTotalWork,
    int StatusWorkDone,
    string StatusWorkState,
    string PrimaryContactFirstName,
    string PrimaryContactLastName,
    string PrimaryContactEmail
);
public record class CustomerDetails(
    int Id,
    string Name,
    IEnumerable<ContractDetails> Contracts
);
public record class CustomerSummary(
    int Id,
    string Name,
    int TotalNumberOfContracts,
    int NumberOfOpenContracts
);
public record class CreateCustomer(string Name);
public record class UpdateCustomer(string Name);

First, let’s fix our update problem, starting with the reimplementation of the update endpoint leveraging DTOs (see the DTOCustomersController.cs file):

// PUT dto/customers/1
[HttpPut(“{customerId}”)]
public async Task<ActionResult<CustomerDetails>> PutAsync(
        int customerId,
        [FromBody] UpdateCustomer input,
        ICustomerRepository customerRepository)
{
    // Get the customer
    var customer = await customerRepository.FindAsync(
        customerId,
        HttpContext.RequestAborted
    );
    if (customer == null)
    {
        return NotFound();
    }
    // Update the customer’s name using the UpdateCustomer DTO
    var updatedCustomer = await customerRepository.UpdateAsync(
        customer with { Name = input.Name },
        HttpContext.RequestAborted
    );
    if (updatedCustomer == null)
    {
        return Conflict();
    }
    // Map the updated customer to a CustomerDetails DTO
    var dto = MapCustomerToCustomerDetails(updatedCustomer);
    // Return the DTO
    return dto;
}

In the preceding code, the main differences are (highlighted):

  • The request body is now bound to the UpdateCustomer class instead of the Customer itself.
  • The action method returns an instance of the CustomerDetails class instead of the Customer itself.

However, we can see more code in our controller action than before. That’s because the controller now handles the data changes instead of the clients. The action now does:

  1. Load the data from the database.
  2. Ensure the entity exists.
  3. Use the input DTO to update the data, limiting the clients to a subset of properties.
  4. Proceed with the update.
  5. Ensure the entity still exists (handles conflicts).
  6. Copy the Customer into the output DTO and return it.

By doing this, we now control what the clients can do when they send a PUT request through the input DTO (UpdateCustomer). Moreover, we encapsulated the logic to calculate the statistics on the server. We hid the computation behind the output DTO (CustomerDetails), which lowers the complexity of our user interface and allows us to improve the performance without impacting any of our clients (loose coupling).Furthermore, we now use the customerId parameter.If we send the same HTTP request as before, which sends more data than we accept, only the customer’s name will change. On top of that, we get all the data we need to display the customer’s statistics. Here’s a response example:

{
  “id”: 1,
  “name”: “Some new name”,
  “contracts”: [
    {
      “id”: 1,
      “name”: “First contract”,
      “description”: “This is the first contract.”,
      “statusTotalWork”: 100,
      “statusWorkDone”: 100,
      “statusWorkState”: “Completed”,
      “primaryContactFirstName”: “John”,
      “primaryContactLastName”: “Doe”,
      “primaryContactEmail”: “[email protected]
    },
    {
      “id”: 2,
      “name”: “Some other contract”,
      “description”: “This is another contract.”,
      “statusTotalWork”: 100,
      “statusWorkDone”: 25,
      “statusWorkState”: “InProgress”,
      “primaryContactFirstName”: “Jane”,
      “primaryContactLastName”: “Doe”,
      “primaryContactEmail”: “[email protected]
    }
  ]
}

As we can see from the preceding response, only the customer’s name changed, but we now received the statusWorkDone and statusTotalWork fields. Lastly, we flattened the data structure.

DTOs are a great resource to flatten data structures, but you don’t have to. You must always design your systems, including DTOs and data contracts, for specific use cases.

As for the dashboard, the “get all customers” endpoint achieves this by doing something similar. It outputs a collection of CustomerSummary objects instead of the customers themselves. In this case, the controller executes the calculations and copies the entity’s relevant properties to the DTO. Here’s the code:

// GET: dto/customers
[HttpGet]
public async Task<IEnumerable<CustomerSummary>> GetAllAsync(
    ICustomerRepository customerRepository)
{
    // Get all customers
    var customers = await customerRepository.AllAsync(
        HttpContext.RequestAborted
    );
    // Map customers to CustomerSummary DTOs
    var customersSummary = customers
        .Select(customer => new CustomerSummary(
            Id: customer.Id,
            Name: customer.Name,
            TotalNumberOfContracts: customer.Contracts.Count,
            NumberOfOpenContracts: customer.Contracts.Count(x => x.Status.State != WorkState.Completed)
        ))
    ;
    // Return the DTOs
    return customersSummary;
}

In the preceding code, the action method:

  1. Read the entities
  2. Create the DTOs and calculate the number of open contracts.
  3. Return the DTOs.

As simple as that, we now encapsulated the computation on the server.

You should optimize such code based on your real-life data source. In this case, a static List<T> is low latency. However, querying the whole database to get a count can become a bottleneck.

Calling the endpoint results in the following:

[
  {
    “id”: 1,
    “name”: “Some new name”,
    “totalNumberOfContracts”: 2,
    “numberOfOpenContracts”: 1
  },
  {
    “id”: 2,
    “name”: “Some mega-corporation”,
    “totalNumberOfContracts”: 1,
    “numberOfOpenContracts”: 1
  }
]

It is now super easy to build our dashboard. We can query that endpoint once and display the data in the UI. The UI offloaded the calculation to the backend.

User interfaces tend to be more complex than APIs because they are stateful. As such, offloading as much complexity to the backend helps. You can use a Backend-for-frontend (BFF) to help with this task. We explore ways to layer APIs, including the BFF pattern in Chapter 19, Introduction to Microservices Architecture.

Lastly, you can play with the API using the HTTP requests in the MVC.API.DTO.http file. I implemented all the endpoints using a similar technique. If your controller logic becomes too complex, it is good practice to encapsulate it into other classes. We explore many techniques to organize application code in Section 4: Applications patterns.

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.

What’s an object’s lifetime? – Dependency Injection

When we create an instance manually, using the new keyword, we create a hold on that object; we know when we create it and when its life ends. That’s the lifetime of the object.Of course, using the new keyword leaves no chance to control these objects from the outside, enhance them, intercept them, or swap them for another implementation—as covered in the preceding Code smell – Control Freak section.

.NET object lifetime

With dependency injection, we need to forget about controlling objects and start to think about using dependencies, or more explicitly, depending on their interfaces. In ASP.NET Core, there are three possible lifetimes to choose from:

LifetimeDescription
TransientThe container creates a new instance every time.
ScopedThe container creates an instance per HTTP request and reuses it. In some rare cases, we can also create custom scopes.
SingletonThe container creates a single instance of that dependency and always reuses that unique object.

 Table 8.1: objects lifetime description

We can now manage our volatile dependencies using one of those three scopes. Here are some questions to help you choose:

  • Do I need a single instance of my dependency? Yes? Use the singleton lifetime.
  • Do I need a single instance of my dependency shared over an HTTP request? Yes? Use the scoped lifetime.
  • Do I need a new instance of my dependency every time? Yes? Use the transient lifetime.

A general approach to object lifetime is to design the components to be singletons. When impossible, we go for scoped. When scoped is also impossible, go for transient. This way, we maximize instance reuse, lower the overhead of creating objects, lower the memory cost of keeping those objects in memory, and lower the amount of garbage collection needed to remove unused instances.

For example, we can pick singleton mindlessly for stateless objects, which are the easiest to maintain and less likely to break.

For stateful objects, where multiple consumers use the same instance, we must ensure the object is thread-safe if the lifetime is singleton or scoped because multiple consumers could try to access it simultaneously.

One essential aspect to consider when choosing a lifetime is the consumers of stateful objects. For example, if we load data related to the current user, we must ensure that data do not leak to other users. To do so, we can define the lifetime of that object to scoped, which is limited to a single HTTP request. If we don’t want to reuse that state between multiple consumers, we can choose a transient lifetime to ensure every consumer gets their own instance.

How does that translate into code? .NET offers multiple extension methods to help us configure the lifetimes of our objects, like AddTransient, AddScoped, and AddSingleton, which explicitly state their lifetimes.

We use the built-in container throughout the book with many of its registration methods, so you should grow familiar with it very quickly. It has good discoverability, so you can explore the possibilities using IntelliSense while writing code or reading the documentation.

Next, we use those methods and explore how to register dependencies with the container.

Code smell – Control Freak – Dependency Injection

Control freak is a code smell and even an anti-pattern that forbids us from using the new keyword. Yes, using the new keyword is the code smell! The following code is wrong and can’t leverage DI:

namespace CompositionRoot.ControlFreak;
public class Consumer
{
    public void Do()
    {
        var dependency = new Dependency();
        dependency.Operation();
    }
}
public class Dependency
{
    public void Operation()
        => throw new NotImplementedException();
}

The highlighted line shows the anti-pattern in action. To enable the Consumer class to use dependency injection, we could update it like the following:

public class Consumer
{
    private readonly Dependency _dependency;
    public DIEnabledConsumer(Dependency dependency)
    {
        _dependency = dependency;
    }
    public void Do()
    {
        _dependency.Operation();
    }
}

The preceding code removes the new keyword and is now open for modification. The highlighted lines represent the constructor injection pattern we explore subsequently in this chapter.Nevertheless, do not ban the new keyword just yet. Instead, every time you use it, ask yourself whether the object you instantiated using the new keyword is a dependency that could be managed by the container and injected instead.To help with that, I borrowed two terms from Mark Seemann’s book Dependency Injection in .NET; the name Control Freak also comes from that book. He describes the following two categories of dependencies:

  • Stable dependencies
  • Volatile dependencies

Next is my take on defining them.

Stable dependencies

Stable dependencies should not break our application when a new version is released. They should use deterministic algorithms (input X should always produce output Y), and you should not expect to change them with something else in the future.

Most data structures devoided of behaviors, like Data Transfer Objects (DTOs), fall into this category. You can also consider the .NET BCL as stable dependencies.

We can still instantiate objects using the new keyword when they fall into this category because the dependencies are stable and unlikely to break anything if they change.Next, we look at their counterpart.

Volatile dependencies

Volatile dependencies can change at runtime, like extendable elements with contextual behaviors. They may also be likely to change for various reasons like new features development.

Most classes we create, such as data access and business logic code, are volatile dependencies.

The primary way to break the tight coupling between classes is to rely on interfaces and DI and no longer instantiate those volatile dependencies using the new keyword. Volatile dependencies are why dependency injection is key to building flexible, testable, and maintainable software.

Conclusion

To conclude this interlude: don’t be a control freak anymore; those days are behind you!

When in doubt, inject the dependency instead of using the new keyword.

Next, we explore the available lifetimes we can attribute to our volatile dependencies.

Object lifetime

Now that we understand we should no longer use the new keyword, we need a way to create those classes. From now on, the IoC container will play that role and manage object instantiation and their lifetime for us.

Striving for adaptability – Dependency Injection

To achieve a high degree of flexibility with DI, we can apply the following formula, driven by the SOLID principles:Object A should not know about object B that it is using. Instead, A should use an interface, I, implemented by B, and B should be resolved and injected at runtime.Let’s decompose this:

  • Object A should depend on interface I instead of concrete type B.
  • Instance B, injected into A, should be resolved by the IoC container at runtime.
  • A should not be aware of the existence of B.
  • A should not control the lifetime of B.

We can also inject objects directly without passing by an interface. It all depends on what we inject, in what context, and our requirements. We tackle many use cases throughout the book to help you understand DI.

Next, we translate this equation into an analogy that helps explain the reasons to use a container.

Understanding the use of the IoC container

To better understand the use of the IoC container and to create an image around the previous adaptability concept, let’s start with a LEGO® analogy where IoC is the equivalent of drawing a plan to build a LEGO®castle:

  1. We draw the plan
  2. We gather the blocks
  3. We press the start button on a hypothetical robot builder
  4. The robot assembles the blocks by following our plan
  5. The castle is complete

By following this logic, we can create a new 4×4 block with a unicorn painted on its side (concrete type), update the plan (composition root), and then press the restart button to rebuild the castle with that new block inserted into it, replacing the old one without affecting the structural integrity of the castle (program). As long as we respect the 4×4 block contract (interface), everything is updatable without impacting the rest of the castle, leading to great flexibility.Following that idea, if we need to manage every single LEGO® block one by one, it would quickly become incredibly complex! Therefore, managing all dependencies by hand in a project would be super tedious and error-prone, even in the smallest program. This situation is where an IoC container (the hypothetical robot builder) comes into play.

The role of an IoC container

An IoC container manages objects for us. We configure it, and then, when we ask for a service, the container resolves and injects it. On top of that, the container manages the lifetime of dependencies, leaving our classes to do only one thing, the job we designed them to do. No more need to think about their dependencies!The bottom line is that an IoC container is a DI framework that does the auto-wiring for us. We can conceptualize Dependency Injection as follows:

  1. The consumer of a dependency states its needs about one or more dependencies (contracts).
  2. The IoC container injects that dependency (implementation) upon creating the consumer, fulfilling its needs at runtime.

Next, we explore an code smell that applying Dependency Injection helps us avoid.

What is dependency injection? – Dependency Injection

Before you begin: Join our book community on Discord

Give your feedback straight to the author himself and chat to other early readers on our Discord server (find the “architecting-aspnet-core-apps-3e” channel under EARLY ACCESS SUBSCRIPTION).

https://packt.link/EarlyAccess

This chapter explores the ASP.NET Core Dependency Injection (DI) system, how to leverage it efficiently, and its limits and capabilities.We learn to compose objects using DI and delve into the Inversion of Control (IoC) principle. As we traverse the landscape of the built-in DI container, we explore its features and potential uses.Beyond practical examples, we lay down the conceptual foundation of Dependency Injection to understand its purpose, its benefits, and the problems it solves and to lay down the ground for the rest of the book as we rely heavily on DI.We then return to the first three Gang of Four (GoF) design patterns we encountered, but this time, through the lens of Dependency Injection. By refactoring these patterns using DI, we gain a more holistic understanding of how this powerful design tool influences the structure and flexibility of our software.Dependency Injection is a cornerstone in your path toward mastering modern application design and its transformative role in developing efficient, adaptable, testable, and maintainable software.In this chapter, we cover the following topics:

  • What is dependency injection?
  • Revisiting the Strategy pattern
  • Understanding guard clauses
  • Revisiting the Singleton pattern
  • Understanding the Service Locator pattern
  • Revisiting the Factory pattern

What is dependency injection?

DI is a way to apply the Inversion of Control (IoC) principle. IoC is a broader version of the dependency inversion principle (the D in SOLID).The idea behind DI is to move the creation of dependencies from the objects themselves to the composition root. That way, we can delegate the management of dependencies to an IoC container, which does the heavy lifting.

An IoC container and a DI container are the same thing—they’re just different words people use. I use both interchangeably in real life, but I stick to IoC container in the book because it seems more accurate than DI container.

IoC is the concept (the principle), while DI is a way of inverting the flow of control (applying IoC). For example, you apply the IoC principle (inverting the flow) by injecting dependencies at runtime (doing DI) using a container. Feel free to use any or both.

Next, we define the composition root.

The composition root

A critical concept behind DI is the composition root. The composition root is where we tell the container about our dependencies and their expected lifetime: where we compose our dependency trees. The composition root should be as close to the program’s starting point as possible, so from ASP.NET Core 6 onward, the composition root is in the Program.cs file. In the previous versions, it was in the Program or Startup classes.Next, we explore how to leverage DI to create highly adaptable systems.

The Singleton design pattern – Strategy, Abstract Factory, and Singleton Design Patterns

The Singleton design pattern allows creating and reusing a single instance of a class. We could use a static class to achieve almost the same goal, but not everything is doable using static classes. For example, a static class can’t implement an interface. We can’t pass an instance of a static class as an argument because there is no instance. We can only use static classes directly, which leads to tight coupling every time.The Singleton pattern in C# is an anti-pattern, and we should rarely use it, if ever, and use dependency injection instead. That said, it is a classic design pattern worth learning to at least avoid implementing it. We explore a better alternative in the next chapter.Here are a few reasons why we are covering this pattern:

  • It translates into a singleton scope in the next chapter.
  • Without knowing about it, you cannot locate it, try to remove it, or avoid its usage.
  • It is a simple pattern to explore.
  • It leads to other patterns, such as the Ambient Context pattern.

Goal

The Singleton pattern limits the number of instances of a class to one. Then, the idea is to reuse the same instance subsequently. A singleton encapsulates both the object logic itself and its creational logic. For example, the Singleton pattern could lower the cost of instantiating an object with a large memory footprint since the program instantiates it only once.Can you think of a SOLID principle that gets broken right there?The Singleton pattern promotes that one object must have two responsibilities, breaking the Single Responsibility Principle (SRP). A singleton is the object itself and its own factory.

Design

This design pattern is straightforward and is limited to a single class. Let’s start with a class diagram:

 Figure 7.6: Singleton pattern class diagramFigure 7.6: Singleton pattern class diagram 

The Singleton class is composed of the following:

  • A private static field that holds its unique instance.
  • A public static Create() method that creates or returns the unique instance.
  • A private constructor, so external code cannot instantiate it without passing by the Create method.

You can name the Create() method anything or even get rid of it, as we see in the next example. We could name it GetInstance(), or it could be a static property named Instance or bear any other relevant name.

We can translate the preceding diagram to the following code:

public class MySingleton
{
    private static MySingleton?
_instance;
    private MySingleton() { }
    public static MySingleton Create()
    {
        _instance ??= new MySingleton();
        return _instance;
    }
}

The null-coalescing assignment operator ??= assigns the new instance of MySingleton only if the _instance member is null. That line is equivalent to writing the following if statement:

if (_instance == null)
{
    _instance = new MySingleton();
}

Before discussing the code more, let’s explore our new class’s behavior. We can see in the following unit test that MySingleton.Create() always returns the same instance as expected:

public class MySingletonTest
{
    [Fact]
    public void Create_should_always_return_the_same_instance()
    {
        var first = MySingleton.Create();
        var second = MySingleton.Create();
        Assert.Same(first, second);
    }
}

And voilà! We have a working Singleton pattern, which is extremely simple—probably the most simple design pattern that I can think of.Here is what is happening under the hood:

  1. The first time that a consumer calls MySingleton.Create(), it creates the first instance of MySingleton. Since the constructor is private, it can only be created from the inside.
  2. The Create method then persists that first instance to the _instance field for future use.
  3. When a consumer calls MySingleton.Create() a second time, it returns the _instance field, reusing the class’s previous (and only) instance.

Now that we understand the logic, there is a potential issue with that design: it is not thread-safe. If we want our singleton to be thread-safe, we can lock the instance creation like this:

public class MySingletonWithLock
{
    private static readonly object _myLock = new();
    private static MySingletonWithLock?
_instance;
    private MySingletonWithLock() { }
    public static MySingletonWithLock Create()
    {
        lock (_myLock)
        {
            _instance ??= new MySingletonWithLock();
        }
        return _instance;
    }
}

In the preceding code, we ensure two threads are not attempting to access the Create method simultaneously to ensure they are not getting different instances. Next, we improve our thread-safe example by making it shorter.

Project – The mid-range vehicle factory – Strategy, Abstract Factory, and Singleton Design Patterns

To prove the flexibility of our design based on the Abstract Factory pattern, let’s add a new concrete factory named MidRangeVehicleFactory. That factory should return a MidRangeCar or a MidRangeBike instance. Once again, the car and bike are just empty classes (of course, in your programs, they will do something):

public class MiddleGradeCar : ICar { }
public class MiddleGradeBike : IBike { }

The new MidRangeVehicleFactory looks pretty much the same as the other two:

public class MidRangeVehicleFactory : IVehicleFactory
{
    public IBike CreateBike() => new MiddleGradeBike();
    public ICar CreateCar() => new MiddleGradeCar();
}

Now, to test the mid-range factory, we declare the following test class:

namespace Vehicles.MidRange;
public class MidRangeVehicleFactoryTest : BaseAbstractFactoryTest<MidRangeVehicleFactory, MidRangeCar, MidRangeBike>
{
}

Like the low-end and high-end factories, the mid-range test class depends on the BaseAbstractFactoryTest class and specifies the types to test for (highlighted).If we run the tests, we now have the following six passing tests:

 Figure 7.5: Visual Studio Test Explorer showcasing the six passing tests.Figure 7.5: Visual Studio Test Explorer showcasing the six passing tests. 

So, without updating the consumer (the AbstractFactoryTest class), we added a new family of vehicles, the middle-end cars and bikes; kudos to the Abstract Factory pattern for that wonderfulness!

Impacts of the Abstract Factory

Before concluding, what would happen if we packed everything in a large interface instead of using an Abstract Factory (breaking the ISP along the way)? We could have created something like the following interface:

public interface ILargeVehicleFactory
{
    HighEndBike CreateHighEndBike();
    HighEndCar CreateHighEndCar();
    LowEndBike CreateLowEndBike();
    LowEndCar CreateLowEndCar();
}

As we can see, the preceding interface contains four specific methods and seems docile. However, the consumers of that code would be tightly coupled with those specific methods. For example, to change a consumer’s behavior, we’d need to update its code, like changing the call from CreateHighEndBike to CreateLowEndBike, which breaks the OCP. On the other hand, with the factory method, we can set a different factory for the consumers to spit out different results, which moves the flexibility out of the object itself and becomes a matter of composing the object graph instead (more on that in the next chapter).Moreover, when we want to add mid-range vehicles, we must update the ILargeVehicleFactory interface, which becomes a breaking change (the implementation(s) of the ILargeVehicleFactory must be updated). Here’s an example of the two new methods:

public interface ILargeVehicleFactory
{
    HighEndBike CreateHighEndBike();
    HighEndCar CreateHighEndCar();
    LowEndBike CreateLowEndBike();
    LowEndCar CreateLowEndCar();
    MidRangeBike CreateMidRangeBike();
    MidRangeCar CreateMidRangeCar();
}

From there, once the implementation(s) are updated, if we want to consume the new mid-range vehicles, we need to open each consumer class and apply the changes there, which once again breaks the OCP.

The most crucial part is understanding and seeing the coupling and its impacts. Sometimes, it’s okay to tightly couple one or more classes together as we don’t always need the added flexibility the SOLID principles and some design patterns can bring.

Now let’s conclude before exploring the last design pattern of the chapter.

Conclusion

The Abstract Factory pattern is excellent for abstracting away the creation of object families, isolating each family and its concrete implementation, leaving the consumers unaware of the family created at runtime by the factory.We talk more about factories in the next chapter; meanwhile, let’s see how the Abstract Factory pattern can help us follow the SOLID principles:

  • S: Each concrete factory is solely responsible for creating a family of objects. You could combine Abstract Factory with other creational patterns, such as the Prototype and Builder patterns for more complex creational needs.
  • O: We can create new families of objects, like the mid-range vehicles, without breaking existing client code.
  • L: We aim at composition, so there’s no need for any inheritance, implicitly discarding the need for the LSP. If you use abstract classes in your design, you must ensure you don’t break the LSP when creating new abstract factories.
  • I: Extracting a small abstraction with many implementations where each concrete factory focuses on one family makes that interface very focused on one task instead of having a large interface that exposes all types of products (like the ILargeVehicleFactory interface).
  • D: By depending only on interfaces, the consumer is unaware of the concrete types it uses.

Next, we explore the last design pattern of the chapter.

Project – Abstract Factory – Strategy, Abstract Factory, and Singleton Design Patterns

Context: We need to support the creation of multiple models of vehicles. We also need to be able to add new models as they become available without impacting the system. To begin with, we only support high-end and low-end models, but we know this will change sooner rather than later. The program must only support the creation of cars and bikes.For the sake of our demo, the vehicles are just empty classes and interfaces because learning how to model vehicles is not necessary to understand the pattern; that would be noise. The following code represents those entities:

public interface ICar { }
public interface IBike { }
public class LowEndCar : ICar { }
public class LowEndBike : IBike { }
public class HighEndCar : ICar { }
public class HighEndBike : IBike { }

Next, we look at the part that we want to study—the factories:

public interface IVehicleFactory
{
    ICar CreateCar();
    IBike CreateBike();
}
public class LowEndVehicleFactory : IVehicleFactory
{
    public IBike CreateBike() => new LowEndBike();
    public ICar CreateCar() => new LowEndCar();
}
public class HighEndVehicleFactory : IVehicleFactory
{
    public IBike CreateBike() => new HighEndBike();
    public ICar CreateCar() => new HighEndCar();
}

The factories are simple implementations that describe the pattern well:

  • LowEndVehicleFactory creates low-end models.
  • HighEndVehicleFactory creates high-end models.

The consumer of this code is an xUnit test project. Unit tests are often your first consumers, especially if you are doing test-driven development (TDD).To make the tests easier, I created the following base test class:

using Xunit;
namespace Vehicles;
public abstract class BaseAbstractFactoryTest<TConcreteFactory, TExpectedCar, TExpectedBike>
    where TConcreteFactory : IVehicleFactory, new()
{
    // Test methods here
}

The key to that class is the following generic parameters:

  • The TConcreteFactory parameter represents the type of concrete factory we want to test. Its generic constraint specifies that it must implement the IVehicleFactory interface and have a parameterless constructor.
  • The TExpectedCar parameter represents the type of ICar we expect from the CreateCar method.
  • The TExpectedBike parameter represents the type of IBike we expect from the CreateBike method.

The first test method contained by that class is the following:

[Fact]
public void Should_create_a_ICar_of_type_TExpectedCar()
{
    // Arrange
    IVehicleFactory vehicleFactory = new TConcreteFactory();
    var expectedCarType = typeof(TExpectedCar);
    // Act
    ICar result = vehicleFactory.CreateCar();
    // Assert
    Assert.IsType(expectedCarType, result);
}

The preceding test method creates a vehicle factory using the TConcreteFactory generic parameter, then creates a car using that factory. Finally, it asserts ICar instance is of the expected type.The second test method contains by that class is the following:

[Fact]
public void Should_create_a_IBike_of_type_TExpectedBike()
{
    // Arrange
    IVehicleFactory vehicleFactory = new TConcreteFactory();
    var expectedBikeType = typeof(TExpectedBike);
    // Act
    IBike result = vehicleFactory.CreateBike();
    // Assert
    Assert.IsType(expectedBikeType, result);
}

The preceding test method is very similar and creates a vehicle factory using the TConcreteFactory generic parameter but then creates a bike instead of a car using that factory. Finally, it asserts IBike instance is of the expected type.

I used the ICar and IBike interfaces to type the variables instead of var, to clarify the result variable type. In another context, I would have used var instead. The same applies to the IVehicleFactory interface.

Now, to test the low-end factory, we declare the following test class:

namespace Vehicles.LowEnd;
public class LowEndVehicleFactoryTest : BaseAbstractFactoryTest<LowEndVehicleFactory, LowEndCar, LowEndBike>
{
}

That class solely depends on the BaseAbstractFactoryTest class and specifies the types to test for (highlighted).Next, to test the high-end factory, we declare the following test class:

namespace Vehicles.HighEnd;
public class HighEndVehicleFactoryTest : BaseAbstractFactoryTest<HighEndVehicleFactory, HighEndCar, HighEndBike>
{
}

Like the low-end factory, that class depends on the BaseAbstractFactoryTest class and specifies the types to test for (highlighted).

In a more complex scenario where we can’t use the new() generic constraint, we can leverage an IoC container to create the instance of TConcreteFactory and optionally mock its dependencies.

With that test code, we created the following two sets of two tests:

  • A LowEndVehicleFactory class that should create a LowEndCar instance.
  • A LowEndVehicleFactory class that should create a LowEndBike instance.
  • A HighEndVehicleFactory class that should create a HighEndCar instance.
  • A HighEndVehicleFactory class that should create a HighEndBike instance.

We now have four tests: two for bikes and two for cars.If we review the tests’ execution, both test methods are unaware of types. They use the Abstract Factory (IVehicleFactory) and test the result against the expected type without knowing what they are testing but the abstraction. That shows how loosely coupled the consumers (tests) and the factories are.

We would use the ICar or the IBike instances in a real-world program to do something relevant based on the specifications. That could be a racing game or a rich person’s garage management system; who knows!

The important part of this project is the abstraction of the object creation process. The test code (consumer) is not aware of the implementations.Next, we extend our implementation.

Project – Strategy – Strategy, Abstract Factory, and Singleton Design Patterns-1

Context: We want to sort a collection differently, eventually even using different sort algorithms (out of the scope of the example but possible). Initially, we want to support sorting the elements of any collection in ascending or descending order.To achieve this, we need to implement the following building blocks:

  • The Context is the SortableCollection class.
  • The IStrategy is the ISortStrategy interface.
  • The concrete strategies are:
  • SortAscendingStrategy
  • SortDescendingStrategy

The consumer is a small REST API that allows the user to change the strategy, sort the collection, and display the items. Let’s start with the ISortStrategy interface:

public interface ISortStrategy
{
    IOrderedEnumerable<string> Sort(IEnumerable<string> input);
}

That interface contains only one method that expects a string collection as input and returns an ordered string collection. Now let’s inspect the two implementations:

public class SortAscendingStrategy : ISortStrategy
{
    public IOrderedEnumerable<string> Sort(IEnumerable<string> input)
        => input.OrderBy(x => x);
}
public class SortDescendingStrategy : ISortStrategy
{
    public IOrderedEnumerable<string> Sort(IEnumerable<string> input)
        => input.OrderByDescending(x => x);
}

Both implementations are super simple, using Language Integrated Query (LINQ) to sort the input and return the result directly.

Tip

When using expression-bodied methods, please ensure you do not make the method harder to read for your colleagues (or future you) by creating very complex one-liners. Writing multiple lines often makes the code easier to read.

The next building block to inspect is the SortableCollection class. It is composed of multiple string items (the Items property) and can sort them using an ISortStrategy. On top of that, it implements the IEnumerable<string> interface through its Items property, making it iterable. Here’s the class:

using System.Collections;
using System.Collections.Immutable;
namespace MySortingMachine;
public sealed class SortableCollection : IEnumerable<string>
{
    private ISortStrategy _sortStrategy;
    private ImmutableArray<string> _items;
    public IEnumerable<string> Items => _items;
    public SortableCollection(IEnumerable<string> items)
    {
        _items = items.ToImmutableArray();
        _sortStrategy = new SortAscendingStrategy();
    }
    public void SetSortStrategy(ISortStrategy strategy)
        => _sortStrategy = strategy;
    public void Sort()
    {
        _items = _sortStrategy
            .Sort(Items)
            .ToImmutableArray()
        ;
    }
    public IEnumerator<string> GetEnumerator()
        => Items.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator()
        => ((IEnumerable)Items).GetEnumerator();
}

The SortableCollection class is the most complex one so far, so let’s take a more in-depth look:

  • The _sortStrategy field references the algorithm: an ISortStrategy implementation.
  • The _items field references the strings themselves.
  • The Items property exposes the strings to the consumers of the class.
  • The constructor initializes the Items property using the items parameter and sets the default sorting strategy.
  • The SetSortStrategy method allows consumers to change the strategy at runtime.
  • The Sort method uses the _sortStrategy field to sort the items.
  • The two GetEnumerator methods represent the implementation of the IEnumerable<string> interface and make the class enumerable through the Items property.

With that code, we can see the Strategy pattern in action. The _sortStrategy field represents the current algorithm, respecting an ISortStrategy contract, which is updatable at runtime using the SetSortStrategy method. The Sort method delegates the work to the ISortStrategy implementation (the concrete strategy). Therefore, changing the value of the _sortStrategy field leads to a change of behavior of the Sort method, making this pattern very powerful yet simple. The highlighted code represents this pattern.