
Posted on • Originally published atnikiforovall.github.io
An opinionated look at Minimal API in .NET 6
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.
varapp=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 thatProgram.cs
can get to big. So initial simplicity may lead you to thebig 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.
varbuilder=WebApplication.CreateBuilder(args);builder.AddSerilog();builder.AddSwagger();builder.AddAuthentication();builder.AddAuthorization();builder.Services.AddCors();builder.AddStorage();builder.Services.AddCarter();varapp=builder.Build();varenvironment=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 forIServiceCollection
,IApplicationBuilder
. For Minimal API I would suggest using "file-per-concern" organization. SeeApplicationBuilderExtensions andServiceCollectionExtensions 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:
namespaceMicrosoft.Extensions.DependencyInjection;usingMicrosoft.OpenApi.Models;publicstaticpartialclassServiceCollectionExtensions{publicstaticWebApplicationBuilderAddSwagger(thisWebApplicationBuilderbuilder){builder.Services.AddSwagger();returnbuilder;}publicstaticIServiceCollectionAddSwagger(thisIServiceCollectionservices){services.AddEndpointsApiExplorer();services.AddSwaggerGen(c=>{c.SwaggerDoc("v1",newOpenApiInfo(){Description="Minimal API Demo",Title="Minimal API Demo",Version="v1",Contact=newOpenApiContact(){Name="Oleksii Nikiforov",Url=newUri("https://github.com/nikiforovall")}});});returnservices;}}
Organize endpoints around features
AMinimalApiPlayground 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 registrationICarterModule
.
namespaceMinimalAPI;usingDapper;usingMicrosoft.Data.Sqlite;publicclassTodosModule:ICarterModule{publicvoidAddRoutes(IEndpointRouteBuilderapp){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);}privatestaticasyncTask<IResult>GetTodo(intid,SqliteConnectiondb)=>awaitdb.QuerySingleOrDefaultAsync<Todo>("SELECT * FROM Todos WHERE Id = @id",new{id})isTodotodo?Results.Ok(todo):Results.NotFound();privateasyncTask<IEnumerable<Todo>>GetTodos(SqliteConnectiondb)=>awaitdb.QueryAsync<Todo>("SELECT * FROM Todos");privatestaticasyncTask<IResult>CreateTodo(Todotodo,SqliteConnectiondb){varnewTodo=awaitdb.QuerySingleAsync<Todo>("INSERT INTO Todos(Title, IsComplete) Values(@Title, @IsComplete) RETURNING * ",todo);returnResults.Created($"/todos/{newTodo.Id}",newTodo);}privatestaticasyncTask<IResult>DeleteTodo(intid,SqliteConnectiondb)=>awaitdb.ExecuteAsync("DELETE FROM Todos WHERE Id = @id",new{id})==1?Results.NoContent():Results.NotFound();privatestaticasyncTask<IResult>MarkComplete(intid,SqliteConnectiondb)=>awaitdb.ExecuteAsync("UPDATE Todos SET IsComplete = true WHERE Id = @Id",new{Id=id})==1?Results.NoContent():Results.NotFound();}publicclassTodo{publicintId{get;set;}publicstring?Title{get;set;}publicboolIsComplete{get;set;}}
💡Tip: You can useMethod Group (C#) instead of lambda expression to avoid formatting issues and keep code clean. Also, it provides automatic endpoint metadataaspnetcore#34540, that's cool.
// FROMapp.MapGet("/todos",async(SqliteConnectiondb)=>awaitdb.QueryAsync<Todo>("SELECT * FROM Todos"));// TOapp.MapGet("/api/todos",GetTodos);asyncTask<IEnumerable<Todo>>GetTodos(SqliteConnectiondb)=>awaitdb.QueryAsync<Todo>("SELECT * FROM Todos");
To register modules you simply need to add two lines of code inProgram.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 withCarter 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 withVertical 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
- https://www.hanselman.com/blog/carter-community-for-aspnet-core-means-enjoyable-web-apis-on-the-cutting-edge
- https://github.com/CarterCommunity/Carter
- https://github.com/AnthonyGiretti/aspnetcore-minimal-api
- https://www.youtube.com/watch?v=4ORO-KOufeU&ab_channel=NickChapsas
- https://www.youtube.com/watch?v=bSJ5n7alhTs&ab_channel=dotNET
- https://benfoster.io/blog/mvc-to-minimal-apis-aspnet-6/
- https://blog.ploeh.dk/2011/07/28/CompositionRoot/
Top comments(2)
It's a GitHub theme, please see:github.com/primer/github-vscode-theme
For further actions, you may consider blocking this person and/orreporting abuse