Skip to main content

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

  1. Setup the project

    mkdir JusticeLeague
    cd JusticeLeague
  2. Create a solution

    dotnet new sln -f slnx
    note

    SLNX is better because

    • XML is better, and consistent with .csproj files 👑
    • More readable
    • Manual edits are easier
    • Less merge conflicts
    • Less Verbose 🤢
  3. Add a dotnet tool manifest:

    dotnet new tool-manifest

    Tool manifest - so we can run dotnet tool restore to restore all the tools needed for our application.

  4. Add the Kiota CLI tool to generate API client code:

    dotnet tool install Microsoft.OpenApi.Kiota
  5. Use central package mangement

    dotnet new packagesprops
  6. Create a directory

    mkdir JusticeLeague.Api
    cd JusticeLeague.Api
  7. Create an empty project

    dotnet new web
  8. Add the project to the solution

    cd ..
    dotnet sln add JusticeLeague.Api
  9. Open the solution in your IDE

     .\JusticeLeague.slnx
  10. Create a wwwroot directory in the root of the project

Setup Data

  1. Create a Models\Hero class

    public record Hero(string HeroName, string CivilianName)
    {
    public Guid Id { get; init; } = Guid.NewGuid();
    }
  2. Create Services\HeroService class

    public 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

  1. Remove the default API

  2. Add the following to Program.cs

    app.MapGet("api/heroes", (HeroService heroService) => heroService.GetHeroes());
  3. Run API

  4. Show exception

    note

    The 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.

  5. Add Hero Service to DI Container

      builder.Services.AddSingleton<HeroService>();
  6. Show API working

Add Other APIs

  1. 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);
    });
  2. Test and show API working

note

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

  1. Add Scalar Package

    dotnet add package Scalar.AspNetCore
    dotnet add package Microsoft.AspNetCore.OpenApi
  2. 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);
    });
    }
  3. Update lauch URL in Properties/launchSettings.json Add this to the profiles

    "launchUrl": "scalar",
  4. Run app - should show the Scalar UI at /scalar

Improve Open API Spec ⬇️

  1. Add the following to each API

    .WithName("GetHeroes")
    .WithName("GetHeroById")
    .WithName("CreateHero")
  2. Show how the OperationId is now set in Open API spec

    note

    The Operation ID comes in very handy when generating client code

  3. Add this tag to each API

    .WithTags("Heroes")
  4. Show how the following groups the API in Scalar UI

  5. 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.

  1. Create Endpoints\HeroesEndpoints

    public 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");
    }
    }
  2. Add the following to Program.cs

    app.AddHeroesEndpoints();
  3. This is also where we can use other TypedResults, such as InternalServerError, BadRequest, etc.

  4. Show APIs still working

Add Authentication

note

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.

  1. Add Nuget packages

    dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
    dotnet add package Microsoft.EntityFrameworkCore.InMemory
    dotnet add package Microsoft.EntityFrameworkCore.Design
  2. 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();
  3. Create Infrastructure\Identity\ApplicationDbContext

    public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser>(options);

    public class ApplicationUser : IdentityUser
    {
    }
  4. Add Identity Endpoints

    builder.Services.AddIdentityCore<ApplicationUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddApiEndpoints();
  5. Add In-Memory DB

    builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseInMemoryDatabase("justice-league"));
  6. 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;
    });
    });
  7. Update the URL above with the URL from the web project

  8. Configure the route for our identity endpoints

         app.MapGroup("account").WithTags("Account").MapIdentityApi<ApplicationUser>();

    Tell it to use HTTPS

        app.UseHttpsRedirection();
  9. HeroEndpoints.cs - Require Authorization so that only authenticated users can access the endpoints

    var group = webApplication
    .MapGroup("api/heroes")
    .WithTags("Heroes")
    .RequireAuthorization();
  10. Try logging in through Scalar UI

    1. Try the /api/heroes endpoint - it should return a 401 Unauthorized error because we haven't logged in yet.
    2. Use the /account/register endpoint
    3. Then the /account/login endpoint - get the token from the response and copy it to Scalar UI
    4. Then try the /api/heroes endpoint - it should return a 200 OK response with the list of heroes. 🎉