Polymorphic serialization via System.Text.Json in ASP.NET Core Minimal API dotnet aspnetcore minimal-api
How to add Health Checks to ASP.NET Core project. A coding story. dotnet aspnetcore coding-stories
How to add OpenAPI to ASP.NET Core project. A coding story. dotnet aspnetcore coding-stories

TL;DR

In this blog post, I share my thoughts on how to organize Minimal API projects to keep code structure under control and still get benefits from the low-ceremony approach.


Introduction

Minimal API is a refreshing and promising application model for building lightweight Web APIs. Now you can create a microservice and start prototyping without the necessity to create lots of boilerplate code and worrying about too much about code structure.

var app = WebApplication.Create();
app.MapGet("/", () => "Hello World!");
app.Run();

Presumably, this kind of style gives you a productivity boost and flattens the learning curve for newcomers. So it is considered as a more lightweight version, but using Minimal API doesn’t mean you have to write small applications. It is rather a different application model that one day will be as much powerful as MVC counterpart.

Problem Statement

One of the problems with Minimal API is that Program.cs can get to big. So initial simplicity may lead you to the big ball of mud type of solution. At this point, you want to use refactoring techniques and my goal is to share some ideas on how to tackle emerging challenges.

Example: Building Minimal API

I’ve prepared a demo application. I strongly recommend checking it before you move further.

Source code can be found at GitHub: NikiforovAll/minimal-api-example

minimal-api-banner

Recommendations

My general recommendation is to write something that may be called Modular Minimal API or Vertical Slice Minimal API.

Keep Program.cs aka Composition Root small

A Composition Root is a unique location in an application where modules are composed together. You should have a good understanding of what this application is about just by looking at it.

You want to keep Program.cs clean and focus on high-level modules.

var builder = WebApplication.CreateBuilder(args);

builder.AddSerilog();
builder.AddSwagger();
builder.AddAuthentication();
builder.AddAuthorization();
builder.Services.AddCors();
builder.AddStorage();

builder.Services.AddCarter();

var app = builder.Build();
var environment = app.Environment;

app
    .UseExceptionHandling(environment)
    .UseSwaggerEndpoints(routePrefix: string.Empty)
    .UseAppCors()
    .UseAuthentication()
    .UseAuthorization();

app.MapCarter();

app.Run();

πŸ’‘Tip: One of the techniques you can apply here is to create extension methods for IServiceCollection, IApplicationBuilder. For Minimal API I would suggest using β€œfile-per-concern” organization. See ApplicationBuilderExtensions and ServiceCollectionExtensions folders.

$ tree
.
β”œβ”€β”€ ApplicationBuilderExtensions
β”‚   β”œβ”€β”€ ApplicationBuilderExtensions.cs
β”‚   └── ApplicationBuilderExtensions.OpenAPI.cs
β”œβ”€β”€ assets
β”‚   └── run.http
β”œβ”€β”€ Features
β”‚   β”œβ”€β”€ HomeModule.cs
β”‚   └── TodosModule.cs
β”œβ”€β”€ GlobalUsing.cs
β”œβ”€β”€ MinimalAPI.csproj
β”œβ”€β”€ Program.cs
β”œβ”€β”€ Properties
β”‚   └── launchSettings.json
β”œβ”€β”€ ServiceCollectionExtensions
β”‚   β”œβ”€β”€ ServiceCollectionExtensions.Auth.cs
β”‚   β”œβ”€β”€ ServiceCollectionExtensions.Logging.cs
β”‚   β”œβ”€β”€ ServiceCollectionExtensions.OpenAPI.cs
β”‚   └── ServiceCollectionExtensions.Persistence.cs
└── todos.db

And here is an example of how to add OpenAPI/Swagger concern:

namespace Microsoft.Extensions.DependencyInjection;

using Microsoft.OpenApi.Models;

public static partial class ServiceCollectionExtensions
{
    public static WebApplicationBuilder AddSwagger(this WebApplicationBuilder builder)
    {
        builder.Services.AddSwagger();

        return builder;
    }

    public static IServiceCollection AddSwagger(this IServiceCollection services)
    {
        services.AddEndpointsApiExplorer();
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo()
            {
                Description = "Minimal API Demo",
                Title = "Minimal API Demo",
                Version = "v1",
                Contact = new OpenApiContact()
                {
                    Name = "Oleksii Nikiforov",
                    Url = new Uri("https://github.com/nikiforovall")
                }
            });
        });

        return services;
    }
}

Organize endpoints around features

A MinimalApiPlayground from Damian Edwards is a really good place to start learning more about Minimal API, but things start to get hairy (https://github.com/DamianEdwards/MinimalApiPlayground/blob/main/src/Todo.Dapper/Program.cs). Functionality by functionality you turn into a scrolling machine more and more - no good πŸ˜›. It means we need to organize code into manageable components/modules.

Modular approach allows us to focus on cohesive units of functionality. Luckily, there is an awesome open source project - Carter. It supports some essential missing features (Minimal API .NET 6) and one of them is module registration ICarterModule.

namespace MinimalAPI;

using Dapper;
using Microsoft.Data.Sqlite;

public class TodosModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("/api/todos", GetTodos);
        app.MapGet("/api/todos/{id}", GetTodo);
        app.MapPost("/api/todos", CreateTodo);
        app.MapPut("/api/todos/{id}/mark-complete", MarkComplete);
        app.MapDelete("/api/todos/{id}", DeleteTodo);
    }
    private static async Task<IResult> GetTodo(int id, SqliteConnection db) =>
        await db.QuerySingleOrDefaultAsync<Todo>(
            "SELECT * FROM Todos WHERE Id = @id", new { id })
            is Todo todo
                ? Results.Ok(todo)
                : Results.NotFound();

    private async Task<IEnumerable<Todo>> GetTodos(SqliteConnection db) =>
        await db.QueryAsync<Todo>("SELECT * FROM Todos");

    private static async Task<IResult> CreateTodo(Todo todo, SqliteConnection db)
    {
        var newTodo = await db.QuerySingleAsync<Todo>(
            "INSERT INTO Todos(Title, IsComplete) Values(@Title, @IsComplete) RETURNING * ", todo);

        return Results.Created($"/todos/{newTodo.Id}", newTodo);
    }
    private static async Task<IResult> DeleteTodo(int id, SqliteConnection db) =>
        await db.ExecuteAsync(
            "DELETE FROM Todos WHERE Id = @id", new { id }) == 1
            ? Results.NoContent()
            : Results.NotFound();
    private static async Task<IResult> MarkComplete(int id, SqliteConnection db) =>
        await db.ExecuteAsync(
            "UPDATE Todos SET IsComplete = true WHERE Id = @Id", new { Id = id }) == 1
            ? Results.NoContent()
            : Results.NotFound();
}

public class Todo
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public bool IsComplete { get; set; }
}

πŸ’‘Tip: You can use Method Group (C#) instead of lambda expression to avoid formatting issues and keep code clean. Also, it provides automatic endpoint metadata aspnetcore#34540, that’s cool.

// FROM
app.MapGet("/todos", async (SqliteConnection db) =>
    await db.QueryAsync<Todo>("SELECT * FROM Todos"));

// TO
app.MapGet("/api/todos", GetTodos);

async Task<IEnumerable<Todo>> GetTodos(SqliteConnection db) =>
        await db.QueryAsync<Todo>("SELECT * FROM Todos");

To register modules you simply need to add two lines of code in Program.cs. Modules are registered based on assemblies scanning and added to DI automatically, see. You can go even further and split Carter modules into separate assemblies.

builder.Services.AddCarter();
// ...
app.MapCarter();

I recommend you to enhance your Minimal APIs with Carter because it tries to close the gap between Minimal API and full-fledged ASP.NET MVC version. Go check out Carter on GitHub, give them a Star, try it out!

High cohesion

Modules go well together with Vertical Slice Architecture. Simply start with ./Features folder and keep related models, services, factories, etc. together.

Conclusion

Minimal API doesn’t mean your application has to be small. In this blog post, I’ve shared some ideas on how to handle project complexity. Personally, I like this style and believe that one day Minimal API will be as much powerful as ASP.NET MVC.


Reference


Oleksii Nikiforov

Jibber-jabbering about programming and IT.