Justice League - Minimal API Demo
Minimal APIs in ASP.NET Core are a streamlined way of building HTTP APIs with minimal overhead and ceremony. They provide a simpler and more concise way to define and configure HTTP endpoints, focusing on reducing boilerplate code and enhancing developer productivity, especially for small-to-medium-sized APIs.
Key Features of Minimal APIs
- Simplicity: Minimal APIs are designed to be straightforward and easy to understand, using fewer lines of code to accomplish tasks.
- Reduced Boilerplate: Minimal APIs eliminate much of the boilerplate code associated with traditional ASP.NET Core projects, such as controllers, attributes, and extensive configuration.
- Endpoint Mapping: Define routes and endpoints directly in the Program.cs file, streamlining the routing setup.
- Lightweight: They are ideal for microservices, serverless functions, and small applications where the overhead of the full ASP.NET Core MVC framework is unnecessary.
- Full Power of ASP.NET Core: Despite their simplicity, minimal APIs still provide access to the full power of ASP.NET Core, including dependency injection, middleware, and configuration.
Project Setup
-
Setup the project
mkdir JusticeLeague
cd JusticeLeague -
Create a solution
dotnet new sln -f slnxnoteSLNX is better because
- XML is better, and consistent with
.csprojfiles 👑 - More readable
- Manual edits are easier
- Less merge conflicts
- Less Verbose 🤢
- XML is better, and consistent with
-
Add a dotnet tool manifest:
dotnet new tool-manifestTool manifest - so we can run
dotnet tool restoreto restore all the tools needed for our application. -
Add the Kiota CLI tool to generate API client code:
dotnet tool install Microsoft.OpenApi.Kiota -
Use central package mangement
dotnet new packagesprops -
Create a directory
mkdir JusticeLeague.Api
cd JusticeLeague.Api -
Create an empty project
dotnet new web -
Add the project to the solution
cd ..
dotnet sln add JusticeLeague.Api -
Open the solution in your IDE
.\JusticeLeague.slnx -
Create a
wwwrootdirectory in the root of the project
Setup Data
-
Create a
Models\Heroclasspublic record Hero(string HeroName, string CivilianName)
{
public Guid Id { get; init; } = Guid.NewGuid();
} -
Create
Services\HeroServiceclasspublic class HeroService
{
private readonly List<Hero> _heroNames =
[
new Hero("Superman", "Clark Kent"),
new Hero("Batman", "Bruce Wayne"),
new Hero("Wonder Woman", "Diana Prince"),
new Hero("The Flash", "Barry Allen"),
new Hero("Cyborg", "Victor Stone"),
new Hero("Aquaman", "Arthur Curry")
];
public IEnumerable<Hero> GetHeroes() => _heroNames;
public void AddHero(Hero hero) => _heroNames.Add(hero);
public Hero? GetHeroById(Guid id) => _heroNames.FirstOrDefault(x => x.Id == id);
}
Add First API
-
Remove the default API
-
Add the following to Program.cs
app.MapGet("api/heroes", (HeroService heroService) => heroService.GetHeroes()); -
Run API
-
Show exception
noteThe exception happened because we need to tell ASP.NET Core where HeroService needs to come from. We'll also need to add it to the DI container.
-
Add Hero Service to DI Container
builder.Services.AddSingleton<HeroService>(); -
Show API working
Add Other APIs
-
Add the following to Program.cs
app.MapGet("/api/heroes", (HeroService heroService) => heroService.GetHeroes());
app.MapPost("api/heroes", (string heroName, string civilianName, HeroService heroService) =>
{
var hero = new Hero(heroName, civilianName);
heroService.AddHero(hero);
return TypedResults.Created($"/heroes/{hero.Id}", hero);
});
app.MapGet("api/heroes/{id:Guid}", Results<Ok<Hero>, NotFound>(Guid id, HeroService heroService) =>
{
var hero = heroService.GetHeroById(id);
if (hero == null)
return TypedResults.NotFound();
return TypedResults.Ok(hero);
}); -
Test and show API working
Now we have an API working, but we don't have any documentation for it. Let's add Open API to our project.
Add Open API & Scalar UI
-
Add Scalar Package
dotnet add package Scalar.AspNetCore
dotnet add package Microsoft.AspNetCore.OpenApi -
Configure Open API and Scalar
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();
// ... Other code
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference(options =>
{
options.WithTitle("Justice League API")
.WithTheme(ScalarTheme.Purple)
.WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient);
});
} -
Update lauch URL in
Properties/launchSettings.jsonAdd this to the profiles"launchUrl": "scalar", -
Run app - should show the Scalar UI at
/scalar
Improve Open API Spec ⬇️
-
Add the following to each API
.WithName("GetHeroes")
.WithName("GetHeroById")
.WithName("CreateHero") -
Show how the OperationId is now set in Open API spec
noteThe Operation ID comes in very handy when generating client code
-
Add this tag to each API
.WithTags("Heroes") -
Show how the following groups the API in Scalar UI
-
Show how all the HTTP Status codes in the swagger are correct. That is because we're using typed results.
Refactor API
We've got a bit of duplication going on at the moment and our minimal APIs aren't as DRY as they could be. Let's fix that up.
-
Create
Endpoints\HeroesEndpointspublic static class HeroesEndpoints
{
public static void AddHeroesEndpoints(this WebApplication webApplication)
{
var group = webApplication.MapGroup("api/heroes")
.WithTags("Heroes");
group
.MapGet("", (HeroService heroService) => heroService.GetHeroes())
.WithName("GetHeroes");
group
.MapPost("", (string heroName, string civilianName, HeroService heroService) =>
{
var hero = new Hero(heroName, civilianName);
heroService.AddHero(hero);
return TypedResults.Created($"/heroes/{hero.Id}", hero);
})
.WithName("CreateHeroes");
group
.MapGet("{id:Guid}", Results<Ok<Hero>, NotFound> (Guid id, HeroService heroService) =>
{
var hero = heroService.GetHeroById(id);
if (hero == null)
return TypedResults.NotFound();
return TypedResults.Ok(hero);
})
.WithName("GetHeroById");
}
} -
Add the following to Program.cs
app.AddHeroesEndpoints(); -
This is also where we can use other TypedResults, such as InternalServerError, BadRequest, etc.
-
Show APIs still working
Add Authentication
If this application was going to be used by more than one client we would consider using OIDC or a Client Credentials flow. However, for our use case we only have one client so we're going to use Identity Endpoints and Bearer Tokens.
-
Add Nuget packages
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.EntityFrameworkCore.Design -
Add Auth Services
builder.Services
.AddAuthentication()
//.AddCookie("JusticeLeague.Api");
// NOTE: You swap out BearerToken auth for Cookie auth by using 'AddCookie()`
.AddBearerToken(IdentityConstants.BearerScheme);
builder.Services.AddAuthorization(); -
Create
Infrastructure\Identity\ApplicationDbContextpublic class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser>(options);
public class ApplicationUser : IdentityUser
{
} -
Add Identity Endpoints
builder.Services.AddIdentityCore<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddApiEndpoints(); -
Add In-Memory DB
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("justice-league")); -
Configure the OpenAPI doc to use bearer tokens
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, _, _) =>
{
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes.Add("Bearer", new OpenApiSecurityScheme()
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme."
});
return Task.CompletedTask;
});
}); -
Update the URL above with the URL from the web project
-
Configure the route for our identity endpoints
app.MapGroup("account").WithTags("Account").MapIdentityApi<ApplicationUser>();Tell it to use HTTPS
app.UseHttpsRedirection(); -
HeroEndpoints.cs - Require Authorization so that only authenticated users can access the endpoints
var group = webApplication
.MapGroup("api/heroes")
.WithTags("Heroes")
.RequireAuthorization(); -
Try logging in through Scalar UI
- Try the
/api/heroesendpoint - it should return a 401 Unauthorized error because we haven't logged in yet. - Use the
/account/registerendpoint - Then the
/account/loginendpoint - get the token from the response and copy it to Scalar UI - Then try the
/api/heroesendpoint - it should return a 200 OK response with the list of heroes. 🎉
- Try the