Category Striving for adaptability

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.

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.

Directory structure – Model-View-Controller-2

The advantage of using a helper method is leveraging the ASP.NET Core MVC mechanism, making our life easier. However, you could manually manage the HTTP response using lower-level APIs like HttpContext or create custom classes that implement the IActionResult interface to hook your custom response classes into the MVC pipeline.Now let’s look at the multiple ways we can use to return data to the client:

Return typeDescription
voidWe can return void and manually manage the HTTP response using the HttpContext class. This is the most low-level and complex way.
TModelWe can directly return the model, which ASP.NET Core will serialize. The problem with this approach is that we don’t control the status code, nor can we return multiple different results from the action.
ActionResult IActionResultWe can return one of those two abstractions. The concrete result can take many forms depending on the implementation that the action method returns. However, doing this makes our API less auto-discoverable by tools like SwaggerGen.
ActionResult<TModel>We can return the TModel directly and other results like a NotFoundResult or a BadRequestResult. This is the most flexible way that makes the API the most discoverable by the ApiExplorer.

 Table 6.2: multiple ways to return data

We start with an example where the actions return an instance of the Model class by leveraging the Ok method (highlighted code):

using Microsoft.AspNetCore.Mvc;
namespace MVC.API.Controllers;
[Route(“api/[controller]”)]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet(“IActionResult”)]
    public IActionResult InterfaceAction()
        => Ok(new Model(nameof(InterfaceAction)));
    [HttpGet(“ActionResult”)]
    public ActionResult ClassAction()
        => Ok(new Model(nameof(ClassAction)));
    // …
   
public record class Model(string Name);
}

The problem with the preceding code is API discoverability. The ApiExplorer can’t know what the endpoints return. The ApiExplorer describes the actions as returning 200 OK but doesn’t know about the Model class.To overcome this limitation, we can decorate our actions with the ProducesResponseType attribute, effectively circumventing the limitation as shown below:

[ProducesResponseType(typeof(Model), StatusCodes.Status200OK)]
public IActionResult InterfaceAction() { …
}

In the preceding code, we specify the return type as the first argument and the status code as the second. Using the constants of the StatusCodes class is a convenient way to reference standard status codes. We can decorate each action with multiple ProducesResponseType attributes to define alternate states, such as 404 and 400.

With ASP.NET Core MVC, we can also define conventions that apply broad rules to our controllers, allowing us to define those conventions once and reuse them throughout our application. I left a link in the Further reading section.

Next, We explore how we can return a Model instance directly. The ApiExplorer can discover the return value of the method this way, so we do not need to use the ProducesResponseType attribute:

[HttpGet(“DirectModel”)]
public Model DirectModel()
    => new Model(nameof(DirectModel));

Next, thanks to class conversion operators (see Appendix A for more info), we can do the same with ActionResult<T>, like this:

[HttpGet(“ActionResultT”)]
public ActionResult<Model> ActionResultT()
    => new Model(nameof(ActionResultT));

The main benefit of using ActionResult<T> is to return other types of results. Here is an example showing this where the method returns either Ok or NotFound:

[HttpGet(“MultipleResults”)]
public ActionResult<Model> MultipleResults()
{
    var condition = Random.Shared
        .GetItems(new[] { true, false }, 1)
        .First();
    return condition
        ?
Ok(new Model(nameof(MultipleResults)))
        : NotFound();
}

However, the ApiExplorer does not know about the 404 Not Found, so we must document it using the ProducesResponseType attribute.

We can return a Task<T> or a ValueTask<T> from the action method when the method body is asynchronous. Doing so lets you write the async/await code from the controller.

I highly recommend returning a Task<T> or a ValueTask<T> whenever possible because it allows your REST API to handle more requests using the same resources without effort. Nowadays, non-Task-based methods in libraries are infrequent, so you will most likely have little choice.

We learned multiple ways to return values from an action. The ActionResult<T> class is the most flexible regarding feature support. On the other hand, IActionResult is the most abstract one.Next, we look at routing requests to those action methods.

Directory structure – Model-View-Controller-1

The default directory structure contains a Controllers folder to host the controllers. On top of that, we can create a Models folder to store your model classes or use any other structure.

While controllers are typically housed in the Controllers directory for organizational purposes, this convention is more for the benefit of developers than a strict requirement. ASP.NET Core is indifferent to the file’s location, offering us the flexibility to structure our project as we see fit.

Section 4, Applications Patterns, explores many ways of designing applications.

Next, we look at the central part of this pattern—the controllers.

Controller

The easiest way to create a controller is to create a class inheriting from ControllerBase. However, while ControllerBase adds many utility methods, the only requirement is to decorate the controller class with the [ApiController] attribute.

By convention, we write the controller’s name in its plural form and suffix it with Controller. For example, if the controller relates to the Employee entity, we’d name it EmployeesController, which, by default, leads to an excellent URL pattern that is easy to understand:

  • Get all employees: /employees
  • Get a specific employee: /employees/{id}
  • And so on.

Once we have a controller class, we must add actions. Actions are public methods that represent the operations that a client can perform. Each action represents an HTTP endpoint.More precisely, the following defines a controller:

  • A controller exposes one or more actions.
  • An action can take zero or more input parameters.
  • An action can return zero or one output value.
  • The action is what handles the HTTP request.

We should group cohesive actions under the same controller, thus creating a loosely coupled unit.

For example, the following represents the SomeController class containing a single Get action:

[Route(“api/[controller]”)]
[ApiController]
public class SomeController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Ok();
}

The preceding Get method (action) returns an empty 200 OK response to the client. We can reach the endpoint at the /api/some URI. From there, we can add more actions.

The ControllerBase class gives us access to most of the same utility methods as we had with the Minimal APIs TypedResults class.

Next, we look at returning value.

Returning values

Building a REST API aims to return data to clients and execute remote operations. Most of the plumbing is done for us by the ASP.NET Core code, including serialization.

Most of the ASP.NET Core pipeline is customizable, which is out of the scope of this chapter.

Before returning values, let’s look at a few valuable helper methods provided by the ControllerBase class:

MethodDescription
StatusCodeProduces an empty response with the specified status code. We can optionally include a second argument to serialize in the response body.
OkProduces a 200 OK response, indicating the operation was successful. We can optionally include a second argument to serialize in the response body.
CreatedProduces a 201 Created response, indicating the system created the entity. We can optionally specify the location where to read the entity and the entity itself as arguments. The CreatedAtAction and CreatedAtRoute methods give us options to compose the location value.
NoContentProduces an empty 204 No Content response.
NotFoundProduces a 404 Not Found response, indicating the resource was not found.
BadRequestProduces a 400 Bad Request response, indicating an issue with the client request, often a validation error.
RedirectProduces a 302 Found response, accepting the Location URL as an argument. Different Redirect* methods produce 301 Moved Permanently, 307 Temporary Redirect, and 308 Permanent Redirect responses instead.
AcceptedProduces a 202 Accepted response, indicating the beginning of an asynchronous process. We can optionally specify the location the client can query to learn about the status of the asynchronous operation. We can also optionally specify an object to serialize in the response body. The AcceptedAtAction and AcceptedAtRoute methods give us options to compose the location value.
ConflictProduces a 409 Conflict response, indicating a conflict occurred when processing the request, often a concurrency error.

 Table 6.1: a subset of the ControllerBase methods producing an IActionResult.

Other methods in the ControllerBase class are self-discoverable using IntelliSense (code completion) or in the official documentation. Most, if not all, of what we covered in Chapter 5, Minimal APIs, is also available to controllers.

Conclusion – Minimal API

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.We use DTOs to control the endpoints’ inputs and outputs, giving 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 or data 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 (the abstraction).

You should now understand the added value of DTOs and what part in an API contract they play. Finally, you should have a strong base of Minimal APIs possibilities.

Summary

Throughout the chapter, we explored ASP.NET Core Minimal APIs and their integration with the DTO pattern. Minimal APIs simplify web application development by reducing boilerplate code. The DTO pattern helps us decouple the API contract from the application’s inner workings, allowing flexibility in crafting REST APIs. DTOs can also save bandwidth and flatten or change data structures. Endpoints exposing their domain or data entities directly can lead to issues, while DTO-enabled endpoints offer better control over data exchanges. We also discussed numerous Minimal APIs aspects, including input binding, outputting data, metadata, JSON serialization, endpoint filters, and endpoint organization. With this foundational knowledge, we can begin to design ASP.NET Core minimal APIs.

For more information about Minimal APIs and what they have to offer, you can visit the Minimal APIs quick reference page of the official documentation: https://adpg.link/S47i

In the next chapter, we revisit the same notions in an ASP.NET Core MVC context.