Building REST APIs has become central to many development projects. The choice for building those projects is wide, but if you're a C# developer, the options are more limited. Controller-based APIs have been the most common for a long time, but .NET 6 changes that with a new option. Let's talk about it.
How'd We Get Here?
Connecting computers has been a problem since the first steps of distributed computing some fifty years ago (see Figure 1). Yeah, that makes me feel old too. Remote Procedure Calls were as important as APIs in modern development. With REST, OData, GraphQL, GRPC, and the like, we have a lot of options to create ways of communicating between apps.
Although lots of these technologies are thriving, using REST as a way of communicating is still a stalwart in today's development world. Microsoft has had a number of solutions for creating REST APIs over the years, but for the past decade or so, Web API has been the primary tool. Based on the ASP.NET MVC framework, Web API was meant to treat REST verbs and nouns as first-class citizens. Being able to create a class that represents a surface area (often tied to a “noun” in the REST sense) that's tied together with a routing library is still a viable way to build APIs in today's world.
One of the drawbacks to the Web API framework (e.g., controllers), is that there's a bit of ceremony involved for small APIs (or microservices). For example, controllers are classes that represent one or more possible calls to an API:
[Route("api/[Controller]")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class OrdersController : Controller
{
readonly IDutchRepository _repository;
readonly ILogger<OrdersController> _logger;
readonly IMapper _mapper;
readonly UserManager<StoreUser> _userManager;
public OrdersController(
IDutchRepository repository,
ILogger<OrdersController> logger,
IMapper mapper,
UserManager<StoreUser> userManager)
{
_repository = repository;
_logger = logger;
_mapper = mapper;
_userManager = userManager;
}
[HttpGet]
public IActionResult Get(bool includeItems = true)
{
try
{
var username = User.Identity.Name;
var results = _repository
.GetOrdersByUser(username, includeItems);
return Ok(_mapper
.Map<IEnumerable<OrderViewModel>>(results));
}
catch (Exception ex)
{
_logger.LogError($"Failed : {ex}" );
return BadRequest($"Failed");
}
}
...
This code is typical of Web API controllers. But does that mean it's bad? No. For larger APIs and ones that have advanced needs (e.g., rich authentication, authorization, and versioning), this structure works great. But for some projects, a simpler way to build APIs is really needed. Some of this pressure is coming from other frameworks where building APIs feels smaller, but it's also driven by wanting to be able to design/prototype APIs more quickly.
This need isn't all that new. In fact, the Nancy framework (https://github.com/NancyFx/Nancy) was a C# solution for mapping APIs calls way back then (although it's deprecated now). Even newer libraries like Carter (https://github.com/CarterCommunity/Carter) are trying to accomplish the same thing. Having efficient and simple ways to create APIs is a necessary technique. You shouldn't take Minimal APIs as the “right” or “wrong” way to build APIs. Instead, you should see it as another tool to build your APIs.
Enough talk, let's dig into how it works.
What Are Minimal APIs?
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 call 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. 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 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. But I can't really dig into this without first talking about how the new Startup experience in .NET 6 works.
The core idea behind Minimal APIs is to remove some of the ceremony of creating simple APIs.
The New Startup Experience
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 Juris.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. With 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 supports 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();
Let's talk about the individual features here.
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. You may have noticed earlier that I'd left a space to register services before I built the WebApplication
:
var bldr = WebApplication.CreateBuilder(args);
// Register services here
var app = bldr.Build();
You can just use the builder object to access the services, like so:
var bldr = WebApplication.CreateBuilder(args);
// Register services
bldr.Services.AddDbContext<JurisContext>();
bldr.Services.AddTransient<IJurisRepository, JurisRepository>();
var app = bldr.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 (IJurisRepository 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, IJurisRepository 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, IJurisRepository 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,
IJurisRepository repo) =>
{
// ...
});
For other verbs, you need to handle mapping of other verbs using MapMethods:
app.MapMethods("/clients", new [] { "PATCH" },
async (IJurisRepository 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 (IJurisRepository 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, IJurisRepository 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, IJurisRepository 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 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, IJurisRepository 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:
bldr.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 (IJurisRepository repo) =>
{
return Results.Ok(await repo.GetClientsAsync());
}).AllowAnonymous();
In this way, you can mix and match authentication and authorization as you like.
Using Minimal APIs without Top-Level Functions
This new change to .NET 6 can come to a shock to many of you. You might not want to change your Program.cs
to use Top-Level Functions for all your projects, but can you still use Minimal APIs without having to move to Top-Level Functions. If you remember, earlier in the article I mentioned that most of the magic of Minimal APIs comes from the IEndpointRouteBuilder
interface. Not only does the WebApplication
class support it, but it's also used in the traditional Startup
class you may already be using. When you call UseEndpoints
, the delegate you specify there passes in an IEndpointRouteBuilder, which means you can just call MapGet
:
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/clients", async (IJurisRepository repo) =>
{
return Results.Ok(await repo.GetClientsAsync());
}).AllowAnonymous();
});
}
Although I think that Minimal APIs are most useful for green-field projects or prototyping projects, you can use them in your existing projects (assuming you've upgraded to .NET 6).
Where Are We?
Hopefully, you've seen here that Minimal APIs are a new way to build your APIs without much of the plumbing and ceremony that are involved with controller-based APIs. At the same time, I hope you've seen that as complexity increases, controller-based APIs have benefits as well. I see Minimal APIs as a starting point for creating APIs and, as a project matures, I might move to controller-based APIs. Although it's very new, I think Minimal APIs are a great way of creating your APIs. The patterns and best practices about how to use them will only get answered in time. I hope you can contribute to that conversation!
If you'd like a copy of the code for this article, please visit: https://github.com/shawnwildermuth/codemag-minimalapis.