Low Ceremony, High Value: A Tour of Minimal APIs in .NET 6

In this post, let's take a tour of Minimal APIs in .NET 6.

Dave Brock
Dave Brock

This post was originally published on the Telerik Blog.

When developing APIs in ASP.NET Core, you're traditionally forced into using ASP.NET Core MVC. Going against many of the core tenets of .NET Core, MVC projects give you everything and the kitchen sink. After creating a project from the MVC template and noticing all that it contains, you might be thinking: all this to get some products from a database? Unfortunately, with MVC it requires so much ceremony to build an API.

Looking at it another way: if I'm a new developer or a developer looking at .NET for the first time (or after a long break), it's a frustrating experience—not only do I have to learn how to build an API, I have to wrap my head around all I have to do in ASP.NET Core MVC. If I can build services in Node with just a few lines of code, why can't I do it in .NET?

Starting with .NET 6 Preview 4, you can. The ASP.NET team has rolled out Minimal APIs, a new, simple way to build small microservices and HTTP APIs in ASP.NET Core. Minimal APIs hook into ASP.NET Core's hosting and routing capabilities and allow you to build fully functioning APIs with just a few lines of code. This does not replace building APIs with MVC—if you are building complex APIs or prefer MVC, you can keep using it as you always have—but its a nice approach to writing no-frills APIs.

In this post, I'll give you a tour of Minimal APIs. I'll first walk you through how it will work with .NET 6 and C# 10. Then, I'll describe how to start playing with the preview bits today. Finally, we'll look at the path forward.

Write a Minimal API with Three Lines of Code

If you want to create a Minimal API, you can make a simple GET request with just three lines of code.

var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
await app.RunAsync();

That's it! When I run this code, I'll get a 200 OK response with the following:  

HTTP/1.1 200 OK
Connection: close
Date: Tue, 01 Jun 2021 02:52:42 GMT
Server: Kestrel
Transfer-Encoding: chunked

Hello World!

How is this even possible? Thanks to top-level statements, a welcome C# 9 enhancement, you can execute a program without a namespace declaration, class declaration, or even a Main(string[] args method. This alone saves you nine lines of code. Even without the Main method, we can still infer arguments—the compiler takes care of this for you.

You'll also notice the absence of using statements. This is because by default, in .NET 6, ASP.NET Core will use global usings—a new way to declare your usings in a single file, avoiding the need to declare them in individual source files. I can keep my global usings in a devoted .usings file, as you'll see here:

global using System;
global using System.Net.Http;
global using System.Threading.Tasks;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.DependencyInjection;

If you've worked with Razor files in ASP.NET Core, this is similar to using a _Imports.razor file that allows you to keep @using directives out of your Razor views. Of course, this will be out-of-the-box behavior but doesn't have to replace what you're doing now. Use what works best for you.

Going back to the code, after creating a WebApplication instance, ASP.NET Core uses MapGet to add an endpoint that matches any GET requests to the root of the API. Right now, I'm only returning a string. I can use lambda improvements to C# 10 to pass in a callback—common use cases might be a model or an Entity Framework context. We'll provide a few examples to show off its flexibility.

Use HttpClient with Minimal APIs

If you're writing an API, you're likely using HttpClient to consume APIs yourself. In my case, I'll use the HttpClient to call off to the Ron Swanson Quotes API to get some inspiration. Here's how I can make a async call to make this happen:

var app = WebApplication.Create(args);
app.MapGet("/quote", async () => 
    await new HttpClient().GetStringAsync("https://ron-swanson-quotes.herokuapp.com/v2/quotes"));
await app.RunAsync();

When I execute this response, I'll get a wonderful quote that I will never disagree with:

HTTP/1.1 200 OK
Connection: close
Date: Fri, 04 Jun 2021 11:27:47 GMT
Server: Kestrel
Transfer-Encoding: chunked

["Dear frozen yogurt, you are the celery of desserts. Be ice cream or be nothing. Zero stars."]

In more real-world scenarios, you'll probably call GetFromJsonAsync with a model, but that can be done just as easily. Speaking of models, let's take a look to see how that works.

Work with Models

With just an additional line of code, I can work with a Person record. Records, also a C# 9 feature, are reference types that use value-based equality and help enforce immutability. With positional parameters, you can declare a model in just a line of code. Check this out:

var app = WebApplication.Create(args);
app.MapGet("/person", () => new Person("Bill", "Gates"));
await app.RunAsync();

public record Person(string FirstName, string LastName);

In this case, the model binding is handled for us, as we get this response back:

HTTP/1.1 200 OK
Connection: close
Date: Fri, 04 Jun 2021 11:36:31 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

{
  "firstName": "Bill",
  "lastName": "Gates"
}

As we get closer to the .NET 6 release, this will likely work with annotations as well, like if I wanted to make my LastName required:

public record Person(string FirstName, [Required] string LastName);

So far, we haven't passed anything to our inline lambdas. If we set a POST endpoint, we can pass in the Person and output what was passed in. (Of course, a more common ideal real-world scenario would be passing in a database context. I'll leave that as an exercise for you, as setting up a database and initializing data is outside the scope of this post.)

var app = WebApplication.Create(args);
app.MapPost("/person", (Person p) => $"We have a new person: {p.FirstName}     {p.LastName}");
await app.RunAsync();

public record Person(string FirstName, string LastName);

When I use a tool such as Fiddler (wink, wink), I'll get the following response:

HTTP/1.1 200 OK
Connection: close
Date: Fri, 04 Jun 2021 11:36:31 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

We have a new person: Ron Swanson

Use middleware and dependency injection with Minimal APIs

Your production-grade APIs—no offense, Ron Swanson—will need to deal with dependencies and middleware. You can handle this all through your Program.cs file, as there is no Startup file out of the box. When you create a WebApplicationBuilder, you have access to the trusty IServiceCollection to register your services.

Here's a common example, when you want only to show exception details when developing locally.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

// endpoints

There's nothing against you creating a Startup file yourself as you always have, but you can do it right here in Program.cs as well.

Try Out Minimal APIs Yourself

If you'd like to try out Minimal APIs yourself right now, you have two choices: live on the edge or live on the bleeding edge.

Live On The Edge: Using the Preview Bits

Starting with Preview 4, you can use that release to explore how Minimal APIs work, with a couple of caveats:

  • You can't use global usings
  • The lambdas will be casted

Both of these are resolved with C# 10, but the Preview 4 bits use C# 9 for now. If you want to use Preview 4, install the latest .NET 6 SDK—I'd also recommend installing the latest Visual Studio 2019 Preview. Here's how our first example would look. (I know, six lines of code. What a drag.)

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;

var app = WebApplication.Create(args);
app.MapGet("/", (Func<string>)(() => "Hello World!"));
await app.RunAsync();

If you want to start with an app of your own, you can execute the following from your favorite terminal:

dotnet new web -o MyMinimalApi

Living on the Bleeding Edge: Use C# 10 and the latest compiler tools

If you want to live on the bleeding edge, you can use the latest compiler tools and C# 10.

First, you'll need to add a custom nuget.config to the root of your project to get the latest tools:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <!--To inherit the global NuGet package sources remove the <clear/> line below -->
    <clear />
    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
    <add key="dotnet6" value="https://dnceng.pkgs.visualstudio.com/public/_packaging/dotnet6/nuget/v3/index.json" />
    <add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
  </packageSources>
</configuration>

In your project file, add the following to use the latest compiler tools and enable the capability for the project to read your global usings from a .usings file:

<ItemGroup>
   <PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="4.0.0-2.21275.18">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
   </PackageReference>
</ItemGroup>

<ItemGroup>
  <Compile Include=".usings" />
</ItemGroup>

Then, you can create and update a .usings file, and you are good to go! I owe a debt of gratitude to Khalid Abuhakmeh and his CsharpTenFeatures repo for assistance. Feel free to refer to that project if you have issues getting the latest tools.

What does this mean for APIs in ASP.NET Core?

If you're new to building APIs in ASP.NET Core, this is likely a welcome improvement. You can worry about building APIs and not all the overhead that comes with MVC.

If you've developed ASP.NET Core APIs for a while, like me, you may be greeting this with both excitement and skepticism. This is great, but does it fit the needs of a production-scale API? And when it does, will it be hard to move over to the robust capabilities of ASP.NET Core MVC?

With Minimal APIs, the goal is to move out core API building capabilities—the ones that only exist in MVC today—and allow them to be used outside of MVC. When extracting these components away to a new paradigm, you can rely on middleware-like performance. Then, if you need to move from inline lambdas to MVC and its classes and controllers, the ASP.NET team plans to provide a smooth migration for you. These are two different roads with a bridge between them.

If you think long-term, Minimal APIs could be the default way to build APIs in ASP.NET Core—in most cases, it's better to start off small and then grow, rather than starting with MVC and not leveraging all its capabilities. Once you need it, it'll be there.

Of course, we've only scratched the service in all you can do with Minimal APIs. I'm interested in what you've built with them. What are your thoughts? Leave a comment below.

ASP.NET CoreAPIs