Skip to content

C# source generator for ASP.NET Core Minimal APIs implementing the REPR pattern. Zero reflection, compile-time code generation.

License

Notifications You must be signed in to change notification settings

stephanprobst/Endpointer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

40 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Endpointer

Build Coverage NuGet License: MIT Roslyn .NET 8 .NET 9 .NET 10

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.

Features

  • 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 IIncrementalGenerator API
  • No reflection - Everything resolved at compile time

Quick Start

1. Install the package

dotnet add package Endpointer

2. Create an endpoint

using Microsoft.AspNetCore.Http.HttpResults;

public class GetTimeEndpoint(TimeProvider timeProvider)
{
    public record GetTimeResponse(DateTimeOffset CurrentTime);

    public class Endpoint : IEndpoint
    {
        public void MapEndpoint(IEndpointRouteBuilder endpoints)
        {
            endpoints.MapGet("/time", (GetTimeEndpoint ep) => ep.Handle());
        }
    }

    public Ok<GetTimeResponse> Handle()
    {
        return TypedResults.Ok(new GetTimeResponse(timeProvider.GetUtcNow()));
    }
}

3. Register in Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointer();  // Generated - registers all endpoint classes

var app = builder.Build();

app.MapEndpointer();  // Generated - maps all endpoints

await app.RunAsync();

That's it! The source generator discovers all IEndpoint implementations and generates the registration code.

How It Works

Endpointer uses Roslyn's incremental source generator to:

  1. Discover endpoints - Finds all nested classes implementing IEndpoint
  2. Extract metadata - Captures the outer class name and its primary constructor parameters
  3. Generate registration - Creates extension methods for DI and route mapping

Generated Code

The generator produces two files:

IEndpoint.g.cs - The marker interface:

public interface IEndpoint
{
    void MapEndpoint(IEndpointRouteBuilder endpoints);
}

EndpointerRegistration.g.cs - Extension methods:

public static class EndpointerExtensions
{
    public static IServiceCollection AddEndpointer(this IServiceCollection services)
    {
        services.AddScoped<GetTimeEndpoint>();
        services.AddScoped<GetUserEndpoint>();
        services.AddScoped<CreateUserEndpoint>();
        // ... all discovered endpoints
        return services;
    }

    public static IEndpointRouteBuilder MapEndpointer(this IEndpointRouteBuilder endpoints)
    {
        new GetTimeEndpoint.Endpoint().MapEndpoint(endpoints);
        new GetUserEndpoint.Endpoint().MapEndpoint(endpoints);
        new CreateUserEndpoint.Endpoint().MapEndpoint(endpoints);
        // ... all discovered endpoints
        return endpoints;
    }
}

The REPR Pattern

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 IEndpoint class)
  • Response - Output DTOs (records)
  • Handler - Business logic (methods on outer class)

Requirements

  • .NET 10.0 or later (for the application)
  • The generator itself targets netstandard2.0 for broad compatibility

Building

# Build
dotnet build src/Endpointer.slnx

# Test
dotnet test --solution src/Endpointer.slnx

Full Example

A complete endpoint with request/response DTOs, dependency injection, and OpenAPI metadata:

using Microsoft.AspNetCore.Http.HttpResults;

namespace MyApi.Endpoints.Products;

public class CreateProductEndpoint(IProductRepository repository, ILogger<CreateProductEndpoint> logger)
{
    // Request
    public record CreateProductRequest(
        string Name,
        string Description,
        decimal Price,
        string Category);

    // Response
    public record ProductResponse(
        int Id,
        string Name,
        string Description,
        decimal Price,
        string Category,
        DateTimeOffset CreatedAt);

    // Endpoint
    public class Endpoint : IEndpoint
    {
        public void MapEndpoint(IEndpointRouteBuilder endpoints)
        {
            endpoints.MapPost("/products", (CreateProductEndpoint ep, CreateProductRequest request) => 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();
        }
    }

    // Handler
    public async Task<Results<Created<ProductResponse>, ValidationProblem>> HandleAsync(CreateProductRequest request)
    {
        if (request.Price < 0)
        {
            return TypedResults.ValidationProblem(new Dictionary<string, string[]>
            {
                ["Price"] = ["Price must be greater than or equal to zero."]
            });
        }

        logger.LogInformation("Creating product {Name} in category {Category}", request.Name, request.Category);

        var product = await repository.CreateAsync(request);

        var response = new ProductResponse(
            product.Id,
            product.Name,
            product.Description,
            product.Price,
            product.Category,
            product.CreatedAt);

        return TypedResults.Created($"/products/{response.Id}", response);
    }
}

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

C# source generator for ASP.NET Core Minimal APIs implementing the REPR pattern. Zero reflection, compile-time code generation.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages