Request Filters, Parameter Mapping ASP.NET Core 7

Traditionally, REST API endpoints could be created in ASP.NET Core applications by using so-called controller classes. Different controllers would stand in for different application domains or entities. Additionally, a portion of the URL path is typically shared by all endpoints assigned to the same controller. To manage user data, for instance, a class called UsersController would be used, and all of its endpoints would begin with the /users URL path.

Since.NET 6, we have been able to create REST API endpoints on ASP.NET Core without having to define controllers. Instead, we can directly map a given HTTP verb to a method in the code that launches the application.

The use of Minimal APIs has several significant advantages. It is significantly less verbose than using conventional controllers, which is one of its advantages. Creating each endpoint only takes a few lines of code. Both writing and reading it are simple. Moreover, reading it involves less cognitive effort.

The fact that using Minimal APIs is quicker than using conventional controllers is another significant advantage. The methods representing the endpoints become simpler to compile and run because there is less code and a much more simplified bootstrapping.

Of course, using minimal APIs has some drawbacks as well. First off, breaking up REST API into distinct controllers would make the code in an enterprise-grade application with a lot of endpoints easier to read and maintain. Second, because Minimal APIs are still a young technology, some features from controller-based APIs might be absent. Since the release of.NET 7, however, this has been constrained to a small subset of lesser-used features that are relatively simple to find a replacement for; as a result, purely in terms of functionality, Minimal APIs can now replace controller-based APIs in almost all scenarios.

We will discuss the features that Minimal APIs now offer with the.NET 7 release in this article. We will look at why less verbose minimal APIs are now almost as robust as conventional controller-based APIs.

Minimal API basics

Imagine that we have a ToDoApiApp ASP.NET Core Web API project. The entry point for this.NET 7 project is a condensed version of the Program.cs file. The following will be the contents of this file:
using Microsoft.AspNetCore.Mvc;
using TodoApiApp;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton
    <ITodosRepository, TodosRepository>();

var app = builder.Build();

app.MapGet("/todos", (
    [FromServices] ITodosRepository repository) =>
{
    return repository.GetTodos();
});

app.MapGet("/todos/{id}", (
    [FromRoute] int id,
    [FromServices] ITodosRepository repository) =>
{
    return repository.GetTodo(id);
});

app.Run();

Two examples of minimal API endpoints are visible because they are registered directly in the logic for application startup. We use the MapGet method twice after creating the app variable. The /todos URL path is being linked to the initial call. The /todos/id> URL path is connected to the second call.

We have an anonymous method connected to the given path in both scenarios. The GetTodos method of the repository object’s results are returned in the first method. In the second case, we are calling the repository object’s GetTodo method with an integer id parameter and returning the results.

We have an anonymous method connected to the given path in both scenarios. The GetTodos method of the repository object’s results are returned in the first method. In the second case, we are calling the repository object’s GetTodo method with an integer id parameter and returning the results.

As a result, as demonstrated by the example above, Minimum API endpoints use a mapping method on the object that serves as a representation of the application. The methods for mapping are unique to an HTTP verb. Both of the aforementioned instances in the example use a mapping technique unique to the HTTP GET verb. This is the basis for the name MapGet. We would have used MapPost or MapPut if we had been using the POST or PUT verb, respectively.

The first parameter passed in when this method is called is the URL path that it corresponds to. By enclosing route parameters in curly brackets, as seen in the /todos/id path, we can use them. Then comes the approach that corresponds to this path. As in the two examples above, we can either have an anonymous method or pass a reference to an existing method.

Two different kinds of parameters are used in our methods. The parameters identified by the FromRoute attribute are taken from the method’s mapped URL path. The objects pulled from the dependency injection system are represented by the parameters marked with the FromServices attribute.

In our situation, both routes contain an implementation of the ITodosRepository interface. Earlier, we used the AddSingleton method on the Services property of the builder object to map this interface to the TodosRepository class. The definitions of the class and interface are as follows:

namespace TodoApiApp;

public interface ITodosRepository
{
    IEnumerable<(int id,
        string description)> GetTodos();
    string GetTodo(int id);
    void InsertTodo(string description);
    void UpdateTodo(int id, string description);
    void DeleteTodo(int id);
}

internal class TodosRepository : ITodosRepository
{
    private readonly Dictionary<int, string> todos
        = new Dictionary<int, string>();
    private int currentId = 1;

    public IEnumerable<(int id,
        string description)> GetTodos()
    {
        var results = new List<(int id,
            string description)>();

        foreach (var item in todos)
        {
            results.Add((item.Key,
                item.Value));
        }

        return results;
    }

    public string GetTodo(int id)
    {
        return todos[id];
    }

    public void InsertTodo(
        string description)
    {
        todos[currentId] = description;
        currentId++;
    }

    public void UpdateTodo(
        int  id, string description)
    {
        todos[id] = description;
    }

    public void DeleteTodo(
        int id)
    {
        todos.Remove(id);
    }
}

This service essentially functions as a TODO list that we can read, add to, edit, and delete items from. Each item is represented as a string value and has a distinct integer identifier that is represented by the dictionary key when the items are stored in a dictionary. REST API endpoints have been put in place so far for two actions: reading the entire list and reading individual items. We will now carry out the remaining operations while showcasing the new.NET 7 Minimal APIs features.

Request filters in Minimal APIs

A strong feature of Minimal APIs is request filtering, which enables programmers to add extra processing steps to incoming requests. Either before or after the request reaches the endpoint, it can run some code.

Request validation frequently uses request filtering. In our example, we will apply it in this manner. The following code will be added to our Program.cs file just before the Run method call on the app object.

app.MapPost("/todos/{description}", (
    [FromRoute] string description,
    [FromServices] ITodosRepository repository) =>
{
    repository.InsertTodo(description);

}).AddEndpointFilter(async (context, next) =>
{
    var description = (string?)context.Arguments[0];
    if (string.IsNullOrWhiteSpace(description))
    {
        return Results.Problem("Empty TODO description not allowed!");
    }
    return await next(context);
});

In this illustration, we mapped a POST endpoint so that we could add something to the TODO list. By using the AddEndpointFilter method, we added an endpoint filter to it. One method for including an endpoint filter is as described here. The method in this particular implementation accepts two parameters: context and next.

The request context is represented by the context parameter. Data can be extracted from it and examined or modified. The request processing chain continues with the next parameter. There is no limit to how many request filters we can add. Once there are no more processing steps left in the chain, the endpoint method will be called.

We can use the static Problem method on the Results class to skip the request processing step and return the response sooner. In the example, if the entered TODO item is empty, we short-circuit the pipeline and return a validation error. If not, we call the method represented by the subsequent delegate to pass the request on to the subsequent processing stage.

Automatic mapping of headers and query string parameters

The ability of Minimal APIs to automatically map request parameters from the query string or headers is another helpful feature. Creating an object with the properties that correspond to the anticipated parameters, passing it as a parameter into the endpoint method, and designating it with the AsParameters attribute are all that are required.

Here is an illustration of it. We will add this additional endpoint mapping to the app object. The TODO list’s existing items can be changed using this method, which is mapped to the PUT HTTP verb.

app.MapPut("/todos/{id}", (
    [FromRoute] int id,
    [AsParameters] EditTodoRequest request,
    [FromServices] ITodosRepository repository) =>
{
    repository.UpdateTodo(id, request.Description);
});

As a set of input parameters, we are inserting an object of the EditTodoRequest type. This class is described as follows:

internal struct EditTodoRequest
{
    public string Description { get; set; }
}

We only have a single field called Description, so it will automatically be mapped to a description request parameter. Additional properties in this class could be created if we needed to pass more parameters, and they would be mapped appropriately.

Working with file data in Minimal APIs

Prior to.NET 7, Minimal APIs only supported straightforward request types. However, we can now carry out more sophisticated operations, like file processing. It is currently as easy as adding an IFormFile parameter to the endpoint method to upload a file to an API endpoint.

We added the following endpoint method to the endpoint mappings in the Program.cs file to show how file upload functions. This approach presupposes that the file we’re uploading is a text file with a line for each new TODO item that needs to be added.

app.MapPost("/todos/upload", (IFormFile file,
    [FromServices] ITodosRepository repository) =>
{
    using var reader = new StreamReader(file.OpenReadStream());

    while (reader.Peek() >= 0)
        repository.InsertTodo(reader.ReadLine() ?? string.Empty);

});

The OpenReadStream method on the IFormFile implementation is all that is required to read from the uploaded file, as shown by this method. The Peek method on the stream can then be used to see if there are any more lines. By using the ReadLine method, we can read the following line if there are any lines.

Conclusion

Our overview of the most potent features added to the ASP.NET Core Minimal APIs with the.NET 7 release comes to a close now. There have been added features besides these ones. But these characteristics stand out the most.

Minimal APIs are almost as powerful as conventional controller-based APIs with their current functionality. Controller-based APIs are still preferred in some use cases. For instance, applications with a lot of API endpoints are simpler to organize in terms of controllers. However, there is hardly any functionality that controllers can perform that Minimal APIs cannot.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *