C# Minimal APIs Cheat Sheet
Covers ASP.NET Core minimal API setup, routing, model binding, dependency injection, validation, and OpenAPI integration for .NET 8/9.
Minimal API Bootstrap
The full `Program.cs` for a minimal API app — no Startup.cs needed.
var builder = WebApplication.CreateBuilder(args);builder.Services.AddEndpointsApiExplorer();builder.Services.AddOpenApi(); // .NET 9 built-in OpenAPIvar app = builder.Build();app.MapGet("/", () => "Hello, World!");app.MapGet("/items/{id:int}", (int id) => Results.Ok(new { id }));app.MapOpenApi();app.Run();
Routes, Route Groups & Verbs
Group related endpoints and share prefixes/filters/tags.
var items = app.MapGroup("/items").WithTags("Items");items.MapGet("/", () => Results.Ok(itemStore));items.MapGet("/{id:guid}", (Guid id) => itemStore.TryGetValue(id, out var item) ? Results.Ok(item) : Results.NotFound());items.MapPost("/", (CreateItemDto dto, IItemService svc) =>{ var created = svc.Create(dto); return Results.Created($"/items/{created.Id}", created);});items.MapDelete("/{id:guid}", (Guid id, IItemService svc) => svc.Delete(id) ? Results.NoContent() : Results.NotFound());
Dependency Injection & Validation
Handlers pull services straight from DI by parameter type; use filters for validation.
builder.Services.AddScoped<IItemService, ItemService>();builder.Services.AddValidation(); // .NET 8+ DataAnnotations validationapp.MapPost("/items", (CreateItemDto dto, IItemService svc) =>{ if (!MiniValidator.TryValidate(dto, out var errors)) return Results.ValidationProblem(errors); return Results.Ok(svc.Create(dto));}).AddEndpointFilter(async (ctx, next) =>{ // custom endpoint filter, e.g. logging/auth checks return await next(ctx);});
`Results` Helper Methods
Common typed responses returned from handler delegates.
- Results.Ok(obj)- 200 with JSON body
- Results.Created(uri, obj)- 201 with Location header
- Results.NoContent()- 204 with empty body
- Results.NotFound()- 404
- Results.BadRequest(obj)- 400 with error payload
- Results.ValidationProblem(errors)- 400 in RFC 7807 problem-details shape
- TypedResults.Ok(obj)- strongly-typed variant, improves OpenAPI schema generation
Custom Parameter Binding with `BindAsync`
Implement a static `BindAsync` method to teach minimal APIs how to construct a complex type from the request.
public class SortOptions{ public string Field { get; init; } = "id"; public bool Descending { get; init; } public static ValueTask<SortOptions?> BindAsync(HttpContext context, ParameterInfo parameter) { var field = context.Request.Query["sort"].FirstOrDefault() ?? "id"; var desc = context.Request.Query["desc"] == "true"; return ValueTask.FromResult<SortOptions?>(new SortOptions { Field = field, Descending = desc }); }}app.MapGet("/items", (SortOptions sort) => Results.Ok(itemStore.OrderBy(i => sort.Field, sort.Descending)));
Grouping Parameters with `[AsParameters]`
Bundle query, route, and injected-service parameters into one type to keep handler signatures short.
public class ItemQuery{ [FromQuery] public int Page { get; set; } = 1; [FromQuery] public int PageSize { get; set; } = 20; [FromServices] public IItemService Service { get; set; } = default!;}app.MapGet("/items/search", ([AsParameters] ItemQuery query) =>{ var results = query.Service.Search(query.Page, query.PageSize); return Results.Ok(results);});
Composable Endpoint Filters (Class-Based + Inline)
Stack multiple `IEndpointFilter` implementations for validation and cross-cutting concerns; each can short-circuit by not calling `next`.
public class ValidationFilter<T> : IEndpointFilter{ public async ValueTask<object?> InvokeAsync( EndpointFilterInvocationContext ctx, EndpointFilterDelegate next) { var arg = ctx.GetArgument<T>(0); var results = new List<ValidationResult>(); if (!Validator.TryValidateObject(arg!, new ValidationContext(arg!), results, true)) return Results.ValidationProblem(results.ToDictionary( r => r.MemberNames.FirstOrDefault() ?? "", r => new[] { r.ErrorMessage! })); return await next(ctx); // continue the pipeline }}app.MapPost("/items", (CreateItemDto dto) => Results.Ok(dto)) .AddEndpointFilter<ValidationFilter<CreateItemDto>>() .AddEndpointFilter(async (ctx, next) => { var sw = Stopwatch.StartNew(); var result = await next(ctx); app.Logger.LogInformation("Handler took {Ms}ms", sw.ElapsedMilliseconds); return result; });
Authorization Policies & Rate Limiting
Apply named authorization policies and rate limiters per-endpoint via chained extension methods.
builder.Services.AddAuthorization(options =>{ options.AddPolicy("AdminOnly", p => p.RequireRole("Admin"));});builder.Services.AddRateLimiter(options =>{ options.AddFixedWindowLimiter("fixed", opt => { opt.PermitLimit = 100; opt.Window = TimeSpan.FromMinutes(1); });});var app = builder.Build();app.UseRateLimiter();app.MapDelete("/items/{id:guid}", (Guid id) => Results.NoContent()) .RequireAuthorization("AdminOnly") .RequireRateLimiting("fixed");
Endpoint Metadata & OpenAPI Fluent Methods
Chained methods that shape how an endpoint appears in the generated OpenAPI document without changing runtime behavior.
- WithName(name)- names the endpoint for link generation, e.g. `Results.CreatedAtRoute`
- WithTags(tags)- groups endpoints under a heading in the OpenAPI doc / Swagger UI
- WithOpenApi()- enriches the generated operation with reflected parameter/response metadata
- Produces<T>(status)- documents a response type/status code for OpenAPI only
- Accepts<T>(contentType)- documents the expected request body shape and content type
- ExcludeFromDescription()- hides an endpoint entirely from the generated OpenAPI document
- WithSummary / WithDescription- human-readable text attached to the OpenAPI operation
- RequireCors(policyName)- applies a named CORS policy to just this endpoint instead of globally
Prefer the `TypedResults` static class over `Results` in handler return types — it gives you compile-time-checked return types AND automatically produces accurate OpenAPI response schemas, which plain `Results` (returning IResult) cannot.