Minimal APIs: Beyond the Basics
I attended the virtual .NET Conf this year and found out about some of the newer offerings for Microsoft developers. I liked the looks of the Minimal API for building lightweight REST HTTP apps. So, I decided to convert one our legacy ASP.NET 4.0 web services to an ASP.NET Core 8.0 Minimal API.
I kicked off the process by going through the To Do List and Pizza Store tutorials. They taught me how to establish the entity framework database connection, add OpenAPI documentation with Swagger, and create routes that respond to GET and POST requests.
This was a great starting foundation. It was amazing how quickly, and with how few lines of code, I could be returning pretty-printed JSON from the database and auto-generating this gorgeous testing interface!
I needed to go just a few steps further beyond the basics in order for my app to be ready. Here’s a summary of the things I did next, which aren’t in either of the sample projects.
1. Error Messaging
It’s a good idea to introduce error management first, because most of the bugs pop up early in the development process. After reading about how to Handle errors in ASP.NET Core web APIs, I added this to my Program.cs file:
builder.Services.AddProblemDetails();
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
...
app.UseStatusCodePages(statusCodeHandlerApp =>
{
statusCodeHandlerApp.Run(async httpContext =>
{
var pds = httpContext.RequestServices.GetService<IProblemDetailsService>();
if (pds == null
|| !await pds.TryWriteAsync(new() { HttpContext = httpContext }))
{
await httpContext.Response.WriteAsync("Fallback: An error occurred.");
}
});
});
This will cause error messages for handled exceptions to be returned in the JSON response. In cases where the name of a route is typed wrong, a 404 “not found” error is generated. If you add the code above, you will be saved from looking like your whole application has gone missing.
What about unhandled exceptions? Those generate a helpful developer exception page in development.
By default, you will not see that page in production, instead, you will get a scary crash page. If you add this code to Program.cs, you can control what you see in production, and make it look like the handled exceptions do:
if (app.Environment.IsProduction())
{
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async httpContext =>
{
var pds = httpContext.RequestServices.GetService<IProblemDetailsService>();
if (pds == null
|| !await pds.TryWriteAsync(new() { HttpContext = httpContext }))
{
await httpContext.Response.WriteAsync("Fallback: An error occurred.");
}
});
});
}
2. JSON Serialization
One of the first bugs I encountered was caused by the legacy project’s use of capital letters in the object properties. In my entity framework models, I put all of the database column names in upper case, because I needed them to be that way in the JSON response object.
They were getting automatically converted to lower case somewhere in the pipeline, and then all the legacy JavaScript code in the website that was consuming this service didn’t recognize them!
Thankfully, I found out this was being caused by the JSON serializer, and there was a setting I could change to make it leave the case alone:
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
{
//Doesn't consider case when looking for a matching property
options.SerializerOptions.PropertyNameCaseInsensitive = true;
//Keeps object property names the same case as they are defined in the model
options.SerializerOptions.PropertyNamingPolicy = null;
//Pretty prints the output in the browser! :)
options.SerializerOptions.WriteIndented = true;
});
A list of all the JSON options that can be changed is here.
3. CORS Options
When I tried to call my service from another web server, I got a CORS error. Previously, I learned and wrote about how to enable CORS in IIS. This project taught me how to enable it in ASP.NET. If you want to open everything up, you can just do builder.Services.AddCors() & app.UseCors().
I decided to refine things a bit, and define different policies to use in development and production:
builder.Services.AddCors(options =>
{
options.AddPolicy(name: "OpenPolicy",
policy =>
{
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
});
options.AddPolicy(name: "RestrictedPolicy",
policy =>
{
policy.WithOrigins("https://server.com", "https://anotherserver.com")
.AllowAnyHeader()
.WithMethods("GET", "HEAD", "POST", "OPTIONS");
});
...
if (app.Environment.IsDevelopment())
{
app.UseCors("OpenPolicy");
}
if (app.Environment.IsProduction())
{
app.UseCors("RestrictedPolicy");
}
4. Output Caching
At this point, I asked some coworkers to test the website that was calling the new service, and everything was looking good. But when I decided to write this article, I revisited the documentation. After I read the summaries of all the different middleware components I could use, I decided to fancy it up some more!
I thought a little caching couldn’t hurt. It would speed things up and reduce the database load. There is response caching and output caching available. I can’t tell exactly what the difference is. I chose to try output caching because it’s newer (released with ASP.NET Core 7) and it looked more simple to implement.
This code is all you need to add a cache:
builder.Services.AddOutputCache(options =>
{
options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(30);
});
...
app.UseOutputCache();
...
app.MapGet("/route", (string? myParameter) =>
{
return MyFunction(myParameter);
}).WithOpenApi().CacheOutput();
If the user makes a request to the same route with the same input parameters within a 30 second time period, the database will not be queried again, and the stored results will be returned instantly!
5. Separate Routing File
One of the things that make Minimal APIs attractive is that they don’t need a controller. All the routes can be defined concisely in the main Program.cs file. But after I added all the neat stuff I described above, that file had become rather large. When I saw it is possible to move the routes to a different location, I created an Endpoints.cs class and moved them there:
namespace MyWebService
{
public static class Endpoints
{
public static void Map(WebApplication app)
{
//all my routes
app.MapGet()
app.MapPost()
...
}
}
}
Here’s how I reference it in my Program.cs code:
using MyWebService;
...
Endpoints.Map(app);
app.Run();
It’s almost like I have a controller back! I like the separation of concerns and better organization of doing it that way.
My new improved web service looks better and runs faster than the old one (even without the cache).
I am glad I discovered Minimal APIs and I am excited to learn about more technical advances at future Microsoft conferences!