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 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:
- The client sends a request to the server.
- ASP.NET Core leverages its data binding and parsing mechanism to convert the information of the HTTP request to C# (input DTO).
- The endpoint does what it is supposed to do.
- ASP.NET Core serializes the output DTO to the HTTP response.
- 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.
Leave a Reply