Category Directory structure

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.

Registering our dependencies – Dependency Injection

In ASP.NET Core, we register our dependencies in the Program.cs file, which represents the composition root. Since the minimal hosting model, the WebApplicationBuilder exposes the Services property we use to add our dependencies to the container. Afterward, .NET creates the container when it builds the WebApplication instance.Next is a minimal Program.cs file depicting this concept:

var builder = WebApplication.CreateBuilder(args);
// Register dependencies
var app = builder.Build();
// The IoC container is now available
app.Run();

Then, we use the builder.Services property to register our dependencies in that IServiceCollection implementation. Here’s an example of registering some dependencies:

builder.Services.AddSingleton<Dependency1>();
builder.Services.AddSingleton<Dependency2>();
builder.Services.AddSingleton<Dependency3>();

The preceding code registers the dependencies using the singleton lifetime, so we get the same instance each time we request one.

Remember to compose the program in the composition root. That removes the need for those pesky new keywords spread around your code base and all the tight coupling that come with them. Moreover, it centralizes the application’s composition into that location, creating the plan to assemble the LEGO® blocks.

As you may be thinking right now, that can lead to a lot of registration statements in a single location, and you are correct; maintaining such a composition root would be a challenge in almost any application. To address this concern, we introduce an elegant way to encapsulate the registration code next, ensuring it remains manageable.

Registering your features elegantly

As we’ve just discovered, while we should register dependencies in the composition root, we can also arrange our registration code in a structured manner. For example, we can break down our application’s composition into several methods or classes and invoke them from our composition root. Another strategy could be to use an auto-discovery system to automate the registration of certain services.

The critical part is to centralize the program composition in one place.

A common pattern in ASP.NET Core is having special methods like Add[Feature name]. These methods register their dependencies, letting us add a group of dependencies with just one method call. This pattern is convenient for breaking down program composition into smaller, easier-to-handle parts, like individual features. This also makes the composition root more readable.

A feature is the correct size as long as it stays cohesive. If your feature becomes too big, does too many things, or starts to share dependencies with other features, it may be time for a redesign before losing control over it. That’s usually a good indicator of undesired coupling.

To implement this pattern, we use extension methods, making it trivial. Here’s a guide:

  1. Create a static class named [subject]Extensions in the Microsoft.Extensions.DependencyInjection namespace.
  2. Create an extension method that returns the IServiceCollection interface, which allows method calls to be chained.

According to Microsoft’s recommendation, we should create the class in the same namespace as the element we extend. In our case, the IServiceCollection interface lives in the Microsoft.Extensions.DependencyInjection namespace.

Of course, this is not mandatory, and we can adapt this process to our needs. For example, we can define the class in another namespace if we want consumers to add a using statement implicitly. We can also return something else when the registration process can continue beyond that first method, like a builder interface.

Builder interfaces are used to configure more complex features, like ASP.NET Core MVC. For example, the AddControllers extension method returns an IMvcBuilder interface that contains a PartManager property. Moreover, some extension methods target the IMvcBuilder interface, allowing further configuration of the feature by requiring its registration first; that is, you can’t configure IMvcBuilder before calling AddControllers. You can also design your features to leverage that pattern when needed.

Let’s explore a demo.

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.

An alternate (better) way – Strategy, Abstract Factory, and Singleton Design Patterns

Previously, we used the “long way” of implementing the Singleton pattern and had to implement a thread-safe mechanism. Now that classic is behind us. We can shorten that code and even get rid of the Create() method like this:

public class MySimpleSingleton
{
    public static MySimpleSingleton Instance { get; } = new MySimpleSingleton();
    private MySimpleSingleton() { }
}

The preceding code relies on the static initializer to ensure that only one instance of the MySimpleSingleton class is created and assigned to the Instance property.

This simple technique should do the trick unless the singleton’s constructor executes some heavy processing.

With the property instead of a method, we can use the singleton class like this:

MySimpleSingleton.Instance.SomeOperation();

We can prove the correctness of that claim by executing the following test method:

[Fact]
public void Create_should_always_return_the_same_instance()
{
    var first = MySimpleSingleton.Instance;
    var second = MySimpleSingleton.Instance;
    Assert.Same(first, second);
}

It is usually best to delegate responsibilities to the language or the framework whenever possible like we did here with the property initializer. Using a static constructor would also be a valid, thread-safe alternative, once again delegating the job to language features.

Beware of the arrow operator.

It may be tempting to use the arrow operator => to initialize the Instance property like this: public static MySimpleSingleton Instance => new MySimpleSingleton();, but doing so would return a new instance every time. This would defeat the purpose of what we want to achieve. On the other hand, the property initializer runs only once.

The arrow operator makes the Instance property an expression-bodied member, equivalent to creating the following getter: get { return new MySimpleSingleton(); }. You can consult Appendix A for more information about expression-bodies statements.

Before we conclude the chapter, the Singleton (anti-)pattern also leads to a code smell.

Code smell – Ambient Context

That last implementation of the Singleton pattern led us to the Ambient Context pattern. We could even call the Ambient Context an anti-pattern, but let’s just state that it is a consequential code smell.I do not recommend using ambient contexts for multiple reasons. First, I do my best to avoid anything global; an ambient context is a global state. Globals, like static members in C#, can look very convenient because they are easy to access and use. They are always there and accessible whenever needed: easy. However, they bring many drawbacks in terms of flexibility and testability.When using an ambient context, the following occurs:

  • Tight coupling: global states lead to less flexible systems; consumers are tightly coupled with the ambient context.
  • Testing difficulty: global objects are harder to replace, and we cannot easily swap them for other objects, like a mock.
  • Unforseen impacts: if some part of your system messes up your global state, that may have unexpected consequences on other parts of your system, and you may have difficulty finding out the root cause of those errors.
  • Potential misuse: developers could be tempted to add non-global concerns to the ambient context, leading to a bloated component.

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-2

The _items field is an ImmutableArray<string>, which makes changing the list impossible from the outside. For example, a consumer cannot pass a List<string> to the constructor, then change it later. Immutability has many advantages.

Let’s experiment with this by looking at the Consumer.API project a REST API application that uses the previous code. Next is a breakdown of the Program.cs file:

using MySortingMachine;
SortableCollection data = new(new[] {
    “Lorem”, “ipsum”, “dolor”, “sit”, “amet.”
});

The data member is the context, our sortable collection of items. Next, we look at some boilerplate code to create the application and serialize enum values as strings:

var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.Converters
        .Add(new JsonStringEnumConverter());
});
var app = builder.Build();

Finally, the last part represents the consumer of the context:

app.MapGet(“/”, () => data);
app.MapPut(“/”, (ReplaceSortStrategy sortStrategy) =>
{
    ISortStrategy strategy = sortStrategy.SortOrder == SortOrder.Ascending
        ?
new SortAscendingStrategy()
        : new SortDescendingStrategy();
    data.SetSortStrategy(strategy);
    data.Sort();
    return data;
});
app.Run();
public enum SortOrder
{
    Ascending,
    Descending
}
public record class ReplaceSortStrategy(SortOrder SortOrder);

In the preceding code, we declared the following endpoints:

  • The first endpoint returns the data object when a client sends a GET request.
  • The second endpoint allows changing the sort strategy based on the SortOrder enum when a client sends a PUT request. Once the strategy is modified, it sorts the collection and returns the sorted data.

The highlighted code represents the consumption of this implementation of the strategy pattern.

The ReplaceSortStrategy class is an input DTO. Combined with the SortOrder enum, they represent the data contract of the second endpoint.

When we run the API and request the first endpoint, it responds with the following JSON body:

[
  “Lorem”,
  “ipsum”,
  “dolor”,
  “sit”,
  “amet.”
]

As we can see, the items are in the order we set them because the code never called the Sort method. Next, let’s send the following HTTP request to the API to change the sort strategy to “descending”:

PUT https://localhost:7280/
Content-Type: application/json
{
    “sortOrder”: “Descending”
}

After the execution, the endpoint responds with the following JSON data:

[
  “sit”,
  “Lorem”,
  “ipsum”,
  “dolor”,
  “amet.”
]

As we can see from the content, the sorting algorithm worked. Afterward, the list will remain in the same order if we query the GET endpoint. Next, let’s look at this use case using a sequence diagram:

 Figure 7.3: Sequence diagram sorting the items using the “sort descending strategy”Figure 7.3: Sequence diagram sorting the items using the “sort descending strategy” 

The preceding diagram shows the Program creating a strategy and assigning it to SortableCollection using its SetSortStrategy method. Then, when the Program calls the Sort() method, the SortableCollection instance delegates the sorting computation to the underlying implementation of the ISortStrategy interface. That implementation is the SortDescendingStrategy class (the strategy) which was set by the Program at the beginning.

Sending another PUT request but specifying the Ascending sort order end up in a similar result, but the items would be sorted alphabetically.

The HTTP requests are available in the Consumer.API.http file.

From a strategy pattern perspective, the SortableCollection class (the context) is responsible for referencing and using the current strategy.