Vertical slices, not layers
An operation is one folder: its contract, its handler, its permissions, its tests. Changing an endpoint means opening one directory.

1.0.0-preview.3 · net10.0 · MIT · Native AOT
One class per endpoint, carrying its route, its metadata, and its handling. Ordinary ASP.NET Core underneath, all the way down.
1.0.0-preview.3 is on nuget.org, published through NuGet Trusted Publishing: no long-lived publishing credential exists in the repository or its organization secrets. The API is settling but no longer moving weekly; breaking changes before 1.0 are possible and are listed in the changelog.
An endpoint
The attribute declares the route. The namespace supplies the operation id (InvoicesGet), so nothing has to be named twice. The request record is bound from the route, and the response is serialized and documented.
namespace Billing.Endpoints.Invoices.Get;
public sealed record GetInvoice(string InvoiceId);
[Get("invoices/{invoiceId}")]
public sealed class Endpoint(IInvoiceStore store) : ApiEndpoint<GetInvoice, InvoiceView>
{
public override async Task<InvoiceView> HandleAsync(GetInvoice request, CancellationToken ct) =>
InvoiceView.From(await store.GetAsync(request.InvoiceId, ct));
}var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMinimalEndpoints();
var app = builder.Build();
app.MapEndpointGroup().MapEndpointsFrom(typeof(Program).Assembly, routePrefix: "/api");
app.Run();Why
Past a few dozen routes you are choosing between a Program.cs nobody wants to open, a pile of extension methods that hide the route table, or a framework that replaces ASP.NET Core with its own parallel universe. MinimalEndpoints takes the middle path: it gives you a place to put an endpoint and takes nothing away.
An operation is one folder: its contract, its handler, its permissions, its tests. Changing an endpoint means opening one directory.
Routing, filters, results, CORS, rate limiting, output caching, authorization and OpenAPI stay ASP.NET Core's. Every escape hatch is an IEndpointConventionBuilder.
HandleAsync is a method. Call a service, query a store, dispatch to whatever you already use. The framework owns route, binding and metadata, and stops there.
API Explorer needs a MethodInfo in metadata or your endpoint silently vanishes from the OpenAPI document. Handled once, for every endpoint.
Base types
ApiEndpointWithResult covers operations whose status depends on what happened. The documented schema stays the response type; the wrapper never reaches the wire.
ApiEndpoint<TRequest, TResponse>A request in, a response body outApiEndpoint<TRequest>A request in, 204 No Content outApiEndpointWithoutRequest<TResponse>No contract, a response body outApiEndpointWithResult<TRequest, TResponse>The status code is decided by the handlerApiEndpointWrite the response yourself, straight to HttpContextpublic override async Task<EndpointResult<InvoiceView>> HandleAsync(
CreateInvoice cmd, CancellationToken ct)
{
var (invoice, created) = await store.UpsertAsync(cmd, ct);
return created
? EndpointResult.Status(StatusCodes.Status201Created, InvoiceView.From(invoice))
: EndpointResult.Ok(InvoiceView.From(invoice));
}Binding
Route wins over the body so a resource identifier in the URL cannot be contradicted by the payload. Built in: string, bool, int, long, Guid, enum, DateTimeOffset, anything implementing IParsable<T>, arrays and lists of those from the query string, plus headers and claims — and you can register your own parser.
builder.Services.AddMinimalEndpoints(o => o.ValueBinders.Add<Money>(Money.TryParse));NE0002: Contract 'Transfer' has parameter 'amount' of unsupported type 'Money'.
Implement IParsable<Money>, or register a parser with
AddMinimalEndpoints(o => o.ValueBinders.Add<Money>(...)).Anything unsupported throws, loudly, rather than binding silently to a default. Body handling is explicit per endpoint via options.BodyMode: None, Optional, Required, RequiredWithContentType, or OptionalWithContentType, the last rejecting a non-JSON content type with a bare 415 before the body is read.
This is a design position, not a gap: predictable binding you can hold in your head, and a loud failure instead of a quiet one.
options.StrictTypedParsing rejects a typed route, query, header, or claim value that does not parse, with a 400 naming it, instead of falling back to the parameter's default. One rule across both binders: registered value binders, IParsable fallbacks, and collection elements are rejected the same way a scalar int is. Opt-in, because turning it on changes what an existing API returns.
The binder records which properties a request body actually contained, so a member sent as null stays null while an omitted one falls through to the query string.
Native AOT
The source generator ships inside the MinimalEndpoints package as an analyzer. dotnet add package MinimalEndpoints brings it along. Nothing to configure.
using MinimalEndpoints.Generated;
app.MapEndpointGroup().Map(routePrefix: "/api");var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddMinimalEndpoints();
var app = builder.Build();
// A source-generated serializer context: the JSON half of the AOT story.
app.MapEndpointGroup("Aot", AotJson.Default).Map(routePrefix: "/api");
app.Run();
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(GetWidget))]
[JsonSerializable(typeof(WidgetView))]
internal partial class AotJson : JsonSerializerContext;It emits explicit registration naming every endpoint class, a binder per contract that reads each member by name, and an activator that news the endpoint up from request services. Together they remove every reflection path from the request flow.
samples/Aot publishes as an 11 MB native binary with zero IL trim or AOT warnings, verified in CI on every push. That project treats IL2026, IL3050 and the IL207x/IL209x family as errors, so a reflective path creeping back into the generated flow breaks the build rather than the release.
Using the reflective mapper in a trimmed or AOT project reports IL2026 and IL3050 at the call site rather than failing after deployment.
Both paths produce identical endpoints, pinned by a test that maps the same assembly each way and compares. The reflective MapEndpointsFrom is not going away; it is the documented fallback for anyone who cannot run the generator.
Performance
The per-request pipeline resolves serializer metadata once per endpoint, memoizes a binding plan per contract, streams request bodies in one pass where it provably can, and does O(1) route and query lookups. The repository carries a BenchmarkDotNet suite so the claim is checkable rather than aspirational.
benchmarks/ compares raw minimal APIs (the baseline), MinimalEndpoints reflective, MinimalEndpoints generated, and FastEndpoints, in-process with identical semantics. A setup-time conformance pass asserts all four return the same JSON, so a misbinding stack aborts the run rather than winning it.
On the suite's reference run, both MinimalEndpoints paths sit at raw minimal-API parity or better on GET and POST, and the generated path's POST allocations come in slightly below the raw baseline. Results are machine-dependent by nature: run it yourself with dotnet run -c Release --project benchmarks/MinimalEndpoints.Benchmarks.
Every hot-path optimization is pinned by the reflective-vs-generated conformance suite and a 209-test suite asserting responses are byte-identical before and after.
Diagnostics
NE0001Endpoint declares no route attribute, and the generator cannot see whether Configure supplies oneNE0002A contract parameter has a type the binder cannot produce from a request stringNE0003Configure reads constructor-injected state, which is null at map timeNE0004A contract has more than one public constructor, so the binder will throw when the route is first calledNE0005An endpoint derives ApiEndpointBase directly instead of one of the five mappable base types, so no mapper can dispatch itNE0002 is reported only for GET and HEAD. Everywhere else a contract member may come from the JSON body, where any serializable type is fine.
A runtime-registered value binder is invisible to an analyzer, so you tell the build:
[assembly: EndpointValueBinder(typeof(Money))]Errors
Domain exceptions become responses through translators you register, rather than a global filter every part of the application has to agree on. Problems are written as RFC 9457 ProblemDetails through IProblemDetailsService.
public sealed class BillingExceptionTranslator : IEndpointExceptionTranslator
{
public EndpointProblem? Translate(Exception exception) => exception switch
{
InvoiceNotFoundException => EndpointProblem.General(404, "Invoice not found"),
InvoiceLockedException e => EndpointProblem.General(409, e.Message),
_ => null
};
}Unload safety
If you host plugins in collectible AssemblyLoadContexts, endpoint frameworks are usually where unloading goes to die. Process-global registries, static configuration and captured handler MethodInfo all root the assembly you are trying to release — and none of it is visible until you measure.
RequestDelegate, keeping your async state machine out of retained metadata.[Fact]
public void Endpoint_assemblies_are_collected()
{
var evidence = CollectibleEndpointFixture.RunCycles(cycles: 3);
UnloadEvidence.AssertAllCollected(evidence);
}0 of 3
collectible contexts collected with FastEndpoints 7.2.0 in a harness that compiled three endpoint assemblies, served a request, disposed the host and forced repeated full collections. That isolates a composition-level retention problem; it is not a claim about FastEndpoints in any other respect. It is the reason this library exists.
The kit has no dependency on MinimalEndpoints itself, and can be asked to introduce a deliberate leak so you can confirm it still detects one.
Packages
MinimalEndpointsnone, beyond the ASP.NET Core shared frameworkMinimalEndpoints.OpenApiMinimalEndpoints, Microsoft.AspNetCore.OpenApiMinimalEndpoints.TestingMicrosoft.AspNetCore.TestHost, Microsoft.CodeAnalysis.CSharpbuilder.Services.AddOpenApi();
builder.Services.AddMinimalEndpointsOpenApi();Route, query and header parameters appear in the OpenAPI document only with the MinimalEndpoints.OpenApi package.
Compared to FastEndpoints
FastEndpoints is a mature, popular and genuinely good library, and it does considerably more than this one. If you want a batteries-included framework, use it.
What it does not do
Not in 1.0. Use a plain MapPost beside your endpoints.
Bring FluentValidation, DataAnnotations, or hand-written guards.
net10.0 only. A new library targeting .NET 8 would ship dead code.
Documentation