MintPlayer.AspNetCore.Endpoints 10.0.0

dotnet add package MintPlayer.AspNetCore.Endpoints --version 10.0.0
                    
NuGet\Install-Package MintPlayer.AspNetCore.Endpoints -Version 10.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MintPlayer.AspNetCore.Endpoints" Version="10.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MintPlayer.AspNetCore.Endpoints" Version="10.0.0" />
                    
Directory.Packages.props
<PackageReference Include="MintPlayer.AspNetCore.Endpoints" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MintPlayer.AspNetCore.Endpoints --version 10.0.0
                    
#r "nuget: MintPlayer.AspNetCore.Endpoints, 10.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MintPlayer.AspNetCore.Endpoints@10.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MintPlayer.AspNetCore.Endpoints&version=10.0.0
                    
Install as a Cake Addin
#tool nuget:?package=MintPlayer.AspNetCore.Endpoints&version=10.0.0
                    
Install as a Cake Tool

MintPlayer.AspNetCore.Endpoints

A class-per-endpoint library for ASP.NET Core Minimal APIs with constructor injection, content negotiation, and source-generated endpoint discovery.

Installation

dotnet add package MintPlayer.AspNetCore.Endpoints

The source generator is bundled with the package and works automatically.

Quick start

1. Define an endpoint

using MintPlayer.AspNetCore.Endpoints;

public class HealthCheck : IGetEndpoint
{
    public static string Path => "/health";

    public Task<IResult> HandleAsync(HttpContext httpContext)
        => Task.FromResult(Results.Ok(new { status = "healthy" }));
}

2. Register all endpoints in Program.cs

The source generator creates an extension method named after your assembly. For example, assembly MyApp.Api generates MapMyAppApiEndpoints():

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapMyAppApiEndpoints();

app.Run();

You can override the method name with an assembly attribute:

using MintPlayer.AspNetCore.Endpoints;

[assembly: EndpointsMethodName("MapEndpoints")]

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapEndpoints();

app.Run();

Endpoint levels

The library provides three levels of endpoint abstraction:

Level 1: Raw endpoint (IEndpoint)

Full control over HttpContext. No base class or request binding involved.

public class HealthCheck : IGetEndpoint
{
    public static string Path => "/health";

    public Task<IResult> HandleAsync(HttpContext httpContext)
        => Task.FromResult(Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }));
}

Level 2: Typed request (IEndpoint<TRequest>)

Automatic request binding. The class must be partial so the generator can add the appropriate base class.

  • POST / PUT / PATCH endpoints get content-negotiated body parsing for free (MVC input formatters with JSON fallback).
  • GET / DELETE endpoints must override BindRequestAsync to parse route values, query strings, etc.
public partial class UpdateUser : IPutEndpoint<UpdateUserRequest>, IMemberOf<UsersApi>
{
    public static string Path => "/{id}";

    public override Task<IResult> HandleAsync(UpdateUserRequest request, CancellationToken ct)
    {
        return Task.FromResult(Results.Ok(new { id = request.Id, name = request.Name }));
    }
}
public partial class DeleteUser : IDeleteEndpoint<GetUserRequest>, IMemberOf<UsersApi>
{
    public static string Path => "/{id}";

    protected override ValueTask<GetUserRequest?> BindRequestAsync(HttpContext context)
    {
        var id = int.Parse(context.Request.RouteValues["id"]!.ToString()!);
        return ValueTask.FromResult<GetUserRequest?>(new GetUserRequest(id));
    }

    public override Task<IResult> HandleAsync(GetUserRequest request, CancellationToken ct)
    {
        return Task.FromResult(Results.NoContent());
    }
}

Level 3: Typed request + response (IEndpoint<TRequest, TResponse>)

Same as Level 2, plus the source generator emits .Produces<TResponse>(statusCode) for OpenAPI/Swagger documentation.

Override SuccessStatusCode to change the documented status code (default: 200).

public partial class CreateUser : IPostEndpoint<CreateUserRequest, CreateUserResponse>, IMemberOf<UsersApi>
{
    public static string Path => "/";

    static int IEndpoint<CreateUserRequest, CreateUserResponse>.SuccessStatusCode => 201;

    public override Task<IResult> HandleAsync(CreateUserRequest request, CancellationToken ct)
    {
        var response = new CreateUserResponse(42, request.Name, request.Email);
        return Task.FromResult(Results.Created($"/api/users/42", response));
    }
}

HTTP method interfaces

Convenience interfaces automatically provide the HTTP method. Each comes in three levels:

Interface HTTP method Body parsing
IGetEndpoint / IGetEndpoint<TReq> / IGetEndpoint<TReq, TResp> GET No (manual binding)
IPostEndpoint / IPostEndpoint<TReq> / IPostEndpoint<TReq, TResp> POST Yes
IPutEndpoint / IPutEndpoint<TReq> / IPutEndpoint<TReq, TResp> PUT Yes
IPatchEndpoint / IPatchEndpoint<TReq> / IPatchEndpoint<TReq, TResp> PATCH Yes
IDeleteEndpoint / IDeleteEndpoint<TReq> / IDeleteEndpoint<TReq, TResp> DELETE No (manual binding)

For custom or multiple HTTP methods, implement IEndpoint directly and provide Methods:

public class PreflightEndpoint : IEndpoint
{
    public static string Path => "/api/{**path}";
    public static IEnumerable<string> Methods => ["OPTIONS", "HEAD"];

    public Task<IResult> HandleAsync(HttpContext httpContext)
        => Task.FromResult(Results.Ok());
}

Route groups

Group endpoints under a shared prefix with IEndpointGroup and IMemberOf<TGroup>:

public class UsersApi : IEndpointGroup
{
    public static string Prefix => "/api/users";

    static void IEndpointGroup.Configure(RouteGroupBuilder group)
    {
        group.WithTags("Users");
    }
}

Endpoint paths become relative to the group prefix:

// Resolves to GET /api/users/
public class ListUsers : IGetEndpoint, IMemberOf<UsersApi>
{
    public static string Path => "/";

    public Task<IResult> HandleAsync(HttpContext httpContext)
        => Task.FromResult(Results.Ok(new[] { new { Id = 1, Name = "Alice" } }));
}

// Resolves to GET /api/users/{id}
public partial class GetUser : IGetEndpoint<GetUserRequest, UserResponse>, IMemberOf<UsersApi>
{
    public static string Path => "/{id}";

    protected override ValueTask<GetUserRequest?> BindRequestAsync(HttpContext context)
    {
        var id = int.Parse(context.Request.RouteValues["id"]!.ToString()!);
        return ValueTask.FromResult<GetUserRequest?>(new GetUserRequest(id));
    }

    public override Task<IResult> HandleAsync(GetUserRequest request, CancellationToken ct)
    {
        var user = new UserResponse(request.Id, "Alice", "alice@example.com");
        return Task.FromResult(Results.Ok(user));
    }
}

Nested groups

Groups can be nested at any depth by implementing IMemberOf<TParentGroup> on a group class:

// Root group: /api
public class ApiGroup : IEndpointGroup
{
    public static string Prefix => "/api";
}

// Nested under ApiGroup: /api/users
public class UsersApi : IEndpointGroup, IMemberOf<ApiGroup>
{
    public static string Prefix => "/users";

    static void IEndpointGroup.Configure(RouteGroupBuilder group)
    {
        group.WithTags("Users");
    }
}

// Nested under ApiGroup: /api/products
public class ProductsApi : IEndpointGroup, IMemberOf<ApiGroup>
{
    public static string Prefix => "/products";

    static void IEndpointGroup.Configure(RouteGroupBuilder group)
    {
        group.WithTags("Products");
    }
}

// Resolves to GET /api/users/
public class ListUsers : IGetEndpoint, IMemberOf<UsersApi>
{
    public static string Path => "/";
    // ...
}

// Resolves to GET /api/products/
public class ListProducts : IGetEndpoint, IMemberOf<ProductsApi>
{
    public static string Path => "/";
    // ...
}

The generator emits nested MapGroup calls:

var grp0 = MapGroup<ApiGroup>(app);          // /api
{
    var grp1 = MapGroup<UsersApi>(grp0);      // /api/users
    Map<ListUsers>(grp1, _f0);
}
{
    var grp2 = MapGroup<ProductsApi>(grp0);   // /api/products
    Map<ListProducts>(grp2, _f1);
}

Nesting works at any depth. Each group's Prefix is relative to its parent.

Route configuration

Use the static Configure method to add authorization, caching, rate limiting, CORS, or other endpoint metadata:

public class SecureEndpoint : IGetEndpoint
{
    public static string Path => "/api/secret";

    static void IEndpointBase.Configure(RouteHandlerBuilder builder)
    {
        builder.RequireAuthorization("AdminPolicy");
    }

    public Task<IResult> HandleAsync(HttpContext httpContext)
        => Task.FromResult(Results.Ok("secret data"));
}

Dependency injection

Endpoints are instantiated per-request using ActivatorUtilities, so constructor injection works:

public partial class CreateUser : IPostEndpoint<CreateUserRequest, CreateUserResponse>, IMemberOf<UsersApi>
{
    private readonly IUserService _userService;
    private readonly ILogger<CreateUser> _logger;

    public CreateUser(IUserService userService, ILogger<CreateUser> logger)
    {
        _userService = userService;
        _logger = logger;
    }

    public static string Path => "/";

    public override async Task<IResult> HandleAsync(CreateUserRequest request, CancellationToken ct)
    {
        _logger.LogInformation("Creating user {Name}", request.Name);
        var user = await _userService.CreateAsync(request, ct);
        return Results.Created($"/api/users/{user.Id}", user);
    }
}

Endpoints also support IDisposable and IAsyncDisposable for cleanup:

public partial class GetUser : IGetEndpoint<GetUserRequest, UserResponse>, IMemberOf<UsersApi>
{
    public static string Path => "/{id}";

    // ...

    public override ValueTask DisposeAsync()
    {
        // Cleanup resources
        return ValueTask.CompletedTask;
    }
}

Content negotiation

POST, PUT, and PATCH endpoints automatically support content negotiation:

  1. If AddControllers() or AddMvc() was called, MVC input formatters are used (supports JSON, XML, custom formatters).
  2. Otherwise, falls back to ReadFromJsonAsync<T>().

Override BindRequestAsync in body endpoints for custom binding (e.g., multi-source, form data):

public partial class UploadFile : IPostEndpoint<UploadRequest>
{
    public static string Path => "/api/upload";

    protected override async ValueTask<UploadRequest?> BindRequestAsync(HttpContext context)
    {
        var form = await context.Request.ReadFormAsync();
        return new UploadRequest(form.Files["file"]!, form["description"].ToString());
    }

    public override async Task<IResult> HandleAsync(UploadRequest request, CancellationToken ct)
    {
        // Process upload
        return Results.Ok();
    }
}

Endpoint metadata

The EndpointDescriptor list is available on the generated extensions class for introspection:

// Access all registered endpoint descriptors
var endpoints = MyAppEndpointsExtensions.Endpoints;

foreach (var ep in endpoints)
{
    Console.WriteLine($"{string.Join(", ", ep.Methods)} {ep.Path} -> {ep.HandlerType.Name}");
}

Manual registration

For one-off registrations without the source generator:

app.MapEndpoint<HealthCheck>();

Analyzer diagnostics

The source generator emits diagnostics for common mistakes:

Code Description
MPEP001 Endpoint class implements a typed endpoint interface and must be declared as partial
MPEP002 Endpoint class already has a base class; the generator cannot add the required base class
MPEP003 Endpoint class implements IMemberOf<T> for multiple groups; only one group is allowed

How the source generator works

The source generator discovers all classes implementing IEndpointBase and generates:

  1. Partial class declarations for typed endpoints, adding the appropriate base class (PostEndpoint<T>, GetEndpoint<T>, etc.)
  2. Pre-compiled factory fields using ActivatorUtilities.CreateFactory<T>() for fast endpoint instantiation
  3. A single Map{Name}Endpoints() extension method that registers all endpoints, including route groups
  4. .Produces<TResponse>(statusCode) calls for endpoints with a response type
  5. An Endpoints property listing all registered EndpointDescriptor records

For a typed POST endpoint like:

public partial class CreateUser : IPostEndpoint<CreateUserRequest, CreateUserResponse>, IMemberOf<UsersApi>
{
    // ...
}

The generator emits:

// Partial class with base class
partial class CreateUser : PostEndpoint<CreateUserRequest> { }

// Factory field (pre-compiled, allocated once)
private static readonly ObjectFactory<CreateUser> _f0 =
    ActivatorUtilities.CreateFactory<CreateUser>(Type.EmptyTypes);

// Inside the mapping method
var grp = MapGroup<UsersApi>(app);
var b = Map<CreateUser>(grp, _f0);
Produces<CreateUser, CreateUserRequest, CreateUserResponse>(b);

Full example

Models:

public record CreateUserRequest(string Name, string Email);
public record CreateUserResponse(int Id, string Name, string Email);
public record GetUserRequest(int Id);
public record UserResponse(int Id, string Name, string Email);
public record UpdateUserRequest(int Id, string Name, string Email);

Route groups (nested):

public class ApiGroup : IEndpointGroup
{
    public static string Prefix => "/api";
}

public class UsersApi : IEndpointGroup, IMemberOf<ApiGroup>
{
    public static string Prefix => "/users";

    static void IEndpointGroup.Configure(RouteGroupBuilder group)
    {
        group.WithTags("Users");
    }
}

Endpoints:

public class ListUsers : IGetEndpoint, IMemberOf<UsersApi>
{
    public static string Path => "/";

    public Task<IResult> HandleAsync(HttpContext httpContext)
        => Task.FromResult(Results.Ok(new[]
        {
            new { Id = 1, Name = "Alice", Email = "alice@example.com" },
            new { Id = 2, Name = "Bob", Email = "bob@example.com" },
        }));
}

public partial class GetUser : IGetEndpoint<GetUserRequest, UserResponse>, IMemberOf<UsersApi>
{
    public static string Path => "/{id}";

    protected override ValueTask<GetUserRequest?> BindRequestAsync(HttpContext context)
    {
        var id = int.Parse(context.Request.RouteValues["id"]!.ToString()!);
        return ValueTask.FromResult<GetUserRequest?>(new GetUserRequest(id));
    }

    public override Task<IResult> HandleAsync(GetUserRequest request, CancellationToken ct)
    {
        var user = new UserResponse(request.Id, "Alice", "alice@example.com");
        return Task.FromResult(Results.Ok(user));
    }
}

public partial class CreateUser : IPostEndpoint<CreateUserRequest, CreateUserResponse>, IMemberOf<UsersApi>
{
    public static string Path => "/";
    static int IEndpoint<CreateUserRequest, CreateUserResponse>.SuccessStatusCode => 201;

    public override Task<IResult> HandleAsync(CreateUserRequest request, CancellationToken ct)
    {
        var response = new CreateUserResponse(42, request.Name, request.Email);
        return Task.FromResult(Results.Created($"/api/users/42", response));
    }
}

public partial class UpdateUser : IPutEndpoint<UpdateUserRequest>, IMemberOf<UsersApi>
{
    public static string Path => "/{id}";

    public override Task<IResult> HandleAsync(UpdateUserRequest request, CancellationToken ct)
    {
        return Task.FromResult(Results.Ok(new { id = request.Id, name = request.Name }));
    }
}

public partial class DeleteUser : IDeleteEndpoint<GetUserRequest>, IMemberOf<UsersApi>
{
    public static string Path => "/{id}";

    protected override ValueTask<GetUserRequest?> BindRequestAsync(HttpContext context)
    {
        var id = int.Parse(context.Request.RouteValues["id"]!.ToString()!);
        return ValueTask.FromResult<GetUserRequest?>(new GetUserRequest(id));
    }

    public override Task<IResult> HandleAsync(GetUserRequest request, CancellationToken ct)
    {
        return Task.FromResult(Results.NoContent());
    }
}

Program.cs:

using MintPlayer.AspNetCore.Endpoints;

[assembly: EndpointsMethodName("MapEndpoints")]

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapEndpoints();

app.Run();
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on MintPlayer.AspNetCore.Endpoints:

Package Downloads
MintPlayer.Spark

Low-code .NET framework for building data-driven web applications with minimal boilerplate. Uses PersistentObject pattern to eliminate DTOs and repository layers.

MintPlayer.Spark.Authorization

Optional authorization package for MintPlayer.Spark low-code framework. Provides group-based access control for PersistentObjects and Queries.

MintPlayer.Spark.Replication

Cross-module ETL replication for MintPlayer.Spark using RavenDB ETL tasks and the durable message bus.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.0 779 3/15/2026