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

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 object creation using a few classic, simple, and yet powerful design patterns from the Gang of Four (GoF). These patterns allow developers to encapsulate and reuse behaviors, centralize object creation, add flexibility to our designs, or control object lifetime. Moreover, you will most likely use some of them in all software you build directly or indirectly in the future.

GoF

Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides are the authors of Design Patterns: Elements of Reusable Object-Oriented Software (1994), known as the Gang of Four (GoF). In that book, they introduce 23 design patterns, some of which we revisit in this book.

Why are they that important? Because they are the building blocks of robust object composition and help create flexibility and reliability. Moreover, in Chapter 8, Dependency Injection, we make those patterns even more powerful!But first things first. In this chapter, we cover the following topics:

  • The Strategy design pattern
  • The Abstract Factory design pattern
  • The Singleton design pattern

The Strategy design pattern

The Strategy pattern is a behavioral design pattern that allows us to change object behaviors at runtime. We can also use this pattern to compose complex object trees and rely on it to follow the Open/Closed Principle (OCP) without much effort. Moreover, it plays a significant role in the composition over inheritance way of thinking. In this chapter, we focus on the behavioral part of the Strategy pattern. The next chapter covers how to use the Strategy pattern to compose systems dynamically.

Goal

The Strategy pattern aims to extract an algorithm (a strategy) from the host class needing it (the context or consumer). That allows the consumer to decide on the strategy (algorithm) to use at runtime.For example, we could design a system that fetches data from two different types of databases. Then we could apply the same logic to that data and use the same user interface to display it. To achieve this, we could use the Strategy pattern to create two strategies, one named FetchDataFromSql and the other FetchDataFromCosmosDb. Then we could plug the strategy we need at runtime in the context class. That way, when the consumer calls the context, the context does not need to know where the data comes from, how it is fetched, or what strategy is in use; it only gets what it needs to work, delegating the fetching responsibility to an abstracted strategy (an interface).

Design

Before any further explanation, let’s take a look at the following class diagram:

 Figure 7.1: Strategy pattern class diagramFigure 7.1: Strategy pattern class diagram 

Based on the preceding diagram, the building blocks of the Strategy pattern are the following:

  • Context is a class that depends on the IStrategy interface and leverages an implementation of the IStrategy interface to execute the ExecuteAlgo method.
  • IStrategy is an interface defining the strategy API.
  • ConcreteStrategy1 and ConcreteStrategy2 represent one or more different concrete implementations of the IStrategy interface.

In the following diagram, we explore what happens at runtime. The actor represents any code consuming the Context object.

 Figure 7.2: Strategy pattern sequence diagramFigure 7.2: Strategy pattern sequence diagram 

When the consumer calls the Context.SomeOperation() method, it does not know which implementation is executed, which is an essential part of this pattern. The Context class should not be aware of the strategy it uses either. It should run the strategy through the interface, unaware of the implementation. That is the strength of the Strategy pattern: it abstracts the implementation away from both the Context class and its consumers. Because of that, we can change the strategy during either the object creation or at runtime without the object knowing, changing its behavior on the fly.

Note

We could even generalize that last sentence and extend it to any interface. Depending on an interface breaks the ties between the consumer and the implementation by relying on that abstraction instead.

Conclusion – Model-View-Controller

A data transfer object allows us to design an API endpoint with a specific data contract (input and output) instead of exposing the domain or data model. This separation between the presentation and the domain is a crucial element that leads to having multiple independent components instead of a bigger, more fragile one. Using DTOs to control the inputs and outputs gives us more control over what the clients can do or receive.Using the data transfer object pattern helps us follow the SOLID principles in the following ways:

  • S: A DTO adds clear boundaries between the domain model and the API contract. Moreover, having an input and an output DTO help further separate the responsibilities.
  • O: N/A
  • L: N/A
  • I: A DTO is a small, specifically crafted data contract (abstraction) with a clear purpose in the API contract.
  • D: Due to those smaller interfaces (ISP), DTOs allow changing the implementation details of the endpoint without affecting the clients because they depend only on the API contract (an abstraction).

You have learned DTOs’ added value, their role in an API contract, and the ASP.NET Core MVC framework.

Summary

This chapter explored the Model-View-Controller (MVC) design pattern, a well-established framework in the ASP.NET ecosystem that offers more advanced features than its newer Minimal APIs counterpart. Minimal APIs are not competing against MVC; we can use them together. The MVC pattern emphasizes the separation of concerns, making it a proven pattern for creating maintainable, scalable, and robust web applications. We broke down the MVC pattern into its three core components: Models, Views, and Controllers. Models represent data and business logic, Views are user-facing components (serialized data structures), and Controllers act as intermediaries, mediating the interaction between Models and Views. We also discussed using Data Transfer Objects (DTOs) to package data in the format we need, providing many benefits, including flexibility, efficiency, encapsulation, and improved performance. DTOs are a crucial part of the API contract.Now that we have explored principles and methodologies, it is time to continue our learning and tackle more design patterns and features. The following two chapters explore our first Gang of Four (GoF) design patterns and deep dive into ASP.NET Core dependency injection (DI). All of this will help us to continue on the path we started: to learn the tools to design better software.

Raw CRUD Controller – Model-View-Controller

Many issues can arise if we create a CRUD controller to manage the customers directly (see RawCustomersController.cs). First, a little mistake from the client could erase several data points. For example, if the client forgets to send the contracts during a PUT operation, that would delete all the contracts associated with that customer. Here’s the controller code:

// PUT raw/customers/1
[HttpPut(“{id}”)]
public async Task<ActionResult<Customer>> PutAsync(
    int id,
    [FromBody] Customer value,
    ICustomerRepository customerRepository)
{
    var customer = await customerRepository.UpdateAsync(
        value,
        HttpContext.RequestAborted);
    if (customer == null)
    {
        return NotFound();
    }
    return customer;
}

The highlighted code represents the customer update. So to mistakenly remove all contracts, a client could send the following HTTP request (from the MVC.API.http file):

PUT {{MVC.API.BaseAddress}}/customers/1
Content-Type: application/json
{
  “id”: 1,
  “name”: “Some new name”,
  “contracts”: []
}

That request would result in the following response entity:

{
  “id”: 1,
  “name”: “Some new name”,
  “contracts”: []
}

Previously, however, that customer had contracts (seeded when we started the application). Here’s the original data:

{
  “id”: 1,
  “name”: “Jonny Boy Inc.”,
  “contracts”: [
    {
      “id”: 1,
      “name”: “First contract”,
      “description”: “This is the first contract.”,
      “status”: {
        “totalWork”: 100,
        “workDone”: 100,
        “state”: “Completed”
      },
      “primaryContact”: {
        “firstName”: “John”,
        “lastName”: “Doe”,
        “email”: “[email protected]
      }
    },
    {
      “id”: 2,
      “name”: “Some other contract”,
      “description”: “This is another contract.”,
      “status”: {
        “totalWork”: 100,
        “workDone”: 25,
        “state”: “InProgress”
      },
      “primaryContact”: {
        “firstName”: “Jane”,
        “lastName”: “Doe”,
        “email”: “[email protected]
      }
    }
  ]
}

As we can see, by exposing our entities directly, we are giving a lot of power to the consumers of our API. Another issue with this design is the dashboard. The user interface would have to calculate the statistics about the contracts. Moreover, if we implement paging the contracts over time, the user interface could become increasingly complex and even overquery the database, hindering our performance.

I implemented the entire API, which is available on GitHub but without UI.

Next, we explore how we can fix those two use cases using DTOs.

Conclusion – Model-View-Controller

This section explored the MVC pattern, how to create controllers and action methods, and how to route requests to those actions.We could talk about MVC for the remainder of the book, but we would be missing the point. The subset of features we covered here should be enough theory to fill the gap you might have had and allow you to understand the code samples that leverage ASP.NET Core MVC.Using the MVC pattern helps us follow the SOLID principles in the following ways:

  • S: The MVC pattern divides the rendering of a data structure into three different roles. The framework handles most of the serialization portion (the View), leaving us only two pieces to manage: the Model and the Controller.
  • O: N/A
  • L: N/A
  • I: Each controller handles a subset of features and represents a smaller interface into the system. MVC makes the system easier to manage than having a single entry point for all routes, like a single controller.
  • D: N/A

Next, we explore the Data Transfer Object pattern to isolate the API’s model from the domain.

Using MVC with DTOs

This section explores leveraging the Data Transfer Object (DTO) pattern with the MVC framework.

This section is the same as we explore in Chapter 5, Minimal APIs, but in the context of MVC. Moreover, the two code projects are part of the same Visual Studio solution for convenience, allowing you to compare the two implementations.

Goal

As a reminder, DTOs aim to control the inputs and outputs of an endpoint by decoupling the API contract from the application’s inner workings. DTOs empower us to define our APIs without thinking about the underlying data structures, leaving us to craft our REST APIs how we want.

We discuss REST APIs and DTOs more in-depth in Chapter 4, REST APIs.

Other possible objectives are to save bandwidth by limiting the amount of information the API transmits, flattening the data structure, or adding API features that cross multiple entities.

Design

Let’s start by analyzing a diagram that expands MVC to work with DTOs:

 Figure 6.2: MVC workflow with a DTOFigure 6.2: MVC workflow with a DTO 

DTOs allow the decoupling of the domain from the view (data) and empower us to manage the inputs and outputs of our REST APIs independently from the domain. The controller still manipulates the domain model but returns a serialized DTO instead.

Project – MVC API

This code sample is the same as in the previous chapter but uses the MVC framework instead of Minimal APIs.Context: we must build an application to manage customers and contracts. We must track the state of each contract and have a primary contact in case the business needs to contact the customer. Finally, we must display the number of contracts and the number of opened contracts for each customer on a dashboard.As a reminder, the model is the following:

namespace Shared.Models;
public record class Customer(
    int Id,
    string Name,
    List<Contract> Contracts
);
public record class Contract(
    int Id,
    string Name,
    string Description,
    WorkStatus Status,
    ContactInformation PrimaryContact
);
public record class WorkStatus(int TotalWork, int WorkDone)
{
    public WorkState State =>
        WorkDone == 0 ?
WorkState.New :
        WorkDone == TotalWork ?
WorkState.Completed :
        WorkState.InProgress;
}
public record class ContactInformation(
    string FirstName,
    string LastName,
    string Email
);
public enum WorkState
{
    New,
    InProgress,
    Completed
}

The preceding code is straightforward. The only piece of logic is the WorkStatus.State property that returns WorkState.New when the work has not yet started on that contract, WorkState.Completed when all the work is completed, or WorkState.InProgress otherwise.The controllers leverage the ICustomerRepository interface to simulate database operations. The implementation is unimportant. It uses a List<Customer> as the database. Here’s the interface that allows querying and updating the data:

using Shared.Models;
namespace Shared.Data;
public interface ICustomerRepository
{
    Task<IEnumerable<Customer>> AllAsync(
        CancellationToken cancellationToken);
    Task<Customer> CreateAsync(
        Customer customer,
        CancellationToken cancellationToken);
    Task<Customer?> DeleteAsync(
        int customerId,
        CancellationToken cancellationToken);
    Task<Customer?> FindAsync(
        int customerId,
        CancellationToken cancellationToken);
    Task<Customer?> UpdateAsync(
        Customer customer,
        CancellationToken cancellationToken);
}

Now that we know about the underlying foundation, we explore a CRUD controller that does not leverage DTOs.

Attribute routing – Model-View-Controller

Attribute routing maps an HTTP request to a controller action. Those attributes decorate the controllers and the actions to create the complete routes. We already used some of those attributes. Nonetheless, let’s visit those attributes:

namespace MVC.API.Controllers.Empty;
[Route(“empty/[controller]”)]
[ApiController]
public class CustomersController : ControllerBase
{
    [HttpGet]
    public Task<IEnumerable<Customer>> GetAllAsync(
        ICustomerRepository customerRepository)
        => throw new NotImplementedException();
    [HttpGet(“{id}”)]
    public Task<ActionResult<Customer>> GetOneAsync(
        int id, ICustomerRepository customerRepository)
        => throw new NotImplementedException();
    [HttpPost]
    public Task<ActionResult> PostAsync(
        [FromBody] Customer value, ICustomerRepository customerRepository)
        => throw new NotImplementedException();
    [HttpPut(“{id}”)]
    public Task<ActionResult<Customer>> PutAsync(
        int id, [FromBody] Customer value,
        ICustomerRepository customerRepository)
        => throw new NotImplementedException();
    [HttpDelete(“{id}”)]
    public Task<ActionResult<Customer>> DeleteAsync(
        int id, ICustomerRepository customerRepository)
        => throw new NotImplementedException();
}

The Route attributes and Http[Method] attributes define what a user should query to reach a specific resource. The Route attribute allows us to define a routing pattern that applies to all HTTP methods under the decorated controller. The Http[Method] attributes determine the HTTP method used to reach that action method. They also offer the possibility to set an optional and additive route pattern to handle more complex routes, including specifying route parameters. Those attributes are beneficial in crafting concise and clear URLs while keeping the routing system close to the controller. All routes must be unique.Based on the code, [Route(“empty/[controller]”)] means that the actions of this controller are reachable through empty/customers (MVC ignores the Controller suffix). Then, the other attributes tell ASP.NET to map specific requests to specific methods:

Routing AttributeHTTP MethodURL
HttpGetGETempty/customers
HttpGet(“{id}”)GETempty/customers/{id}
HttpPostPOSTempty/customers
HttpPut(“{id}”)PUTempty/customers/{id}
HttpDelete(“{id}”)DELETEempty/customers/{id}

 Table 6.3: routing attributes of the example controller and their final URL

As we can see from the preceding table, we can even use the same attribute for multiple actions as long as the URL is unique. In this case, the id parameter is the GET discriminator.Next, we can use the FromBody attribute to tell the model binder to use the HTTP request body to get the value of that parameter. There are many of those attributes; here’s a list:

AttributeDescription
FromBodyBinds the JSON body of the request to the parameter’s type.
FromFormBinds the form value that matches the name of the parameter.
FromHeaderBinds the HTTP header value that matches the name of the parameter.
FromQueryBinds the query string value that matches the name of the parameter.
FromRouteBinds the route value that matches the name of the parameter.
FromServicesInject the service from the ASP.NET Core dependency container.

 Table 6.4: MVC binding sources

ASP.NET Core MVC does many implicit binding, so you don’t always need to decorate all parameters with an attribute. For example, .NET injects the services we needed in the code samples, and we never used the FromServices attribute. Same with the FromRoute attribute.

Now, if we look back at CustomersController, the route map looks like the following (I excluded non-route-related code to improve readability):

URLAction/Method
GET empty/customersGetAllAsync()
GET empty/customers/{id}GetOneAsync(int id)
POST empty/customersPostAsync([FromBody] Customer value)
PUT empty/customers/{id}PutAsync(int id, [FromBody] Customer value)
DELETE empty/customers/{id}DeleteAsync(int id)

 Table 6.5: the map between the URLs and their respective action methods

When designing a REST API, the URL leading to our endpoints should be clear and concise, making it easy for consumers to discover and learn. Hierarchically grouping our resources by responsibility (concern) and creating a cohesive URL space help achieve that goal. Consumers (a.k.a. other developers) should understand the logic behind the endpoints easily. Think about your endpoints as if you were the consumer of the REST API. I would even extend that suggestion to any API; always consider the consumers of your code to create the best possible APIs.

The Model View Controller design pattern – Model-View-Controller

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 delves into the Model-View-Controller (MVC) design pattern, a cornerstone of modern software architecture that intuitively structures your code around entities. MVC is perfect for CRUD operations or to tap into the advanced features unavailable in Minimal APIs. The MVC pattern partitions your application into three interrelated parts: Models, Views, and Controllers.

  • Models, which represent our data and business logic.
  • Views, which are the user-facing components.
  • Controllers, that act as intermediaries, mediating the interaction between Models and Views.

With its emphasis on the separation of concerns, the MVC pattern is a proven pattern for creating scalable and robust web applications. In the context of ASP.NET Core, MVC has provided a practical approach to building applications efficiently for years. While we discussed REST APIs in Chapter 4, this chapter provides insight into how to use MVC to create REST APIs. We also address using Data Transfer Objects (DTOs) within this framework.In this chapter, we cover the following topics:

  • The Model-View-Controller design pattern
  • Using MVC with DTOs

Our ultimate goal is clean, maintainable, and scalable code; the ASP.NET Core MVC framework is a favored tool for achieving this. Let’s dive in!

The Model View Controller design pattern

Now that we have explored the basics of REST and Minimal APIs, it is time to explore the MVC pattern to build ASP.NET Core REST APIs.Model-View-Controller (MVC) is a design pattern commonly used in web development. It has a long history of building REST APIs in ASP.NET and is widely used and praised by many.This pattern divides an application into three interconnected components: the Model, the View, and the Controller. A View in MVC formerly represented a user interface. However, in our case, the View is a data contract that reflects the REST API’s data-oriented nature.

Dividing responsibilities this way aligns with the Single Responsibility Principle (SRP) explored in Chapter 3, Architectural Principles. However, this is not the only way to build REST APIs with ASP.NET Core, as we saw in Chapter 5, Minimal APIs.

The new minimal API model mixed with the Request-EndPoint-Response (REPR) pattern can make building REST APIs leaner. We cover that pattern in Chapter 18, Request-EndPoint-Response (REPR). We could see REPR as what ASP.NET Core Razor Pages are to page-oriented web applications, but for REST APIs.

We often design MVC applications around entities, and each entity has a controller that orchestrates its endpoints. We called those CRUD controllers. However, you can design your controller to fit your needs.In the past few decades, the number of REST APIs just exploded to a gazillion; everybody builds APIs nowadays, not because people follow the trend but based on good reasons. REST APIs have fundamentally transformed how systems communicate, offering various benefits that make them indispensable in modern software architecture. Here are a few key factors that contribute to their widespread appeal:

  • Data Efficiency: REST APIs promote efficient data sharing across different systems, fostering seamless interconnectivity.
  • Universal Communication: REST APIs leverage universally recognized data formats like JSON or XML, ensuring broad compatibility and interoperability.
  • Backend Centralization: REST APIs enable the backend to serve as a centralized hub, supporting multiple frontend platforms, including mobile, desktop, and web applications.
  • Layered Backends: REST APIs facilitate the stratification of backends, allowing for the creation of foundational, low-level APIs that provide basic functionalities. These, in turn, can be consumed by higher-level, product-centric APIs that offer specialized capabilities, thus promoting a flexible and modular backend architecture.
  • Security Measures: REST APIs can function as gateways, providing security measures to protect downstream systems and ensuring data access is appropriately regulated—a good example of layering APIs.
  • Encapsulation: REST APIs allow for the encapsulation of specific units of logic into reusable, independent modules, often leading to cleaner, more maintainable code.
  • Scalability: due to their stateless nature, REST APIs are easier to scale up to accommodate increasing loads.

These advantages greatly facilitate the reuse of backend systems across various user interfaces or even other backend services. Consider, for instance, a typical mobile application that needs to support iOS, Android, and web platforms. By utilizing a shared backend through REST APIs, development teams can streamline their efforts, saving significant time and cost. This shared backend approach ensures consistency across platforms while reducing the complexity of maintaining multiple codebases.

We explore different such patterns in Chapter 19, Introduction to Microservices Architecture.

Raw CRUD endpoints – Minimal API

Many issues can arise if we create CRUD endpoints to manage the customers directly (see CustomersEndpoints.cs). First, a little mistake from the client could erase several data points. For example, if the client forgets to send the contracts during a PUT operation, that would delete all the contracts associated with that customer. Here’s the controller code:// PUT raw/customers/1
group.MapPut(“/{customerId}”, async (int customerId, Customer input, ICustomerRepository customerRepository, CancellationToken cancellationToken) =>

The highlighted code represents the customer update. So to mistakenly remove all contracts, a client could send the following HTTP request (from the Minimal.API.http file):PUT {{Minimal.API.BaseAddress}}/customers/1
Content-Type: application/json

That request would result in the following response entity:

{
  “id”: 1,
  “name”: “Some new
name”,
  “contracts”: []

Previously, however, that customer had contracts (seeded when we started the application). Here’s the original data:{
  “id”: 1,
  “name”: “Jonny Boy Inc.”,

As we can see, by exposing our entities directly, we are giving a lot of power to the consumers of our API. Another issue with this design is the dashboard. The user interface would have to calculate the statistics about the contracts. Moreover, if we implement paging the contracts over time, the user interface could become increasingly complex and even overquery the database, hindering our performance.

I implemented the entire API, which is available on GitHub but without UI.

Next, we explore how we can fix those two use cases using DTOs.

DTO-enabled endpoints

To solve our problems, we reimplement the endpoints using DTOs. These endpoints use methods instead of inline delegates and returns Results<T1, T2, …> instead IResult. So, let’s start with the declaration of the endpoints:

var group = routes
    .MapGroup(“/dto/customers”)
    .WithTags(“Customer DTO”)
    .WithOpenApi()
;
group.MapGet(“/”, GetCustomersSummaryAsync)
    .WithName(“GetAllCustomersSummary”);
group.MapGet(“/{customerId}”, GetCustomerDetailsAsync)
    .WithName(“GetCustomerDetailsById”);
group.MapPut(“/{customerId}”, UpdateCustomerAsync)
    .WithName(“UpdateCustomerWithDto”);
group.MapPost(“/”, CreateCustomerAsync)
    .WithName(“CreateCustomerWithDto”);
group.MapDelete(“/{customerId}”, DeleteCustomerAsync)
    .WithName(“DeleteCustomerWithDto”);

Next, 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 DTOEndpoints.cs file):// PUT dto/customers/1
private static async Task<Results<
    Ok<CustomerDetails>,

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 when the operation succeeds.

However, we can see more code in our endpoint than before. That’s because it now handles the 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”,

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 endpoint executes the calculations and copies the entity’s relevant properties to the DTO. Here’s the code:// GET: dto/customers
private static async Task<Ok<IEnumerable<CustomerSummary>>> GetCustomersSummaryAsync(
    ICustomerRepository customerRepository,
    CancellationToken cancellationToken)

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

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 endpoints become too complex, it is good practice to encapsulate them into other classes. We explore many techniques to organize application code in Section 4: Applications patterns.

Using Minimal APIs with Data Transfer Objects – Minimal API

This section explores leveraging the Data Transfer Object (DTO) pattern with minimal APIs.

This section is the same as we explore in Chapter 6, MVC, but in the context of Minimal APIs. Moreover, the two code projects are part of the same Visual Studio solution for convenience, allowing you to compare the two implementations.

Goal

As a reminder, DTOs aim to control the inputs and outputs of an endpoint by decoupling the API contract from the application’s inner workings. DTOs empower us to define our APIs without thinking about the underlying data structures, leaving us to craft our REST APIs how we want.

We discuss REST APIs and DTOs more in-depth in Chapter 4, REST APIs.

Other possible objectives are to save bandwidth by limiting the amount of information the API transmits, flattening the data structure, or adding API features that cross multiple entities.

Design

Let’s start by analyzing a diagram that shows how minimal APIs work with DTOs:

 Figure 5.4: An input DTO hitting some domain logic, then the endpoint returning an output DTOFigure 5.4: An input DTO hitting some domain logic, then the endpoint returning an output DTO 

DTOs allow the decoupling of the domain (3) from the request (1) and the response (5). This model empowers us to manage the inputs and outputs of our REST APIs independently from the domain. Here’s the flow:

  1. The client sends a request to the server.
  2. ASP.NET Core leverages its data binding and parsing mechanism to convert the information of the HTTP request to C# (input DTO).
  3. The endpoint does what it is supposed to do.
  4. ASP.NET Core serializes the output DTO to the HTTP response.
  5. The client receives and handles the response.

Let’s explore some code to understand the concept better.

Project – Minimal API

This code sample is the same as the next chapter but uses Minimal APIs instead of the MVC framework.Context: we must build an application to manage customers and contracts. We must track the state of each contract and have a primary contact in case the business needs to contact the customer. Finally, we must display the number of contracts and the number of opened contracts for each customer on a dashboard.The model is the following:

namespace Shared.Models;
public record class Customer(
    int Id,
    string Name,
    List<Contract> Contracts
);
public record class Contract(
    int Id,
    string Name,
    string Description,
    WorkStatus Status,
    ContactInformation PrimaryContact
);
public record class WorkStatus(int TotalWork, int WorkDone)
{
    public WorkState State =>
        WorkDone == 0 ?
WorkState.New :
        WorkDone == TotalWork ?
WorkState.Completed :
        WorkState.InProgress;
}
public record class ContactInformation(
    string FirstName,
    string LastName,
    string Email
);
public enum WorkState
{
    New,
    InProgress,
    Completed
}

The preceding code is straightforward. The only piece of logic is the WorkStatus.State property that returns WorkState.New when the work has not yet started on that contract, WorkState.Completed when all the work is completed, or WorkState.InProgress otherwise.The endpoints (CustomersEndpoints.cs) leverage the ICustomerRepository interface to simulate database operations. The implementation is unimportant. It uses a List<Customer> as the database. Here’s the interface that allows querying and updating the data:

using Shared.Models;
namespace Shared.Data;
public interface ICustomerRepository
{
    Task<IEnumerable<Customer>> AllAsync(
        CancellationToken cancellationToken);
    Task<Customer> CreateAsync(
        Customer customer,
        CancellationToken cancellationToken);
    Task<Customer?> DeleteAsync(
        int customerId,
        CancellationToken cancellationToken);
    Task<Customer?> FindAsync(
        int customerId,
        CancellationToken cancellationToken);
    Task<Customer?> UpdateAsync(
        Customer customer,
        CancellationToken cancellationToken);
}

Now that we know about the underlying foundation, we explore CRUD endpoints that do not leverage DTOs.