Install
$ agentstack add skill-managedcode-dotnet-skills-minimal-apis ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Minimal APIs
Trigger On
- building new HTTP APIs in ASP.NET Core
- creating lightweight microservices
- choosing between Minimal APIs and controllers
- organizing endpoints with route groups
- implementing validation and filters
Documentation
References
- [patterns.md](references/patterns.md) - detailed route groups, filters, TypedResults patterns, parameter binding, error handling, and testing
- [anti-patterns.md](references/anti-patterns.md) - common Minimal API mistakes to avoid
When to Use Minimal APIs vs Controllers
| Use Minimal APIs | Use Controllers | |------------------|-----------------| | New projects | Existing MVC/API projects | | Microservices | Complex model binding | | Simple CRUD APIs | OData, JsonPatch | | Lightweight handlers | Heavy use of attributes | | .NET 8+ projects | Need [ApiController] features |
Workflow
- Define endpoints directly in Program.cs (for small APIs)
- Use route groups for related endpoints
- Move handlers to separate classes as the API grows
- Apply filters for cross-cutting concerns
- Use TypedResults for type-safe responses
- Generate OpenAPI docs with
.WithOpenApi()
Current Upstream Notes
dotnet/aspnetcorev9.0.17is a servicing release; it does not change the Minimal API route-group/filter/TypedResults model.- Continue to use the
aspnetcore-10.0Learn overview and Minimal API pages when exact OpenAPI, filter, or parameter-binding behavior matters.
Basic Patterns
Simple Endpoints
var app = builder.Build();
app.MapGet("/", () => "Hello World");
app.MapGet("/products/{id}", (int id) => Results.Ok(new { Id = id }));
app.MapPost("/products", (Product product) => Results.Created($"/products/{product.Id}", product));
TypedResults (Strongly-Typed)
app.MapGet("/products/{id}", Results, NotFound> (int id, AppDb db) =>
{
var product = db.Products.Find(id);
return product is not null
? TypedResults.Ok(product)
: TypedResults.NotFound();
});
Dependency Injection
app.MapGet("/products", async (IProductService service) =>
{
return await service.GetAllAsync();
});
// Or with [FromServices] for clarity
app.MapGet("/products", async ([FromServices] IProductService service) =>
await service.GetAllAsync());
Route Groups
Basic Grouping
var products = app.MapGroup("/api/products");
products.MapGet("/", GetAll);
products.MapGet("/{id}", GetById);
products.MapPost("/", Create);
products.MapPut("/{id}", Update);
products.MapDelete("/{id}", Delete);
Groups with Shared Configuration
var api = app.MapGroup("/api")
.RequireAuthorization()
.AddEndpointFilter();
var products = api.MapGroup("/products")
.WithTags("Products");
var orders = api.MapGroup("/orders")
.WithTags("Orders")
.RequireAuthorization("AdminOnly");
Endpoint Filters
Inline Filter
app.MapGet("/products/{id}", (int id) => Results.Ok(id))
.AddEndpointFilter(async (context, next) =>
{
var id = context.GetArgument(0);
if (id : IEndpointFilter where T : class
{
public async ValueTask InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var argument = context.Arguments
.OfType()
.FirstOrDefault();
if (argument is null)
return Results.BadRequest("Invalid request body");
var validator = context.HttpContext.RequestServices
.GetService>();
if (validator is not null)
{
var result = await validator.ValidateAsync(argument);
if (!result.IsValid)
return Results.ValidationProblem(result.ToDictionary());
}
return await next(context);
}
}
// Usage
products.MapPost("/", Create)
.AddEndpointFilter>();
Global Filters via Root Group
// All endpoints inherit filters from root group
var root = app.MapGroup("")
.AddEndpointFilter()
.AddEndpointFilter();
root.MapGet("/health", () => Results.Ok());
root.MapGroup("/api/products").MapGet("/", GetProducts);
Organizing Larger APIs
Extension Method Pattern
// ProductEndpoints.cs
public static class ProductEndpoints
{
public static RouteGroupBuilder MapProductEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/products")
.WithTags("Products");
group.MapGet("/", GetAll);
group.MapGet("/{id}", GetById);
group.MapPost("/", Create);
return group;
}
private static async Task>> GetAll(IProductService service)
=> TypedResults.Ok(await service.GetAllAsync());
private static async Task, NotFound>> GetById(
int id, IProductService service)
{
var product = await service.GetByIdAsync(id);
return product is not null
? TypedResults.Ok(product)
: TypedResults.NotFound();
}
private static async Task> Create(
CreateProductRequest request, IProductService service)
{
var product = await service.CreateAsync(request);
return TypedResults.Created($"/api/products/{product.Id}", product);
}
}
// Program.cs
app.MapProductEndpoints();
app.MapOrderEndpoints();
Request/Response DTOs
// Separate from domain models
public record CreateProductRequest(string Name, decimal Price);
public record UpdateProductRequest(string Name, decimal Price);
public record ProductResponse(int Id, string Name, decimal Price);
// Don't expose domain entities directly
app.MapPost("/products", (CreateProductRequest request, IMapper mapper) =>
{
var product = mapper.Map(request);
// ...
return TypedResults.Created($"/products/{product.Id}",
mapper.Map(product));
});
Anti-Patterns to Avoid
| Anti-Pattern | Why It's Bad | Better Approach | |--------------|--------------|-----------------| | Everything in Program.cs | Unmaintainable | Use extension methods | | No route groups | Repetitive config | Group related endpoints | | Manual validation | Error-prone | Use filters + FluentValidation | | Exposing entities | Tight coupling | Use DTOs | | No TypedResults | No compile-time checks | Use TypedResults | | Ignoring OpenAPI | No documentation | Add .WithOpenApi() |
OpenAPI Integration
builder.Services.AddOpenApi();
app.MapOpenApi(); // Serves OpenAPI spec
app.MapGet("/products", GetProducts)
.WithName("GetProducts")
.WithSummary("Get all products")
.WithDescription("Returns a list of all available products")
.Produces>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status500InternalServerError);
Deliver
- clean, organized Minimal API endpoints
- proper use of route groups and filters
- type-safe responses with TypedResults
- OpenAPI documentation
- validation with endpoint filters
Validate
- endpoints return correct status codes
- validation filters catch invalid input
- OpenAPI spec is accurate
- route groups share common configuration
- handlers are testable (can mock dependencies)
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: managedcode
- Source: managedcode/dotnet-skills
- License: MIT
- Homepage: https://skills.managed-code.com
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.