Source-generated endpoint discovery for structured ASP.NET Core Minimal APIs.
No runtime reflection. Fully AOT-compatible. Just organize your endpoints as classes and let the source generator wire everything up.
Minimal APIs are great, but Program.cs becomes a wall of MapGet/MapPost calls as your project grows:
// Program.cs — before (gets unwieldy fast)
app.MapGet("/api/users", (IUserService users) => users.GetAll());
app.MapGet("/api/users/{id}", (int id, IUserService users) => users.GetById(id));
app.MapPost("/api/users", ([FromBody] CreateUserRequest req, IUserService users) => users.Create(req));
app.MapPut("/api/users/{id}", (int id, [FromBody] UpdateUserRequest req, IUserService users) => users.Update(id, req));
app.MapDelete("/api/users/{id}", (int id, IUserService users) => users.Delete(id));
// ... 50 more endpointsOrganize endpoints as classes. One line registers them all:
// Program.cs — after
app.MapEndpoints();Each endpoint is a focused class:
[Get("/api/users/{id}")]
[Tags("Users")]
public class GetUser
{
public async Task<IResult> HandleAsync(int id, IUserService users, CancellationToken ct)
{
var user = await users.GetByIdAsync(id, ct);
return user is null ? Results.NotFound() : Results.Ok(user);
}
}The source generator discovers all endpoint classes at compile time and generates the MapEndpoints() extension method — zero reflection, zero startup cost.
dotnet add package MinimalEndpointsusing MinimalEndpoints;
[Get("/api/hello")]
public class HelloEndpoint
{
public string Handle() => "Hello, World!";
}var app = builder.Build();
app.MapEndpoints(); // discovers and registers all endpoint classes
app.Run();That's it. The source generator handles the rest.
[Get("/api/items")] // app.MapGet(...)
[Post("/api/items")] // app.MapPost(...)
[Put("/api/items/{id}")] // app.MapPut(...)
[Delete("/api/items/{id}")] // app.MapDelete(...)
[Patch("/api/items/{id}")] // app.MapPatch(...)Your endpoint class must have a Handle or HandleAsync method. Parameters are automatically bound:
| Parameter Type | Binding |
|---|---|
Matches {name} in route |
Route parameter |
CancellationToken |
Request cancellation |
HttpContext |
HTTP context |
| Interface or complex type | [FromServices] (DI) |
| Simple type (string, int, etc.) | [FromQuery] |
You can also use explicit attributes: [FromBody], [FromQuery], [FromServices], [FromRoute].
Prefix routes with [EndpointGroup]:
[EndpointGroup("api/v1/users")]
[Get("/{id}")]
public class GetUser
{
public string Handle(int id) => "user";
}
// Registers as: MapGet("api/v1/users/{id}", ...)Endpoints with constructor parameters are resolved from DI:
[Get("/api/data")]
public class GetData
{
private readonly IDataService _data;
public GetData(IDataService data) => _data = data;
public async Task<IResult> HandleAsync(CancellationToken ct)
{
var items = await _data.GetAllAsync(ct);
return Results.Ok(items);
}
}Endpoints without constructors are instantiated directly (no DI registration needed).
[Get("/api/users")]
[Tags("Users", "Admin")]
[ProducesResponse(200)]
[ProducesResponse(404)]
public class ListUsers
{
public string Handle() => "users";
}Generates .WithName("ListUsers"), .WithTags(...), and .Produces(...) calls automatically.
Standard ASP.NET Core [Authorize] attributes are forwarded:
using Microsoft.AspNetCore.Authorization;
[Get("/api/admin/users")]
[Authorize("AdminPolicy")]
public class ListAdminUsers
{
public string Handle() => "admin users";
}
// Generates: .RequireAuthorization("AdminPolicy")MapEndpoints() returns IEndpointRouteBuilder, so you can chain it with your own registrations:
app.MapEndpoints();
app.MapGet("/health", () => "ok"); // hand-written endpoint still worksThe generator produces inspectable, debuggable C# code. For a GetUser endpoint, it generates something like:
public static class MinimalEndpointsRegistrationExtensions
{
public static IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder app)
{
app.MapGet("/api/users/{id}", async (int id, [FromServices] IUserService users, CancellationToken ct) =>
{
var endpoint = new GetUser();
return await endpoint.HandleAsync(id, users, ct);
})
.WithName("GetUser")
.WithTags("Users");
return app;
}
}| Code | Severity | Description |
|---|---|---|
| ME001 | Error | Endpoint class missing Handle/HandleAsync method |
| ME002 | Error | Multiple HTTP method attributes on one class |
| ME003 | Warning | Handle method is not public |
- .NET 8.0+ (netstandard2.0 analyzer, works with any target framework)
- C# 11+ (for attribute usage)
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Write tests for your changes
- Ensure all tests pass (
dotnet test) - Ensure no warnings (
dotnet build --no-incremental -warnaserror) - Submit a pull request