Home ASP.NET Core 7 - New features
Post
Cancel

ASP.NET Core 7 - New features


.NET 7 succeeds .NET 6 with an 18-month (Standard Term Support) release and comes with a massive list of features and improvements, with a strong focus on performance. I started this .NET 7 series with this post, covering the built-in Rate limiting middleware added to ASP.NET Core 7 in depth. The source code I used to showcase this series also used other new features I’m dedicating this post to discussing.


ASP.NET Core 7 improvements


Still about ASP.NET Core and minimal APIs, I’d like to quickly cover Route groups, Endpoint filters and Typed results.

Route Groups

When dealing with regular APIs based on controllers, we can naturally achieve easy organization by having many endpoints underlying the same controller and base route. With minimal APIs, it could be challenging before group routes become available.

Let’s check again the Program.cs and the RouteGrouper.cs classes, respectively:

Program.cs

RouteGrouper.cs

As you can spot, there’s now the MapGroup extension method.

1
RouteGroupBuilder MapGroup(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string prefix);

Then, the RouteGrouper static class above extends the RouteGroupBuilder returned object to centralize the grouped routes more contextualized; hence I named it MapGitHubIssuesRoutes. You can create as many extension methods as possible for your groups.

Note:: if you’re using Swagger, you can also apply the .WithTags(), so the tag name will be shown around the group, making it visually easier to spot on.

1
2
group.MapGroup(Routes.BaseRoute)
    .WithTags("TagNameYouChoose");

Endpoint filters

Endpoint filters act as request interceptors for validating data, either from URL or body parameters, preventing invalid requests from being passed along. Having filters in .NET has been a trivial task for a while if you ever used middleware for endpoints, but nothing was available for minimal API yet. Now you can add it with the AddEndpointFilter<T> extension method, where T must be of IEndpointFilter type.

1
ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next);

Looking back into the RouteGrouper.cs, notice I implemented a filter for a GET route that lists GithubIssues, in which it has an optional maxIssues optional parameter that needs to be greater than zero when specified.

For a more complex validation with an endpoint that adds a new issue through POST, I created a simple class named ValidationFilter<T>, where T was the model I wanted to execute some validate against. I have used the FluentValidation package for such and implemented it through the GithubIssueValidator class:

ValidationFilter.cs

All that ValidationFilter does is check the body T object argument and call the ValidateAsync() methods. Since GithubIssueValidator implements AbstractValidator<GithubIssue>, FluentValidation knows how to return the validation result for that model. In case of a negative, I produce a bad request with the error summary. You could do it entirely differently with your validation implementation if you follow the interface specification and implement the InvokeAsync() method.

Typed results

When using minimal APIs in .NET 6, the default response type was always of IResult, meaning the return type could be of any type.

According to Microsoft:

The IResult interface can represent values returned from minimal APIs that don’t utilize the implicit support for JSON serializing the returned object to the HTTP response. The static Results class is used to create varying IResult objects that represent different types of responses. For example, setting the response status code or redirecting to another URL.

Consequently, unit testing routes required a cast to a concrete type. With .NET 7, this comes with ease when using TypedResults, so you can strongly and safely infer the return type, such as Ok<T> or Created<T>, for example. It increases the readability enormously and benefits OpenAPI implementation and tooling depending on API metadata to provide documentation.

Notice the HandleGetById above is implementing a Tuple with the 2 possible results, Ok and NotFound (up to 6 parameters).

Lastly, the test asserts the return type directly. With all this in mind, you should use TypedResults over Results from now on.

Output caching middleware

The Output caching middleware is not strict to Minimal APIs. Like the Rate Limiting middleware, it can be used in regular controllers through attributes.

Generally speaking, caching means storing data temporarily for a defined timespan. It’s used to improve performance as the cached data will be delivered immediately instead of requiring a re-processing of the logic for each request. There are multiple types of caching available in .NET for different purposes. With .NET 7, you can use a more general-purpose solution for caching through the Output caching middleware to enable caching of HTTP responses. Note that unlike the response caching based on cache headers, Output caching is only configurable on the server, overriding any caching behavior from the client.

To enable Output caching, you only need to call the AddOutputCache() and UseOutputCache() extension methods, as seen in the Program.cs. Furthermore, in the RouteGrouper, the CacheOutput() method was applied to the endpoint I wanted to cache the HTTP responses. Note that there are many overloads for this method; one of them is the policyName.

I added a simple timed cache policy to cache the result list for 20 seconds and applied to the due endpoint:

1
2
group.MapGet(Routes.ListIssuesWithMax, handler.HandleList)
  .CacheOutput(PolicyNames.TwentySecondsCachePolicy);

You can test that by hitting http://localhost:5131/issues/list. Notice that the issue Ids will remain the same for 20 seconds, no matter how many requests you make, and the list will return the same amount even if you add new GithubIssues using the POST endpoint. This is because new records will be added to the in-memory list, as you can prove by getting it by Id with http://localhost:5131/issues/{guidId}; however, the list from the cached endpoint will refresh only when the policy expires.

Important: note that changing the route will create another cached result, as each URL is a unique key by default. You can put a breakpoint in the GithubIssuesRouteHandler.HandleList() method and observe how often it will hit or bypass the execution depending on the URL changes you make. Try it out by changing the take argument http://localhost:5131/issues/list?take=10.

Of course, this is a very simple usage. You can extend it to more advanced options and also creating a custom Policy. Last but not least, the same policy can be used in API controllers with the following code:

1
2
3
4
5
6
public class GibhubIssuesController
{
    [HttpGet]
    [OutputCache(PolicyName = PolicyNames.TwentySecondsCachePolicy)]    
    public async Task<IActionResult> GetIssues() {...}
}


Final thoughts


It’s fascinating to see how the .NET ecosystem evolves and becomes more cohesive and robust with each release. The overall performance is now stellar and very competitive. In addition, there are many other incredible related releases, like C# 11, EF Core 7, NET MAUI, and others. With all we currently have, developing applications from A to Z to publishing is now easy as it never was.

I hope you enjoyed this series so far. Happy coding!


Check the project on GitHub



This post is licensed under CC BY 4.0 by the author.