A C# source generator for ASP.NET Core Minimal APIs implementing the REPR (Request-Endpoint-Response) pattern.
Endpointer is a thin layer above ASP.NET Core - not a framework. It generates the boilerplate at compile time with no reflection, while you keep full control over your endpoints.
- Zero runtime overhead - All code is generated at compile time
- REPR pattern - Clean separation with Request, Endpoint, and Response in one file
- Automatic DI registration - Primary constructor parameters are auto-registered
- Automatic route mapping - All endpoints discovered and mapped via source generation
- Native ASP.NET Core - Uses
TypedResults,IEndpointRouteBuilder, and standard middleware - Incremental generator - Fast builds with Roslyn's latest
IIncrementalGeneratorAPI - No reflection - Everything resolved at compile time
dotnet add package EndpointerusingMicrosoft.AspNetCore.Http.HttpResults;publicclassGetTimeEndpoint(TimeProvidertimeProvider){publicrecordGetTimeResponse(DateTimeOffsetCurrentTime);publicclassEndpoint:IEndpoint{publicvoidMapEndpoint(IEndpointRouteBuilderendpoints){endpoints.MapGet("/time",(GetTimeEndpointep)=>ep.Handle());}}publicOk<GetTimeResponse>Handle(){returnTypedResults.Ok(newGetTimeResponse(timeProvider.GetUtcNow()));}}varbuilder=WebApplication.CreateBuilder(args);builder.Services.AddEndpointer();// Generated - registers all endpoint classesvarapp=builder.Build();app.MapEndpointer();// Generated - maps all endpointsawaitapp.RunAsync();That's it! The source generator discovers all IEndpoint implementations and generates the registration code.
Endpointer uses Roslyn's incremental source generator to:
- Discover endpoints - Finds all nested classes implementing
IEndpoint - Extract metadata - Captures the outer class name and its primary constructor parameters
- Generate registration - Creates extension methods for DI and route mapping
The generator produces two files:
IEndpoint.g.cs - The marker interface:
publicinterfaceIEndpoint{voidMapEndpoint(IEndpointRouteBuilderendpoints);}EndpointerRegistration.g.cs - Extension methods:
publicstaticclassEndpointerExtensions{publicstaticIServiceCollectionAddEndpointer(thisIServiceCollectionservices){services.AddScoped<GetTimeEndpoint>();services.AddScoped<GetUserEndpoint>();services.AddScoped<CreateUserEndpoint>();// ... all discovered endpointsreturnservices;}publicstaticIEndpointRouteBuilderMapEndpointer(thisIEndpointRouteBuilderendpoints){newGetTimeEndpoint.Endpoint().MapEndpoint(endpoints);newGetUserEndpoint.Endpoint().MapEndpoint(endpoints);newCreateUserEndpoint.Endpoint().MapEndpoint(endpoints);// ... all discovered endpointsreturnendpoints;}}REPR (Request-Endpoint-Response) organizes API code by feature rather than by layer:
Endpoints/
├── Users/
│ ├── CreateUserEndpoint.cs # POST /users
│ ├── GetUserEndpoint.cs # GET /users/{id}
│ ├── UpdateUserEndpoint.cs # PUT /users/{id}
│ └── DeleteUserEndpoint.cs # DELETE /users/{id}
└── Health/
└── HealthEndpoint.cs # GET /health
Each file contains:
- Request - Input DTOs (records)
- Endpoint - Route mapping (nested
IEndpointclass) - Response - Output DTOs (records)
- Handler - Business logic (methods on outer class)
- .NET 10.0 or later (for the application)
- The generator itself targets netstandard2.0 for broad compatibility
# Build
dotnet build src/Endpointer.slnx
# Test
dotnet test --solution src/Endpointer.slnxA complete endpoint with request/response DTOs, dependency injection, and OpenAPI metadata:
usingMicrosoft.AspNetCore.Http.HttpResults;namespaceMyApi.Endpoints.Products;publicclassCreateProductEndpoint(IProductRepositoryrepository,ILogger<CreateProductEndpoint>logger){// RequestpublicrecordCreateProductRequest(stringName,stringDescription,decimalPrice,stringCategory);// ResponsepublicrecordProductResponse(intId,stringName,stringDescription,decimalPrice,stringCategory,DateTimeOffsetCreatedAt);// EndpointpublicclassEndpoint:IEndpoint{publicvoidMapEndpoint(IEndpointRouteBuilderendpoints){endpoints.MapPost("/products",(CreateProductEndpointep,CreateProductRequestrequest)=>ep.HandleAsync(request)).WithName("CreateProduct").WithTags("Products").WithSummary("Create a new product").WithDescription("Creates a new product in the catalog and returns the created product with its assigned ID.").Produces<ProductResponse>(StatusCodes.Status201Created).ProducesValidationProblem().WithOpenApi();}}// HandlerpublicasyncTask<Results<Created<ProductResponse>,ValidationProblem>>HandleAsync(CreateProductRequestrequest){if(request.Price<0){returnTypedResults.ValidationProblem(newDictionary<string,string[]>{["Price"]=["Price must be greater than or equal to zero."]});}logger.LogInformation("Creating product {Name} in category {Category}",request.Name,request.Category);varproduct=awaitrepository.CreateAsync(request);varresponse=newProductResponse(product.Id,product.Name,product.Description,product.Price,product.Category,product.CreatedAt);returnTypedResults.Created($"/products/{response.Id}",response);}}This project is licensed under the MIT License - see the LICENSE file for details.