From now on, we can create minimal APIs in NET6 that allows us to write in few lines of code powerful APIs. In this post, I collect all my understanding about this new powerful feature.
The source code of this post is available on GitHub.
What Are Minimal APIs?
So, the core idea behind minimal APIs is to remove some of the ceremony of creating simple APIs. It means defining lambda expressions for individual API calls. For example, this is as simple as it gets:
app.MapGet("/", () => "Hello World!");
This code specifies a route (e.g., “/”) and a callback to execute once a request that matches the route and verb are matched. The method MapGet
is specifically to map a HTTP GET
to the callback function. So, much of the magic is in the type inference that’s happening. When we return a string (like in this example), it’s wrapping that in a HTTP code 200
(e.g., OK) return result.
How do you even call this? Effectively, these mapping methods are exposed. They’re extension methods on the IEndpointRouteBuilder
interface. This interface is exposed by the WebApplication
class that’s used to create a new Web server application in .NET 6.
The New Program.cs
Now, a lot has been written about the desire to take the boilerplate out of the startup experience in C# in general. To this end, Microsoft has added something called “Top Level Statements” to C# 10. This means that the program.cs
that you rely on to start your Web applications don’t need a void Main()
to bootstrap the app. It’s all implied. Before C# 10, a startup looked something like this:
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace Client.Api
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder
CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
The need for a class and a void Main
method that bootstraps the host to start the server is how we’ve been writing ASP.NET in the .NET Core way for a few years now. Now, in minimal APIs in NET6 there is the introduction of top-level statements, they want to streamline this boilerplate, as seen below:
var builder = WebApplication.CreateBuilder(args);
// Setup Services
var app = builder.Build();
// Add Middleware
// Start the Server
app.Run();
Instead of a Startup
class with places to set up services and middleware, it’s all done in this very simple top-level program. What does this have to do with Minimal APIs? The app that the builder object builds support the IEndpointRouteBuilder
interface. So, in our case, the set up to the APIs is just the middleware:
var builder = WebApplication.CreateBuilder(args);
// Setup Services
var app = builder.Build();
// Map APIs
app.MapGet("/", () => "Hello World!");
// Start the Server
app.Run();
Routing
The first thing you might notice is that the pattern for mapping API calls looks a lot like MVC controllers’ pattern matching. This means that Minimal APIs look a lot like controller methods. For example:
app.MapGet("/api/clients", () => new Client()
{
Id = 1,
Name = "Client 1"
});
app.MapGet("/api/clients/{id:int}", (int id) => new Client()
{
Id = id,
Name = "Client " + id
});
Simple paths like the /api/clients
point at simple URI paths, whereas using the parameter syntax (even with constraints) continues to work. Notice that the callback can accept the ID that’s mapped from the URI just like MVC controllers. One thing to notice in the lambda expression is that the parameter types are inferred (like most of C#). This means that because you’re using a URL parameter (e.g., id
), you need to type the first parameter. If you didn’t type it, it would try to guess the type in the lambda expression:
app.MapGet("/api/clients/{id:int}", (id) => new Client()
{
Id = id, // Doesn't Work
Name = "Client " + id
});
This doesn’t work because without the hint of type, the first parameter of the lambda expression is assumed to be an instance of HttpContext
. That’s because, at its lowest level, you can manage your own response to any request with the context object. But for most of you, you’ll use the parameters of the lambda expression to get help in mapping objects and parameters.
Using Services
So far, the APIs calls you’ve seen aren’t anything like real world. In most of those cases, you want to be able to use common services to execute calls. This brings me to how to use Services in Minimal APIs in NET6. You may have noticed earlier that I’d left a space to register services before I built the WebApplication
:
var builder = WebApplication.CreateBuilder(args);
// Register services here
var app = builder.Build();
You can just use the builder object to access the services, like so:
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddDbContext<ClientContext>();
builder.Services.AddTransient<IClientRepository, ClientRepository>();
var app = builder.Build();
Here you can see that you can use the Services
object on the application builder to add any services you need (in this case, I’m adding an Entity Framework Core context object and a repository that I’ll use to execute queries. To use these services, you can simply add them to the lambda expression parameters to use them:
app.MapGet("/clients", async (IClientRepository repo) => {
return await repo.GetClientsAsync();
});
By adding the required type, it will be injected into the lambda expression when it executes. This is unlike controller-based APIs in that dependencies are usually defined at the class level. These injected services don’t change how services are handled by the service layer (i.e., Minimal APIs still create a scope for scoped services). When you’re using URI parameters, you can just add the services required to the other parameters. For example:
app.MapGet("/clients/{id:int}", async (int id, IClientRepository repo) => {
return await repo.GetClientAsync(id);
});
This requires you think about the services you require for each API call separately. But it also provides the flexibility to use services at the API level.
Verbs
So far, all I’ve looked at are HTTP GET APIs. There are methods for the different types of verbs. These include:
- MapPost
- MapPut
- MapDelete
These methods work identically to the MapGet
method. For example, take this call to POST
a new client:
app.MapPost("/clients", async (Client model, IClientRepository repo) =>
{
// ...
});
Notice that the model in this case doesn’t need to use attributes to specify FromBody. It infers the type if the shape matches the type requested. You can mix and match all of what you might need (as seen in MapPut
):
app.MapPut("/clients/{id}", async (int id, ClientModel model,
IClientRepository repo) =>
{
// ...
});
For other verbs, you need to handle mapping of other verbs using MapMethods:
app.MapMethods("/clients", new [] { "PATCH" },
async (IClientRepository repo) => {return await repo.GetClientsAsync();
});
Notice that the MapMethods
method takes a path, but also takes a list of verbs to accept. In this case, I’m executing this lambda expression when a PATCH verb is received. Although you’re creating APIs separately, most of the same code that you’re familiar with will continue to work. The only real change is how the plumbing finds your code.
Using HTTP Status Codes
In these examples, so far, you haven’t seen how to handle different results of an API action. In most of the APIs I write, I can’t assume that it succeeds, and throwing exceptions isn’t the way that I want to handle failure. To that end, you need a way of controlling what status codes to return. These are handled with the Results
static class. You simply wrap your result with the call to Results
and the status code:
app.MapGet("/clients", async (IClientRepository repo) => {
return Results.Ok(await repo.GetClientsAsync());
});
Results supports most status codes you’ll need, like:
- Results.Ok: 200
- Results.Created: 201
- Results.BadRequest: 400
- Results.Unauthorized: 401
- Results.Forbid: 403
- Results.NotFound: 404
- Etc.
In a typical scenario, you might use several of these:
app.MapGet("/clients/{id:int}", async (int id, IClientRepository repo) => {
try {
var client = await repo.GetClientAsync(id);
if (client == null)
{
return Results.NotFound();
}
return Results.Ok(client);
}
catch (Exception ex)
{
return Results.BadRequest("Failed");
}
});
If you’re going to pass in a delegate to the MapXXX
classes, you can simply have them return an IResult
to require a status code:
app.MapGet("/clients/{id:int}", HandleGet);
async Task<IResult> HandleGet(int id, IClientRepository repo)
{
try
{
var client = await repo.GetClientAsync(id);
if (client == null) return Results.NotFound();
return Results.Ok(client);
}
catch (Exception)
{
return Results.BadRequest("Failed");
}
}
Notice that because you’re async
in this example, you need to wrap the IResult
with a Task
object. The resulting return is an instance of IResult
. Although Minimal APIs are meant to be small and simple, you’ll quickly see that, pragmatically, APIs are less about how they’re instantiated and more about the logic inside of them. Both Minimal APIs and controller-based APIs work essentially the same way. The plumbing is all that changes.
Securing Minimal APIs
Although Minimal APIs in NET6 work with authentication and authorization middleware, you may still need a way to specifying, on an API-level, how security should work. If you’re coming from controller-based APIs, you might use the Authorize
attribute to specify how to secure your APIs, but without controllers, you’re left to specify them at the API level. You do this by calling methods on the generated API calls. For example, to require authorization:
app.MapPost("/clients", async (ClientModel model, IClientRepository repo) =>
{
// ...
}).RequireAuthorization();
This call to RequireAuthorization
is tantamount to using the Authorize
filter in controllers (e.g., you can specify which authentication scheme or other properties you need). Let’s say you’re going to require authentication for all calls:
builder.Services.AddAuthorization(cfg => {
cfg.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
You’d then not need to add RequireAuthentication
on every API, but you could override this default by allowing anonymous for other calls:
app.MapGet("/clients", async (IClientRepository repo) =>
{
return Results.Ok(await repo.GetClientsAsync());
}).AllowAnonymous();
In this way, you can mix and match authentication and authorization as you like.
Wrap up
With this post, I introduced minimal APIs in NET6. But it is not finish. There are at least other 2 important thing to learn:
- add Swagger to the project
- how to test the APIs
Now, you find on GitHub the full source code of this post with already Swagger installed and 2 test projects: one for xUnit and one for NUnit. In the next posts, I will explain how.