Minimal APIs allow you to build APIs without the overhead of the complicated MVC solution. Read more to understand how it all works, and what it means for building APIs in ASP.NET Core.
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 it’s 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.
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.
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 an 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.
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
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.
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.
Starting with Preview 4, you can use that release to explore how minimal APIs work, with a couple of caveats:
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
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.
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.
Dave Brock is a software engineer, writer, speaker, open-source contributor and Microsoft MVP. With a focus on Microsoft technologies, Dave enjoys advocating for modern and sustainable cloud-based solutions. He writes regularly at daveabrock.com. To reach Dave, follow him on Twitter, where his dad jokes are a parent.