Beautiful and compact Web APIs revisited – with C# 10 and .NET 6 Preview 7

Β· 2409 words Β· 12 minutes to read

Back in 2012, when the Roslyn compiler was still closes source and its early CTP stages, I blogged about using its C# scripting capabilities to wire up an ASP.NET Web API server in just a few lines of code, with minimal ceremony. In 2014 I built an OWIN-based host, on top of the, then already well-stablished, scriptcs C# scripting project, that utilized the experimental IIS “Helios” server to provide a framework for building tiny C# web applications.

In 2017 I blogged about about building lightweight, minimal microservices with the early versions of ASP.NET Core. Last year, as ASP.NET Core and the “mainstream” C# (despite the initial resistance) started adopting some of these C# scripting concepts, I wrote how they have been incorporated into ASP.NET Core in .NET 5.0, along with diving into some further improvements for building these lightweight Web APIs.

It is now time to have a look at the latest set of changes in this space - as .NET 6 Preview 7, the latest one at the time of writing, and, in particular, C# 10, bring a lot of extra exciting things to the table.

Recap of the last season (.NET 5.0) πŸ”—

C# 9 introduced support for top-level programs, which removed the unnecessary verbosity of the Program class and static Task Main constructs, and allowed “loose” top-level code. That code was then simply pulled into the synthesised entry point by the compiler. C# 9 also gave us local functions, which can masquerade as global functions in top-level programs, and record types which thanks to their conciseness fit perfectly well into the “minimal” code landscape.

All of these features lend themselves really well to building small focused microservices, and ASP.NET Core enhanced that with plenty of extra helper machinery too, decoupling most of its functionalities from the MVC framework, and allowing for things like routing, authorization or authentication to be used standalone. Some serialization and route building helpers were added too, all of which made it much simpler to create the tiny, focused Web APIs. This was all the stuff I attempted to document last year.

.NET 6 and C# 10 improvements πŸ”—

ASP.NET on top of .NET 6 continues to push these ideas further and further, and they fall into place particularly nicely, due the fact that the team has been able to influence the C# 10 language design to level where the newest version of the language itself now ships with features introduced primarily to make the ASP.NET experience as seamless as possible.

First of all, the boostrapping of new applications becomes simpler, as there comes a new type Microsoft.AspNetCore.Builder.WebApplication whose goal is to provide a simple one-stop shop for setting up a web application. It unifies a lot of existing concepts and reduces the need for writing manual application bootstrapping orchestration code (on that note, the WebHostBuilder is still there, but it’s on a deprecation path). In a gist, WebApplication can be used to do just about everything you may need for a Web API - access configuration, configure dependency injection container, configure the application pipeline by registering middleware components and to actually start the HTTP listener.

The simplest possible Web API looks as follows now:

var a = WebApplication.Create(args);
a.MapGet("/", () => "Hello world");

There are several things worth pointing out here.

First of all, there are no using statements here - and this is not because they have been omitted for brevity. C# 10 incorporates one of the wonderful concepts from the C# scripting world, namely the presence of implicit global usings. The usings are controlled via the MSBuild project SDK used and the default implicit usings for Web SDK projects are:


These come on top of the default included set for all SDK projects:


All these namespaces do not need to be imported by hand and the code can rely on them straight up.

In its unifying capacity, WebApplication happens to be all of IApplicationBuilder, IHost and IEndpointRouteBuilder at the same time, and, along its own features, has access to all of the built-in and third party extension methods for these interfaces. In particular, its compatibility with IApplicationBuilder comes in very handy, as that is how we’d normally configure application pipeline before - so the existing extensions all work fine.

Because of how fluent method chains are often written, we cannot write the simple example from above in one line, because the new MapGet extension method returns MinimalActionEndpointConventionBuilder, instead of a WebApplication instance, which is still needed to start the application. Nevertheless, the example looks pretty slick and this new set of endpoint configurations around the MinimalActionEndpointConventionBuilder is very cool - as we shall see in a moment.

In .NET 6 Preview 7, the implicit usings feature is (no pun intended) still implicitly enabled. However, to avoid breaking changes, as it is rather invasive, from next release onwards, ot shall be opt-in via a project level setting:


Changes to lambdas πŸ”—

C# 10 also ships with a set of improvements to lambdas, which make working with minimal ASP.NET Web APIs a true pleasure.

The core changes are:

  • lambdas can now be decorated attributes
  • lambda parameters can now be decorated attributes
  • an explicit return type may be specified before the lambda’s parenthesized parameter list
  • lambdas can have a natural delegate type (if the parameters types are explicit and the return type is explicit or possible to be be inferred)

This solves a ton of problems that developers had to face in the earlier version, in case you wanted to use lambdas to define handlers for HTTP API endpoints - and ultimately these shortcomings were forcing people into the more traditional controller structures.

For example, consider the following code:

var a = WebApplication.Create(args);
a.MapGet("square/{number}", (int number) => Results.Ok($"Squared {number} is: {number * number}."));

We register a single endpoint GET handler, for the /square/{number} route. Because of the above mentioned lambda improvements, the lambda can be used to process the request, along with having support for model binding, and the number is extracted by the framework from the route and supplied to our handler as expected. It all feels very natural.

If we call this endpoint at e.g. /square/2, it will return a JSON string “Squared 2 is: 4.”. The new set of helpers around the Results utility class bring features known from MVC to the minimal APIs here. It is built on top of the IResult class which can be used to define a contract that represents the result of an HTTP endpoint and is now integral part of ASP.NET HTTP abstractions, under Microsoft.AspNetCore.Http.

In addition to all of that, if this endpoint is called with a non-integer route parameter, for example /square/foo, the framework will automatically issue a 400 Bad Request response, without engaging our handler. The ultimate goal of the team was to allow usage of lambdas while having parity when it comes to using attributes and other features available to ASP.NET apps built with controllers.

Taking this further πŸ”—

A more elaborate example can be built once we start adding some custom types into the DI container. In the previous blog posts that I wrote about minimal Web APIs I used a Contact type (which later became Contact record) and a super basic ContactService. In order to allow the older examples to be easy to compare with these new ones here, I will therefore use these samples here as well. They are listed below for the record:

public record Contact(int ContactId, string Name, string Address, string City);

public class ContactService
    private readonly List<Contact> _contacts = new()
            new Contact(1, "Filip W", "Bahnhofstrasse 1", "Zurich"),
            new Contact(2, "Josh Donaldson", "1 Blue Jays Way", "Toronto"),
            new Contact(3, "Aaron Sanchez", "1 Blue Jays Way", "Toronto"),
            new Contact(4, "Jose Bautista", "1 Blue Jays Way", "Toronto"),
            new Contact(5, "Edwin Encarnacion", "1 Blue Jays Way", "Toronto")

    public Task<IEnumerable<Contact>> GetAll() => Task.FromResult(_contacts.AsEnumerable());

    public Task<Contact?> Get(int id) => Task.FromResult(_contacts.FirstOrDefault(x => x.ContactId == id));

    public Task<int> Add(Contact contact)
        var newId = (_contacts.LastOrDefault()?.ContactId ?? 0) + 1;
        _contacts.Add(contact with { ContactId = newId });
        return Task.FromResult(newId);

    public async Task Delete(int id)
        var contact = await Get(id);
        if (contact == null)
            throw new InvalidOperationException(string.Format("Contact with id &#39;{0}&#39; does not exists", id));


In .NET 5.0, we could use the Map{verb} helper extension methods to configure our endpoints, and there were some other helpers for serializing/deserializing request and response bodies as well. On the other hand, the lack of model binding support, lack of integration into DI and the lack of (easy) access to route data were all severe bottlenecks - and all of those are resolved now.

A fully fledged CRUD Web API around this ContactService can be written in roughly 20 lines of code.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();
app.MapGet("/contacts", async (ContactService service) => Results.Ok(await service.GetAll()));

app.MapGet("/contacts/{id}", async (ContactService service, int id) => {
    var contact = await service.Get(id);
    return contact is {} ? Results.Ok(contact) : Results.NotFound();
}).WithMetadata(new RouteNameMetadata("ContactById"));

app.MapPost("/contacts", async (ContactService service, Contact contact) => {
    var id = await service.Add(contact);
    return Results.CreatedAtRoute("ContactById", new { id });

app.MapDelete("/contacts/{id}", async (ContactService service, int id) => {
    await service.Delete(id);
    return Results.NoContent();

Of course it is not really the lines of codes we are after here - it is the conciseness, readability and quality of code. There is no manual parsing of any input parameters, every handler has strongly-typed access to route data, access to request body (if needed - for example the POST request accepts Contact model deserialized from JSON) or access to DI services (ContactService is injected into each handler; it is not necessary here since it’s a singleton, but it’s good for illustrative purposes). All of the endpoint handler make use of different Results-based helpers to produce the relevant responses cleanly and hassle-free.

There is even support for generating a 201 Created response, which returns a Location header with a link to the newly resource. Similarly to how it was done in MVC, it is done by having a route name and being able to reference the route by it. It is still a bit rough-edged, but in the next release a WithName({string}) method will be possible to be used, instead of the rather unpleasant manual setting of route name metadata.

I really love to see how these things come together.

Adding security πŸ”—

Just like we did in the post from last year, let’s have a look at what it takes to introduce API security into such minimal API. To do that, we will configure authentication for our app – the ability to consume JWT tokens, and authorization – the permissions and rules which will be enforced on the authenticated caller. And we shall do it using IdentityServer integration.

This is done similarly to how it would have been in the past, in .NET 5.0 - by adding the relevant Identity Server services into the DI container, as well as configuring the container with the required authN and authZ stuff.

using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder(args);
    .AddAuthorization(options =>
            p => p.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().RequireClaim("scope", "contacts.manage"));
    .AddJwtBearer(o =>
        o.Authority = "http://localhost:5000/openid";
        o.Audience = "embedded";
        o.RequireHttpsMetadata = false;
var app = builder.Build();

Notice that at this point we finally need some using statements for the first time. There is a single contacts.manage authorization policy configured here, which we will apply to some of our HTTP handlers. Authentication tokens will be issued by the Identity Server instance running in the same process as the app, the so-called “embedded identity server” pattern. It will run on the /openid branch of the application, separate from the rest.

Identity Server requires scopes, resources and clients to be fed into it in order for authentication to be possible in the first place. This typically happens by connecting some persistency layer and reading those entities from a database, but for this example, an in-memory configuration suffices.

using IdentityServer4.Models;

public static class IdentityServerBuilderTestExtensions
    public static IIdentityServerBuilder AddTestConfig(this IIdentityServerBuilder builder) =>
        builder.AddInMemoryClients(new[] { new Client
                ClientId = "client1",
                ClientSecrets =
                    new Secret("secret1".Sha256())
                AllowedGrantTypes = { GrantType.ClientCredentials },
                AllowedScopes = { "contacts.manage" }
                new ApiResource("embedded")
                    Scopes = { "contacts.manage" },
                    Enabled = true
            }).AddInMemoryApiScopes(new[] { new ApiScope("contacts.manage") })

Where things get interesting, is how we can now integrate this authentication and authorization set up into the lightweight HTTP handlers we have been using. First we will set up Identity Server to run on the /openid branch of our application as we promised.

app.Map("/openid", false, id =>
    // use embedded identity server to issue tokens


We have to use an overload with the extra boolean to map the path, because - these are, I guess, teething problems - there is otherwise ambiguity between the old Map extension for IApplicationBuilder (which WebApplication happens to be) and the new Map extension for IEndpointRouteBuilder (which it also happens to be). Regardless, at this point we are ready to protect our endpoints.

Let us imagine that the GET endpdoints should be freely accessible, but the POST and DELETE endpdoints should require the token complying with the previously configured contacts.manage policy. Thanks to the lambda improvements in C# 10, we can simply add the familiar [Authorize] attribute to the lambda handlers:

app.MapPost("/contacts", [Authorize("contacts.manage")] async (ContactService service, Contact contact) => {
    var id = await service.Add(contact);
    return Results.CreatedAtRoute("ContactById", new { id });

app.MapDelete("/contacts/{id}", [Authorize("contacts.manage")] async (ContactService service, int id) => {
    await service.Delete(id);
    return Results.NoContent();

Additionally, it is even supported to bind ClaimsPrincipal directly into such lambda-based HTTP endpoint handler as a lambda parameter:

app.MapGet("/current-user", [Authorize] (ClaimsPrincipal principal) => Results.Ok(principal.Claims.ToDictionary(c => c.Type, c => c.Value)));

Very cool indeed, isn’t it? And of course if we try to invoke any of these three endpoints without a token, the framework automatically issues 401 Unauthorized.

Summary πŸ”—

I really love the changes in .NET 6 and looking forward to them shipping RTM this fall.

NET 6 (Preview 7 at the moment!), C# 10 and ASP.NET provide a very exciting set of features for building minimalistic and lightweight Web APIs. And this is a topic very, very close to my heart, due to my long standing involvement in the C# scripting community.

I very much looking forward to writing my Web APIs this way in the future. All the source code is available on Github.


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github and on Mastodon.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP