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
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.
Yury Sobolev is Full Stack Software Developer by passion and profession working on Microsoft ASP.NET Core. Also he has hands-on experience on working with Angular, Backbone, React, ASP.NET Core Web API, Restful Web Services, WCF, SQL Server.