ASP.NET Core cookbook · Beginner

Organize minimal API endpoints into groups

Program.cs grows into hundreds of lines of endpoints with repeated prefixes, auth and tags.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Put each feature's endpoints in a static extension method that maps a route group with shared prefix, tags, authorization and filters. Handlers are static methods returning TypedResults unions.

Solution

// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddSingleton<ProductStore>();
var app = builder.Build();
app.UseAuthorization();
app.MapProductEndpoints();
app.Run();
// Products/ProductEndpoints.cs
using Microsoft.AspNetCore.Http.HttpResults;

public static class ProductEndpoints
{
    public static IEndpointRouteBuilder MapProductEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/products").WithTags("Products");
        group.MapGet("/", List);
        group.MapGet("/{id:int}", Get).WithName("GetProduct");
        group.MapPost("/", Create).RequireAuthorization();
        return app;
    }

    static Ok<IReadOnlyList<Product>> List(ProductStore store) => TypedResults.Ok(store.All());

    static Results<Ok<Product>, NotFound> Get(int id, ProductStore store) =>
        store.Find(id) is { } product ? TypedResults.Ok(product) : TypedResults.NotFound();

    static CreatedAtRoute<Product> Create(NewProduct input, ProductStore store)
    {
        var product = store.Add(input.Name, input.Price);
        return TypedResults.CreatedAtRoute(product, "GetProduct", new { id = product.Id });
    }
}

public record Product(int Id, string Name, decimal Price);
public record NewProduct(string Name, decimal Price);

public class ProductStore
{
    private readonly List<Product> _items = [new(1, "Mug", 8.50m)];
    public IReadOnlyList<Product> All() => _items;
    public Product? Find(int id) => _items.FirstOrDefault(p => p.Id == id);
    public Product Add(string name, decimal price)
    {
        var product = new Product(_items.Max(p => p.Id) + 1, name, price);
        _items.Add(product);
        return product;
    }
}

Pitfalls

  • Lambdas with many injected services inline in Program.cs; static methods are easier to test and read.
  • Returning IResult everywhere, losing response metadata for OpenAPI.
  • Applying RequireAuthorization() endpoint by endpoint and forgetting one; put it on the group.

Variations

  • Nest groups (group.MapGroup("/{id}/reviews")) for sub-resources.
  • Use a fallback authorization policy so every endpoint requires auth unless marked AllowAnonymous().

Further reading: Route groups

Reference: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/route-handlers#route-groups

More ASP.NET Core recipes

Was this page helpful?