In this article, we are going to explain the core idea and basic concepts of the minimal APIs in .NET 6. But if we try to explain it in one sentence, it will be that it is an API without the need for a controller.
Other than a theoretical explanation, we are going to dive into the code and show how we can implement a minimal API that has all CRUD operations.
Let’s start.
The Origin of the Minimal APIs
The story of the minimal API started in November 2019. The developer community got a chance to see the implementation of the distributed calculator in Go, Python, C#, and Javascript. When the community started comparing how many files and lines of code are needed to do almost the same thing with C# compared to other languages, it was apparent that C# seems more complicated than the rest of them.
Imagine the number of concepts and features accumulated over the years and how overwhelming it might be for a newcomer to dig into the world of .NET web development. Therefore, a .NET Core team wanted to reduce the complexity for all developers (newcomers and veterans) and embrace minimalism. So if we’re going to create a simple API with a single endpoint, we should be able to do it within a single file. But if we need to switch later to use controllers again, we should also be able to do that.
Let’s see what they have accomplished.
How to Setup Minimal APIs?
We need to have Visual Studio 2022 with the ASP.NET and web development workload to follow along with this article.
To create a minimal API, we are going to create a C# project from the ASP.NET Core Empty template and uncheck all the checkboxes in the additional information dialog. Doing that, we are going to end up with the Program
class with four lines in it:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => "Hello World!"); app.Run();
And that’s it. Once we run our app, we will see the Hello Word!
message in a browser.
Let’s explain how it is possible to have an API with one endpoint up and running with four lines of code.
The first thing we can notice is missing using
directives. C#10 introduced global using
directives, and the common ones are included by default if the feature is enabled. Since one of them is Microsoft.AspNetCore.Builder
we don’t need to write anything.
Then we can see MapGet
, an extension method from the EndpointRouteBuilderExtensions
class that accepts a delegate as one of the parameters. Accepting any delegate is another example where C#10 put minimal API at its best. We can pass any method to the MapGet
method, and the compiler will do its best to figure out how to convert it to the RequestDelegate
. If it can’t, it will let us know.
Lastly, with the app.Run()
method, we are able to run our app.
How Does Dependency Injection Work With the Minimal APIs?
A new feature in the dependency injection (DI) container in the .NET 6 enables us to know which type is registered as a resolvable type in the container. That means we can amend a delegate in the MapGet
method with some types that we have registered with the DI without the need for additional attributes or injections via constructors. For example, we can write:
app.MapGet("/", (IHttpClientFactory httpClientFactory) => "Hello World!"));
And the framework will know that IHttpClientFactory
is registered as a resolvable type. It can visit the DI container and populate it with the registered type. This was not possible before .NET 6.
Implementation of the CRUD Methods in the Minimal APIs
For this example, we are going to use the Entity Framework Core in-memory database. You can find a detailed setup in the Creating Multiple Resources with a Single Request in ASP.NET Core article.
Since we are going to operate on articles, let’s create an Article
class:
public class Article { public int Id { get; set; } public string? Title { get; set; } public string? Content { get; set; } public DateTime? PublishedAt { get; set; } }
And an ArticleRequest
record:
public record ArticleRequest(string? Title, string? Content, DateTime? PublishedAt);
Let’s first implement get methods and explain what’s happening:
app.MapGet("/articles", async (ApiContext context) => Results.Ok(await context.Articles.ToListAsync())); app.MapGet("/articles/{id}", async (int id, ApiContext context) => { var article = await context.Articles.FindAsync(id); return article != null ? Results.Ok(article) : Results.NotFound(); });
For both methods, we add the route pattern as a first parameter. In the first MapGet
implementation ApiContext
is resolved in the delegate because it is registered as a resolvable type. In the second MapGet
implementation, we add id
as an additional parameter in the delegate. We could also change the order of the parameters:
app.MapGet("/articles/{id}", async (ApiContext context, int id) => { var article = await context.Articles.FindAsync(id); return article != null ? Results.Ok(article) : Results.NotFound(); });
And everything will continue to work. That is that nifty feature where the compiler tries to resolve any delegate as a RequestDelegate
, and it is doing a pretty good job.
To continue, let’s implement POST and DELETE methods:
app.MapPost("/articles", async (ArticleRequest article, ApiContext context) => { var createdArticle = context.Articles.Add(new Article { Title = article.Title ?? string.Empty, Content = article.Content ?? string.Empty, PublishedAt = article.PublishedAt, }); await context.SaveChangesAsync(); return Results.Created($"/articles/{createdArticle.Entity.Id}", createdArticle.Entity); }); app.MapDelete("/articles/{id}", async (int id, ApiContext context) => { var article = await context.Articles.FindAsync(id); if (article == null) { return Results.NotFound(); } context.Articles.Remove(article); await context.SaveChangesAsync(); return Results.NoContent(); });
Finally, we are going to implement the PUT method:
app.MapPut("/articles/{id}", async (int id, ArticleRequest article, ApiContext context) => { var articleToUpdate = await context.Articles.FindAsync(id); if (articleToUpdate == null) return Results.NotFound(); if (article.Title != null) articleToUpdate.Title = article.Title; if (article.Content != null) articleToUpdate.Content = article.Content; if (article.PublishedAt != null) articleToUpdate.PublishedAt = article.PublishedAt; await context.SaveChangesAsync(); return Results.Ok(articleToUpdate); });
Since we want to focus on Minimal APIs our implementation is simple and it is missing proper request model validations or using mapping with AutoMapper. You can read how to apply all these properly in our ASP.NET Core Web API – Post, Put, Delete article.
Interestingly, there isn’t an extension method MapPatch
in the EndpointRouteBuilderExtensions
class. But we can use MapMethods
method that is more robust and adaptable than the previous ones:
app.MapMethods("/articles/{id}", new[] { "PATCH" }, async (int id, ArticleRequest article, ApiContext context) => { ... });
If you want to implement the PATCH method in the right way, you can read more about it in our Using HttpClient to Send HTTP PATCH Requests in ASP.NET Core article.
How to Use Swagger, Authentication, and Authorization?
The great thing is that there isn’t any real difference in how to set up and use Swagger in the Minimal APIs than before. You can read more about Swagger, and how to configure it in our Configuring and Using Swagger UI in ASP.NET Core Web API article.
The same goes for authentication and authorization. The only new feature is adding the authentication and authorization attributes (or any attribute) to the delegate methods. That was not possible in the older versions of the C#. So if we would implement authentication as in our ASP.NET Core Authentication with JWT article, we could add [Authorize]
attribute on our delegates:
app.MapPut("/articles/{id}", [Authorize] async (int id, ArticleRequest article, ApiContext context) => { ... }
Organizing the Code in Minimal APIs
With minimal APIs the Program
class can become quite large with a lot of code lines. To avoid that, let’s show how we can organize our code.
We are going to extract the code within each mapping method into a separate ArticleService
class that is going to implement IArticleService
interface (you can find the implementation in our source code). And then, since we can inject our service into delegate methods we can change our code to:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("api")); builder.Services.AddScoped<IArticleService, ArticleService>(); var app = builder.Build(); app.MapGet("/articles", async (IArticleService articleService) => await articleService.GetArticles()); app.MapGet("/articles/{id}", async (int id, IArticleService articleService) => await articleService.GetArticleById(id)); app.MapPost("/articles", async (ArticleRequest articleRequest, IArticleService articleService) => await articleService.CreateArticle(articleRequest)); app.MapPut("/articles/{id}", async (int id, ArticleRequest articleRequest, IArticleService articleService) => await articleService.UpdateArticle(id, articleRequest)); app.MapDelete("/articles/{id}", async (int id, IArticleService articleService) => await articleService.DeleteArticle(id)); app.Run();
Our Program
class looks more organized now. We could further extract all mapping calls to a separate extension method, but with each refactor, we are diverging from the original idea of the minimal APIs to be straightforward.
Conclusion
In this article, we’ve talked about minimal API origin and its motivation to exist. We also have shown how to create minimal API with CRUD operations.