The goal of Minimal APIs in ASP.NET Core is to create HTTP API’s with minimum dependencies.
In the world of microservices, the backend API’s are becoming way too smaller and easier to work with. The minimal API’s may come in handy when working with small API’s which has only a few dependencies.
We can create minimal API’s either with Visual Studio 2022 or dotnet CLI.
dotnet new webapi -minimal -o TodoApi cd TodoApi code -r ../TodoApi
I’m not writing a step-by-step guide on how to create minimal APIs on visual studio 2022 as Microsoft documentation has those steps already.
But, an important step when creating ASP.NET Core Web API is to select .NET 6 framework
and uncheck the "Use controller (uncheck to use minimal APIs)"
option in the Additional information dialog.
Notice we also have an option not to use the top-level statements checkbox here. If we check the "Do not use top-level statements"
checkbox, we will have the usual Program.cs
file with Main
method in it instead of top-level statements.
Once we create a minimal API, we will have the default weather app which generates 5 random weather numbers for us.
Here is what the Program.cs
file looks like with minimal API.
var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; app.MapGet("/weatherforecast", () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); return forecast; }) .WithName("GetWeatherForecast"); app.Run(); internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); }
Within the Program.cs
file, we see swagger is added to services, and swagger UI is also added to the request pipeline.
We can run and see if we want.
ConfigureServices()
and Configure()
methods?If you are not aware of these methods. The ConfigureServices()
and Configure()
methods are called by the ASP.NET Core runtime when the app starts. But, with minimal API’s these methods are removed and we can write our dependencies/middleware directly in the Program.cs
.
If you want to register any services to the container then you’ve to add them after the builder is created with CreateBuilder(args)
method but should not be after the builder.Build()
. Assume that this is our old ConfigureServices method.
And if you want to add anything to the request pipeline like our old Configure method, you’ve to configure them with after builder.Build()
but before app.Run()
. Assume that this is our old Configure method.
The Program.cs
file now does not have any Main
method. This is because of the new language feature introduced as part of C# 9: Top-Level statements (we could configure the minimal APIs to generate the Main method by checking the Do not use top-level statements
checkbox mentioned in Figure 1).
Behind the scenes, the compiler will inject the code you wrote in Program.cs
into the main method for us.
Here is the Intermediate Language (IL) generated for Program.cs file in ILDASM
Before we write minimal API’s let’s build a User repository.
public interface IUserRepository { User GetUser(int id); bool UpdateUser(int id, User user); void DeleteUser(int id); } public class UserRepository : IUserRepository { private List<User> _users = new List<User>(); public UserRepository() { GenerateSeedData(); } public User GetUser(int id) { return _users.FirstOrDefault(u => u.Id == id); } public bool UpdateUser(int id, User user) { var dbUser = _users.FirstOrDefault(u => u.Id == id); if (dbUser is null) return false; dbUser.UserName = user.UserName; dbUser.FirstName = user.FirstName; dbUser.LastName = user.LastName; return true; } public void DeleteUser(int id) { var user = _users.FirstOrDefault(u => u.Id == id); if (user is null) return; _users.Remove(user); } private void GenerateSeedData() { _users = new Faker<User>() .StrictMode(true) .RuleFor(u => u.Id, f => f.IndexFaker) .RuleFor(u => u.UserName, f => f.Person.UserName) .RuleFor(u => u.FirstName, f => f.Person.FirstName) .RuleFor(u => u.LastName, f => f.Person.LastName) .Generate(50); } }
Our user repository has 3 basic operations
With the Faker class, we generated seed data for our repository. Faker class is part of Bogus library and Bogus is a fake data generator for C#. If you need more information about Bogus and how to generate fake data, take a look at this article.
Now, let’s write our minimal API’s for user operations. Ideally, we’d have managers to have some business logic and also call the repositories. But, for this article let’s wire the repository directly to the APIs.
app.MapGet("/user/{id}", (int id, IUserRepository userRepository) => { var user = userRepository.GetUser(id); if (user is null) return Results.BadRequest(); return Results.Ok(user); }).WithName("GetUsers"); app.MapPut("/user/update/{id}", (int id, User user, IUserRepository userRepository) => { return userRepository.UpdateUser(id, user); }).WithName("UpdateUsers"); app.MapDelete("/user/{id}", (int id, IUserRepository userRepository) => { userRepository.DeleteUser(id); return Results.Ok(); }).WithName("DeleteUser");
We have different extension methods for GET (MapGet), POST (MapPost), PUT (MapPut), DELETE (MapDelete) HTTP verbs. All these extension methods take the URL as the first argument and a Delegate as a second parameter. These methods are like our action methods in the MVC world.
Notice we are adding the IUserRepository
to every endpoint here. The IUserRepository
will be resolved with dependency injection as one would expect.
We also see Results.Ok()
, Results.BadRequest()
methods in our API’s. Results
is a static helper class that exits in Microsoft.AspNetCore.Http
namespace which provides basic return types for API’s. Here is the list of built-in results in the Results class.
At the end of the API, we have .WithName
method which is optional here. The argument you provide here will be operationId
in the swagger.json
file for each of the operations.
Now, Let’s run and see how they all work.
Here’s the Program.cs
file after adding our user endpoints.
using MinimalAPIs.Repositories; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // Add services to the container. builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddSingleton<IUserRepository, UserRepository>(); var app = builder.Build(); app.MapControllers(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.MapGet("/user/{id}", (int id, IUserRepository userRepository) => { var user = userRepository.GetUser(id); if (user is null) return Results.BadRequest(); return Results.Ok(user); }).WithName("GetUsers"); app.MapPut("/user/update/{id}", (int id, User user, IUserRepository userRepository) => { return userRepository.UpdateUser(id, user); }).WithName("UpdateUsers"); app.MapDelete("/user/{id}", (int id, IUserRepository userRepository) => { userRepository.DeleteUser(id); return Results.Ok(); }).WithName("DeleteUser"); app.Run();
Minimal APIs also has few limitations.
ModelState.IsValid
on your controller actions then it won’t work with Minimal API’s. You have to write your own validators to validate the input. Filters were added to Minimal API’s in ASP.NET Core 7.0.
As you can see in this post, we’ve come far from the MVC mindset here. We don’t see any controllers or action methods that typically create our endpoints in the Web API’s.
If you are starting with a brand new microservice that is fairly small, then it’s better to start with minimal API.
But, if you think your backend is going to have more endpoints in the future like an enterprise backend then using controllers might be a better choice.
Karthik is a passionate Full Stack developer working primarily on .NET Core, microservices, distributed systems, VUE and JavaScript. He also loves NBA basketball so you might find some NBA examples in his posts and he owns this blog.
In this post, we’ll see how to test gRPC Server applications using different clients. And… Read More
In this post, we'll create a new gRPC project in ASP.NET Core and see what's… Read More
In this blog post, we’ll see how to run dotnet core projects without opening visual… Read More
Programmatically evaluating policies is useful when we want to provide access or hide some data… Read More
We saw how we could set up policy-based authorization in our previous article. In this… Read More
What is policy-based authorization and how to set up policy-based authorization with handlers and policies… Read More
This website uses cookies.