All posts by Pamela Sullivan

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.

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.

Goal – Model-View-Controller

In the context of REST APIs, the MVC pattern aims to streamline the process of managing an entity by breaking it down into three separate, interacting components. Rather than struggling with large, bloated blocks of code that are hard to test, developers work with smaller units that enhance maintainability and promote efficient testing. This compartmentalization results in small, manageable pieces of functionality that are simpler to maintain and test.

Design

MVC divides the application into three distinct parts, where each has a single responsibility:

  • Model: The model represents the data and business logic we are modeling.
  • View: The view represents what the user sees. In the context of REST APIs, that usually is a serialized data structure.
  • Controller: The controller represents a key component of MVC. It orchestrates the flow between the client request and the server response. The primary role of the controller is to act as an HTTP bridge. Essentially, the controller facilitates the communication in and out of the system.

The code of a controller should remain minimalistic and not contain complex logic, serving as a thin layer between the clients and the domain.

We explore alternative points of view in Chapter 14, Layering and Clean Architecture.

Here is a diagram that represents the MVC flow of a REST API:

 Figure 6.1: Workflow of a REST API using MVCFigure 6.1: Workflow of a REST API using MVC 

In the preceding diagram, we send the model directly to the client. In most scenarios, this is not ideal. We generally prefer sending only the necessary data portion, formatted according to our requirements. We can design robust API contracts by leveraging the Data Transfer Object (DTO) pattern to achieve that. But before we delve into that, let’s first explore the basics of ASP.NET Core MVC.

Anatomy of ASP.NET Core web APIs

There are many ways to create a REST API project in .NET, including the dotnet new webapi CLI command, also available from Visual Studio’s UI. Next, we explore a few pieces of the MVC framework, starting with the entry point.

The entry point

The first piece is the entry point: the Program.cs file. Since .NET 6, there is no more Startup class by default, and the compiler autogenerates the Program class. As explored in the previous chapter, using the minimal hosting model leads to a simplified Program.cs file with less boilerplate code.Here is an example:

using Shared;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCustomerRepository();
builder.Services
    .AddControllers()
    .AddJsonOptions(options => options
        .JsonSerializerOptions
        .Converters
        .Add(new JsonStringEnumConverter())
    )
;
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseDarkSwaggerUI();
}
app.MapControllers();
app.InitializeSharedDataStore();
app.Run();

In the preceding Program.cs file, the highlighted lines identify the minimum code required to enable ASP.NET Core MVC. The rest is very similar to the Minimal APIs code.

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.

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.