DEV Community

loading...
Cover image for An opinionated look at Minimal API in .NET 6

An opinionated look at Minimal API in .NET 6

Oleksii Nikiforov
Software Engineer at EPAM. Interested in .NET, TypeScript, Docker, table tennis, and good music.
Originally published at nikiforovall.github.io ・5 min read

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();
Enter fullscreen mode Exit fullscreen mode

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

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();
Enter fullscreen mode Exit fullscreen mode

πŸ’‘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
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

πŸ’‘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");
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

Discussion (2)

Collapse
pjole profile image
pjole • Edited

Which is this color scheme? I like it!

Collapse
nikiforovall profile image
Oleksii Nikiforov Author

It's a GitHub theme, please see: github.com/primer/github-vscode-theme