What are Minimal APIs in ASP.NET Core 6

What is a Minimal API?

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.

How to create Minimal API’s?

We can create minimal API’s either with Visual Studio 2022 or dotnet CLI.

With dotnet CLI

dotnet new webapi -minimal -o TodoApi
cd TodoApi
code -r ../TodoApi

With Visual Studio 2022

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.

Setup wizard for minimal API's in Visual Studio 2022
Figure 1: setup minimal API’s in Visual Studio 2022

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.

Where are 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.

Depicting the ConfigureServices and Configure methods in Program.cs in Minimal API's
Figure 2: Depicting the ConfigureServices and Configure methods in Program.cs

Ok, where is the main entry point?

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

Intermediate Language (IL) showing Main method in Minimal API
Intermediate Language showing the Main method in Minimal API

Setting up Repository for minimal API

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

  1. We can get a user by their Id
  2. Update the user’s first name, last name, and also user name
  3. Delete a user by Id

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.

Writing a minimal API

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.

Demo of minimal apis on coderethinked.com
Demo: Minimal API demo

Full Program.cs code

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();

Limitations of Minimal API’s

Minimal APIs also has few limitations.

  • * No support for filters. Yes, we cannot use exception filters, result filters, action filters etc
  • No built-in support for validation (IModelValidator). If you’ve been using 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.
  • No support for OData, JsonPatch, ApiVersioning

Filters were added to Minimal API’s in ASP.NET Core 7.0.

When to use Minimal API’s?

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.

References