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 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.
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.
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.
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.
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.
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 CreatedGameResponseor
BadRequest`.
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
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.
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.
GameExceptionFilter
wraps the invocation of the next handler within a try/catch. If a GameNotFoundException
exception is caught, the filter returns a NotFound
result.
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!

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