Luna Tech

Tutorials For Dummies.

ASP.NET Core Minimal API

2021-11-21


0. Intro

.NET 6 has shipped a new feature to build API called Minimal APIs, today we’re going to explore this new feature with some simple demos, discuss the difference between the minimal way and the traditional way (using controller to build APIs), and when to use which approach in terms of building your next web API project.

Prerequisite

  1. Install .NET 6 SDK
  2. Install Visual Studio 2022

1. Create a Web API project

Pre .NET 6 - The Controller Approach

We will select to create an ASP.NET Core Web API project with an example controller for the RESTful HTTP service.

We can select target framework, authentication type and other options.

These are the files created for this template.

If we run the project, it has a nice swagger interface displaying the available APIs.

While this looks pretty nice, the code itself does seem to be quite complicated, you’ve got the standard Program.cs and Startup.cs file with some standard setups, then you’ve got your model somewhere, a dedicated controller to map all the routes related to that model.

This file is not very user-friendly tbh, it has many unused using statements and requires a learning curve to understand what the code is doing.

.NET 6 - The Controller Approach

If you go with the controller approach in .NET 6 framework, you will notice that the Startup.cs file is gone and the Program.cs file looks much cleaner and relevant to the API itself, I can see I’m using swagger OpenAPI because I ticked the box when creating this project. Every line of code is readable and understandable.

Note: This change is actually called a new Hosting Model which utilizes the top-level statement feature introduced in C# 9.0, the old Hosting Model will continue to work if you upgrade your ASP.NET Core project to .NET 6.0.

And in VS2022, many projects templates have changed to this new Hosting Model.

.NET 6 - The Minimal API Approach

When you create an ASP.NET Core Web API with .NET 6 framework in VS 2022, you can see there’s an extra tickbox for you to choose whether you want to go with the traditional controller approach or the minimal API approach.

Note: You still have the authentication, https, docker and swagger support with the minimal api approach.

Super Minimal

If you want to go super minimal, you can choose to create an ASP.NET Core Empty project. Your project will only one api endpoint, you can add model, swagger, authentication, data access layer as needed.

This template is suitable for creating quick PoC projects, mock APIs and microservices.

You can also use the command dotnet new webapp -o [projectName] to create this type of minimal API app.


2. Basics

Enable Hot Reload

You can use dotnet watch run to run this project, or use visual studio to run it.

Note: You still need to refresh the page with the hot reload turned on.

Change Port

Application port is defined in the launchSettings.json file, you can also change environment here, by default it is set to “Development”.

Route Variables

You can access the variable directly, or use HttpContext (explained later).

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hello/{name}", (string name) => $"Hello, {name}");

app.Run();

Route Constraint

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/todos/{name}", (string name) => $"The Todo name is {name}");
app.MapGet("/todos/{id:int}", (int id) => $"The Todo id is {id}");

app.Run();

Note: invalid type will throw 404 error, this is only used to disambiguate similar routes, not for user input validation.

HttpContext

Using HttpContext gives you access to all request information, like the body and cookies. You can read from the request property of HttpContext and write to the Response property.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hi/{name}", (HttpContext ctx) => $"Hi, {ctx.Request.RouteValues["name"]}");

app.Run();

Model Binding

Create a record (an immutable data type).

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

List<Todo> todos = new List<Todo>()
{
    new Todo("1", "Todo 1", false),
    new Todo("2", "Todo 2", false),
    new Todo("3", "Todo 3", false)
};

app.MapGet("/todos", () => todos);
app.MapGet("/todos/{id}", (string id) => todos.Find(t => t.id == id));

app.Run();

internal record Todo(string id, string text, bool isCompleted);

3. Add Swagger/OpenAPI

dotnet add package Swashbuckle.AspNetCore

Navigate to https://localhost:xxxx/swagger to check the API docs.

using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo API", Description = "Keep track of your tasks", Version = "v1" });
});


var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "Todo API V1");
    });
}

app.MapGet("/todos/{name}", (string name) => $"The Todo name is {name}");
app.MapGet("/todos/{id:int}", (int id) => $"The Todo id is {id}");

app.Run();

4. Http methods

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<TodoRepository>();
var app = builder.Build();

app.MapGet("/todos", ([FromServices] TodoRepository repo) =>
{
    return repo.GetAll();
});

app.MapGet("/todos/{id}", ([FromServices] TodoRepository repo, string id) =>
{
    var todo = repo.GetById(id);
    return todo is not null ? Results.Ok(todo) : Results.NotFound();
});

app.MapPost("/todos", ([FromServices] TodoRepository repo, Todo todo) =>
{
    repo.Create(todo);
    return Results.Created($"/todos/{todo.id}", todo);
});

app.MapPut("/todos/{id}", ([FromServices] TodoRepository repo, string id, Todo todo) =>
{
    var existingTodo = repo.GetById(id);
    if (existingTodo is not null)
    {
        repo.Update(todo);
        return Results.Ok(todo);
    }
    return Results.NotFound();
});

app.MapDelete("/todos/{id}", ([FromServices] TodoRepository repo, string id) =>
{
    var existingTodo = repo.GetById(id);
    if (existingTodo is not null)
    {
        repo.Delete(id);
        return Results.Ok();
    }
    return Results.NotFound();
});

app.Run();

// Model
internal record Todo(string id, string item, bool isCompleted);

// Logic
class TodoRepository
{
    private readonly Dictionary<string, Todo> _todos = new Dictionary<string, Todo>()
    {
        {"1", new Todo("1", "todo 1", false) },
        {"2", new Todo("2", "todo 2", false) },
        {"3", new Todo("3", "todo 3", true) }
    };

    public void Create(Todo todo)
    {
        if (todo is null || _todos.ContainsKey(todo.id))
        {
            return;
        }
        _todos[todo.id] = todo;
    }

    public Todo GetById(string id)
    {
        if (_todos.ContainsKey(id))
            return _todos[id];
        return null;
    }

    public List<Todo> GetAll()
    {
        return _todos.Values.ToList();
    }

    public void Update(Todo todo)
    {
        var prev = GetById(todo.id);
        if (prev is null)
        {
            return;
        }
        _todos[todo.id] = todo;
    }

    public void Delete(string id)
    {
        _todos.Remove(id);
    }
}

5. Compare Minimal APIs and APIs with controllers

Minimal APIs are architected to create HTTP APIs with minimal dependencies. They are ideal for microservices and apps that want to include only the minimum files, features, and dependencies in ASP.NET Core.

Pros:

Cons: Minimal APIs are still at an early stage and don’t fully support some functions, such as filters, model binding, binding from forms, validation, ApiVersioning and so on.

Carter is a library that allows Nancy-esque routing for use with ASP.Net Core. It can also be used to create simple HTTP APIs.


6. A Simple Demo App

GitHub Link


References