| name | dotnet-minimal-apis |
| description | Builds ASP.NET Core Minimal APIs -- route groups, filters, TypedResults, OpenAPI. |
| metadata | {"short-description":".NET skill guidance for web tasks"} |
dotnet-minimal-apis
Minimal APIs are Microsoft's recommended approach for new ASP.NET Core HTTP API projects. They provide a lightweight,
lambda-based programming model with first-class OpenAPI support, endpoint filters for cross-cutting concerns, and route
groups for organization at scale.
Scope
- Route groups and endpoint organization
- Endpoint filters for cross-cutting concerns
- TypedResults for compile-time response type safety
- Parameter binding (route, query, body, services)
- JSON configuration with ConfigureHttpJsonOptions
- Carter library integration for auto-discovery modules
Out of scope
- API versioning strategies -- see [skill:dotnet-api-versioning]
- Input validation frameworks -- see [skill:dotnet-input-validation]
- Architectural patterns (vertical slices, CQRS) -- see [skill:dotnet-architecture-patterns]
- Authentication and authorization -- see [skill:dotnet-api-security]
- OpenAPI document generation -- see [skill:dotnet-openapi]
- gRPC and real-time communication -- see [skill:dotnet-grpc] and [skill:dotnet-realtime-communication]
Cross-references: [skill:dotnet-architecture-patterns] for organizing large APIs, [skill:dotnet-input-validation] for
request validation, [skill:dotnet-api-versioning] for versioning strategies, [skill:dotnet-openapi] for OpenAPI
customization.
Route Groups
Route groups organize related endpoints under a shared prefix, applying common configuration (filters, metadata,
authorization) once. They replace repetitive chaining of MapGet/MapPost with shared prefixes.
var app = builder.Build();
var products = app.MapGroup("/api/products")
.WithTags("Products")
.RequireAuthorization();
products.MapGet("/", async (AppDbContext db) =>
TypedResults.Ok(await db.Products.ToListAsync()));
products.MapGet("/{id:int}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product product
? TypedResults.Ok(product)
: TypedResults.NotFound());
products.MapPost("/", async (CreateProductDto dto, AppDbContext db) =>
{
var product = new Product { Name = dto.Name, Price = dto.Price };
db.Products.Add(product);
await db.SaveChangesAsync();
return TypedResults.Created($"/api/products/{product.Id}", product);
});
products.MapDelete("/{id:int}", async (int id, AppDbContext db) =>
{
if (await db.Products.FindAsync(id) is not Product product)
return TypedResults.NotFound();
db.Products.Remove(product);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
```text
### Nested Groups
Groups can be nested to compose prefixes and filters:
```csharp
var api = app.MapGroup("/api")
.AddEndpointFilter<RequestLoggingFilter>();
var v1 = api.MapGroup("/v1");
var products = v1.MapGroup("/products").WithTags();
orders = v1.MapGroup().WithTags();
products.MapGet(, GetProducts);
orders.MapPost(, CreateOrder);
```text
---
Endpoint filters provide a pipeline cross-
{
ValueTask<?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
argument = context.Arguments
.OfType<T>()
.FirstOrDefault();
(argument )
TypedResults.BadRequest();
result = validator.ValidateAsync(argument);
(!result.IsValid)
{
TypedResults.ValidationProblem(
result.ToDictionary());
}
next(context);
}
}
```text
```csharp
products.MapPost(, CreateProduct)
.AddEndpointFilter<ValidationFilter<CreateProductDto>>();
products = app.MapGroup()
.AddEndpointFilter<RequestLoggingFilter>();
products.MapGet(, GetProductById)
.AddEndpointFilter( (context, next) =>
{
id = context.GetArgument<>();
(id <= )
TypedResults.BadRequest();
next(context);
});
```text
=>
db.Products.FindAsync(id) Product product
? TypedResults.Ok(product)
: TypedResults.NotFound());
products.MapGet(, ( id, AppDbContext db) =>
db.Products.FindAsync(id) Product product
? Results.Ok(product)
: Results.NotFound());
```text
Use `Results<T1, T2, ...>` to declare all possible response types a single endpoint. This enables accurate OpenAPI
documentation multiple response codes:
```csharp
products.MapPost(, =>
{
( db.Products.AnyAsync(p => p.Sku == dto.Sku))
TypedResults.Conflict();
product = Product { Name = dto.Name, Sku = dto.Sku, Price = dto.Price };
db.Products.Add(product);
db.SaveChangesAsync();
TypedResults.Created(, product);
});
```text
---
.NET adds built- OpenAPI support via `Microsoft.AspNetCore.OpenApi`. Minimal APIs generate OpenAPI metadata
`TypedResults`, parameter bindings, attributes automatically.
```csharp
builder.Services.AddOpenApi();
app = builder.Build();
(app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
```json
```csharp
products.MapGet(, GetProductById)
.WithName()
.WithSummary()
.WithDescription()
.Produces<Product>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status404NotFound);
```
{
{
= routes.MapGroup()
.WithTags();
.MapGet(, GetAll);
.MapGet(, GetById);
.MapPost(, Create);
.MapPut(, Update);
.MapDelete(, Delete);
;
}
Task<Ok<List<Product>>> GetAll(AppDbContext db) =>
TypedResults.Ok( db.Products.ToListAsync());
Task<Results<Ok<Product>, NotFound>> GetById(
id, AppDbContext db) =>
db.Products.FindAsync(id) Product p
? TypedResults.Ok(p)
: TypedResults.NotFound();
Task<Created<Product>> Create(
CreateProductDto dto, AppDbContext db)
{
product = Product { Name = dto.Name, Price = dto.Price };
db.Products.Add(product);
db.SaveChangesAsync();
TypedResults.Created(, product);
}
Task<Results<NoContent, NotFound>> Update(
id, UpdateProductDto dto, AppDbContext db)
{
product = db.Products.FindAsync(id);
(product ) TypedResults.NotFound();
product.Name = dto.Name;
product.Price = dto.Price;
db.SaveChangesAsync();
TypedResults.NoContent();
}
Task<Results<NoContent, NotFound>> Delete(
id, AppDbContext db)
{
product = db.Products.FindAsync(id);
(product ) TypedResults.NotFound();
db.Products.Remove(product);
db.SaveChangesAsync();
TypedResults.NoContent();
}
}
app.MapProductEndpoints();
app.MapOrderEndpoints();
app.MapCustomerEndpoints();
```csharp
For projects that prefer auto-discovery of endpoint modules, the Carter library provides an `ICarterModule` :
```
:
{
{
= app.MapGroup().WithTags();
.MapGet(, (AppDbContext db) =>
TypedResults.Ok( db.Products.ToListAsync()));
.MapGet(, ( id, AppDbContext db) =>
db.Products.FindAsync(id) Product p
? TypedResults.Ok(p)
: TypedResults.NotFound());
}
}
builder.Services.AddCarter();
app = builder.Build();
app.MapCarter();
```csharp
;
});
```json
**Gotcha:** `ConfigureHttpJsonOptions` configures JSON serialization Minimal APIs only. MVC controllers use a
separate pipeline -- configure via `builder.Services.AddControllers().AddJsonOptions(...)`. Mixing them up has no
effect.
---
Minimal APIs bind parameters route, query, headers, body, DI automatically based type attribute
annotations.
```csharp
app.MapGet(, ( id) => ...);
app.MapGet(, ([FromQuery] page, [FromQuery] pageSize) => ...);
app.MapGet(, ([FromHeader(Name = )] correlationId) => ...);
app.MapPost(, (CreateProductDto dto) => ...);
app.MapGet(, (AppDbContext db, ILogger<Program> logger) => ...);
app.MapGet(, ([AsParameters] ProductQuery query) => ...);
;
```text
---
**Do use `Results` `TypedResults` available** -- `Results.Ok()` returns `IResult` the OpenAPI
generator cannot infer response schemas. Use `TypedResults.Ok()` to enable automatic schema generation.
**Do forget `ConfigureHttpJsonOptions` only applies to Minimal APIs** -- MVC controllers need
`.AddControllers().AddJsonOptions()` separately.
**Do apply validation logic inline every endpoint** -- use endpoint filters cross-reference
[] centralized validation patterns.
**Do register filters the wrong order** -- first-registered filter outermost.
Code Navigation (Serena MCP)
Primary approach: Use Serena symbol operations for efficient code navigation:
- Find definitions:
serena_find_symbol instead of text search
- Understand structure:
serena_get_symbols_overview for file organization
- Track references:
serena_find_referencing_symbols for impact analysis
- Precise edits:
serena_replace_symbol_body for clean modifications
When to use Serena vs traditional tools:
- Use Serena: Navigation, refactoring, dependency analysis, precise edits
- Use Read/Grep: Reading full files, pattern matching, simple text operations
- Fallback: If Serena unavailable, traditional tools work fine
Example workflow:
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"