To create REST API services with Microsoft .NET, the Minimal API was introduced with .NET 6. The minimal API makes use of top-level statements and is based on some C# language enhancements such as lambda expression improvements such as natural types and declaring a return type. While the minimal API was great for small service implementations, it received some critism with larger applications because with this one source code file was growing too big. With .NET 7, the minimal API offers new features to resolve all these issues as shown in this article.
Minimal API with .NET 6
Let’s start with .NET 6. With the help of the
WebApplicationBuilder classes, the implementation of the API can be directly added using top-level statements. With .NET 6, the methods
MapPost offer new overloads using a
Delegate type instead of the
Delegate, the base class of all delegates, allows passing lambda expressions directly with the parameter. Here, a feature of C# 10 comes into play: improvements of lambda expressions. Before C# 10, a lambda expression could not be directly assigned to the
Delegate type. Either casting the lambda, or creating a new instance of a
Action, and assign the lambda to this instance was required. This didn’t help with readability. Now you can assign a lambda expression directly to the parameter.
The following code snippet (source file GamesApiDotnet6/Program.cs) shows the
MapPost method passing a lambda expression with a CreateGameRequest and IGamesService parameters. The CreateGamesRequest comes from the HTTP body, the IGamesService is injected from the DI container. To enhance readability (and to resolve conditions where the source cannot be automatically resolved), you can assign attributes such as
[FromBody] to the parameters. With the implementation of the lambda expression, the
CreateGameAsync method is invoked, and a
CreateGameResponse is returned. To return results, the factory class
Results is available with .NET 6 to offer returning results similar to the
ControllerBase class. The methods
WithTags influence the result with the OpenApi description.
To set a move of a game, another
MapPost with a different route is shown with the next code snippet. This method returns
InternalServerError depending on th eoutcome. While the main functionality of this method is implemented with the implementation of the
IGamesService contract, the implementation size of a single minimal API method can easily exceed the size of a small method. Offering multiple API methods, size of Program.cs can grow fast.
Let’s look at what can be done with .NET 7.
Extension Method for IEndpointRouteBuilder
The first step to reduce the implementation within the Program.cs file is to move the implementation of the routes to an extension method. This can even be done using .NET 6. The extension method is defined in a separate file GameEndpoints.cs. The extension method is defined for the
IEndpointRouteBuilder interface. The
MapGameEndpoints method can now be invoked using the
WebApplication instance named
app, and all the routes can specified within the extension method
MapGameEndpoints as shown with the
MapGet method with the following code snippet.
MapGameEndpoints method specifies the
ILogger parameter in addition to the
ILogger parameter allows passing the logging defined by the
ApplicationBuilder to pass it on, and use it within every route implementation.
Several of the endpoints need common functionality, such as authentication, specify tags for OpenAPI, and rate limiting. The common functionality can be specified using the
MapGroup method. This method returns a
RouteGroupBuilder which in turn can be used to specify the routes with the common functionality. With the sample code, a group is created with a common tag and an endpoint filter for logging. Endpoint filters are discussed later in this article.
With this .NET 6 version of the API you’ve seen several invocations of the
Produces method to specify the HTTP status codes that can be returned from an API. This information is used with the OpenAPI description. .NET 7 adds the
TypedResults factory type which is a typed equivalent of the
Results factory type.
Results is declared to return
IResult with every method, whereas
TypedResults return a concrete type, such as
Ok with the
Ok method, and
Created with the
Created method. The types
Created in turn implement
IResult. These typed results help with unit testing, and automatically add type metadata to the OpenAPI description.
In case different results are returned from a method, another feature of C# 10 comes into play: with the lambda expression, the return type can be specified. The following code snippet shows the lambda expression to create a game to return
Task<Results<Created<CreatedGameResponse>, BadRequest>>. The method returns a Task and uses the
async modifier because
await is used with the implementation. The generic type paramter is of a generic
Results type. Similar as you know from the
Action, and the
ValueTuple type, here multiple generic
Results types are defined with two, three, four, … up to six parameters. Each of these parameters has an
IResult constraint which allows only types implementing this interface. You can now specify the possible returns without the need to add mulitple
Produces methods. Specifying ‘Results<Created<CreatedGameResponse>, BadRequest>’ means that the method can return
Created passing a CreatedGameResponse
In case you’ve only one result type, you don’t need to specify the return type with the lambda expression. Using
TypedResults with the return is all what’s needed.
Filters are a great way to add common functionality endpoint implementations, and to simplify the implementation of the endpoint. Check the following code snippet with the implementation of a game move. Compared to the .NET 6 version, the validation of the input parameter, and the exception handling code has been removed from the implementation of this method. Here, just the SetMoveAsync method using the IGamesService implementation is invoked, and a typed result is returned. The validation of the input parameter is done with the filter
GameMoveValidationFilter, and the exception handling is done with the filter
GameMoveExceptionFilter. The filters are specified using the
The implementation of a filter is similar to an ASP.NET Core middleware, the term could be middleware for an endpoint.
An endpoint implements the interface
IEndpointFilter with the method
InvokeAsync. The method
InvokeAsync declares the parameters
next of the types
next, the filter needs to invoke the next filter. The ordering of filters delcared is important: one filter invokes the next one. With the
HttpContext can be accessed, and the arguments provided to a route handler.
The next code snippet shows the
GameMoveValidationFilter implementation. The
InvokeAsync method validates the argument values, and returns a
BadRequest if the validation fails.
GameExceptionFilter wraps the invocation of the next handler within a try/catch. If a
GameNotFoundException exception is caught, the filter returns a
With .NET 7, the minimal API has been extended with several features. The
IEndpointRouteBuilder interface offers the
MapGroup method to group routes to specify common handling for endpoint handlers. Filters allow specifying common functionality, and allow to simplify route handler implementations. Typed results allow to reduce invocations of
Produces. Instead of invoking the
Produces method, return types of the lambda expression implementation can be specifyied using the generic
Results type together with the
TypedResults factory class.
Enjoy learning and programming!
If you enjoyed this article, please support me with a coffee. Thanks!
More information about creating services with ASP.NET Core is available in my book and my workshops.
Read more about ASP.NET Core in my book Professional C# and .NET – 2021 Edition
See Chapter 25, "Services".
Upgrading an ASP.NET Core Web API Project to .NET 6
Natural type of a lambda expression
The complete source code of this sample is available on Professional C# source code – see the folder 5_More/Services/GamesApiDotnet7
3 thoughts on “Minimal API growing with .NET 7”