Archives 2021

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.

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.