Minimal API growing with .NET 7

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 Planet Morning Light

Minimal API with .NET 6

Let’s start with .NET 6. With the help of the WebApplication and WebApplicationBuilder classes, the implementation of the API can be directly added using top-level statements. With .NET 6, the methods MapGet, MapPost offer new overloads using a Delegate type instead of the RequestDelegate type. 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 Func or 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 [FromServices] and [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 WithName, Produces, and WithTags influence the result with the OpenApi description.

.NET 6 Minimal API

To set a move of a game, another MapPost with a different route is shown with the next code snippet. This method returns Ok, BadRequest, NotFound, or 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.

.NET 6 Minimal API

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.

IEndpointRouteBuilder Extension Method

The MapGameEndpoints method specifies the ILogger parameter in addition to the IEndpointRouteBuilder. The ILogger parameter allows passing the logging defined by the ApplicationBuilder to pass it on, and use it within every route implementation.

Grouping Routes

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.

Grouping Routes

Typed Results

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 Ok and Created in turn implement IResult. These typed results help with unit testing, and automatically add type metadata to the OpenAPI description.

Typed Results

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 Func, 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 CreatedGameResponseorBadRequest`.

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.

Multiple Typed Results

Filters

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 AddEndpointFilter method.

Using Filters

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 context and next of the types EndpointFilterInvocationContext and EndpointFilterDelegate. Using next, the filter needs to invoke the next filter. The ordering of filters delcared is important: one filter invokes the next one. With the context, 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.

Endpoint Filter with validation

GameExceptionFilter wraps the invocation of the next handler within a try/catch. If a GameNotFoundException exception is caught, the filter returns a NotFound result.

Endpoint Filter with exception handling

Take away

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!

Christian

If you enjoyed this article, please support me with a coffee. Thanks!

Buy Me A Coffee

More Information

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".

Trainings

Sample source code

Upgrading an ASP.NET Core Web API Project to .NET 6

Web API Updates with .NET 8

Natural type of a lambda expression

OpenAPI

The complete source code of this sample is available on Professional C# source code – see the folder 5_More/Services/GamesApiDotnet7

Advertisement

3 thoughts on “Minimal API growing with .NET 7

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.