Skip to content

ndcorder/MinimalEndpoints

Repository files navigation

MinimalEndpoints

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.

CI NuGet License: MIT

The Problem

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 endpoints

The Solution

Organize 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.

Installation

dotnet add package MinimalEndpoints

Quick Start

1. Create an endpoint class

using MinimalEndpoints;

[Get("/api/hello")]
public class HelloEndpoint
{
    public string Handle() => "Hello, World!";
}

2. Register all endpoints

var app = builder.Build();
app.MapEndpoints(); // discovers and registers all endpoint classes
app.Run();

That's it. The source generator handles the rest.

Features

HTTP Method Attributes

[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(...)

Handle Convention

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].

Endpoint Groups

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}", ...)

Dependency Injection

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).

OpenAPI / Swagger Metadata

[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.

Authorization

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")

Works Alongside Hand-Written Endpoints

MapEndpoints() returns IEndpointRouteBuilder, so you can chain it with your own registrations:

app.MapEndpoints();
app.MapGet("/health", () => "ok"); // hand-written endpoint still works

Generated Code

The 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;
    }
}

Diagnostics

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

Requirements

  • .NET 8.0+ (netstandard2.0 analyzer, works with any target framework)
  • C# 11+ (for attribute usage)

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Write tests for your changes
  4. Ensure all tests pass (dotnet test)
  5. Ensure no warnings (dotnet build --no-incremental -warnaserror)
  6. Submit a pull request

License

MIT

About

Source-generated endpoint discovery for structured ASP.NET Core Minimal APIs

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages