Documentation

adrs/015-extension-methods-endpoint-registration.md

ADR-015: Extension Methods for Endpoint Registration

Status

Accepted

Context

In a microservices architecture with multiple components, we need a clean and consistent way to register API endpoints during application startup. The registration mechanism should be discoverable, maintainable, and follow .NET conventions while keeping the Program.cs file concise and readable.

Challenges:

  • Program.cs can become cluttered with endpoint registrations
  • Need consistent registration pattern across all services
  • Want to maintain component encapsulation
  • Need to support endpoint configuration and middleware
  • Registration order may matter for some components

Decision

We will use extension methods on IEndpointRouteBuilder to register component endpoints, following the pattern Map{ComponentName}Endpoints().

Pattern:

// In component API class
public static class CoreDataApi
{
    public static void MapCoreDataEndpoints(this IEndpointRouteBuilder endpoints)
    {
        // Register all component endpoints
    }
}

// In Program.cs
app.MapCoreDataEndpoints();

Consequences

Positive

  • Clean Program.cs: Minimal code in startup file
  • Discoverability: IntelliSense shows all available Map* methods
  • Encapsulation: Components control their own registration
  • Consistency: Standard pattern across all components
  • Chaining: Extension methods can be chained fluently
  • .NET Convention: Follows established ASP.NET Core patterns

Negative

  • Global Namespace: Extension methods pollute IEndpointRouteBuilder
  • Name Conflicts: Potential for naming collisions
  • Static Coupling: Cannot mock extension methods
  • Discovery: Must know convention to find registration code

Neutral

  • Convention-Based: Relies on naming convention
  • Compile-Time: Registration happens at compile time
  • No Interface: Cannot enforce implementation through interface

Implementation Notes

Basic Extension Method

namespace Acsis.Dynaplex.Engines.CoreData;

public static class CoreDataApi
{
    /// <summary>
    /// Registers CoreData component endpoints
    /// </summary>
    /// <param name="endpoints">The endpoint route builder</param>
    /// <returns>The endpoint route builder for chaining</returns>
    public static IEndpointRouteBuilder MapCoreDataEndpoints(
        this IEndpointRouteBuilder endpoints)
    {
        var group = endpoints.MapGroup("/api/core-data")
            .WithTags("Core Data");

        group.MapGet("/languages", GetLanguages);
        group.MapPost("/languages", CreateLanguage);

        return endpoints; // Enable chaining
    }
}

Program.cs Usage

var builder = WebApplication.CreateBuilder(args);

// Configure services
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure pipeline
app.UseAuthentication();
app.UseAuthorization();

// Register all component endpoints
app.MapCoreDataEndpoints()
   .MapIdentityEndpoints()
   .MapCatalogEndpoints()
   .MapWorkflowEndpoints();

app.Run();

Advanced Registration with Configuration

public static class IdentityApi
{
    public static IEndpointRouteBuilder MapIdentityEndpoints(
        this IEndpointRouteBuilder endpoints,
        Action<IdentityEndpointOptions>? configure = null)
    {
        var options = new IdentityEndpointOptions();
        configure?.Invoke(options);

        var group = endpoints.MapGroup(options.RoutePrefix)
            .WithTags("Identity");

        if (options.RequireAuthorization)
            group.RequireAuthorization();

        group.MapPost("/login", Login)
            .AllowAnonymous();
        group.MapPost("/refresh", RefreshToken);
        group.MapPost("/logout", Logout);

        return endpoints;
    }
}

// Usage
app.MapIdentityEndpoints(options =>
{
    options.RoutePrefix = "/api/auth";
    options.RequireAuthorization = true;
});

Composite Registration Pattern

public static class DynaplexEndpoints
{
    /// <summary>
    /// Registers all Dynaplex component endpoints
    /// </summary>
    public static IEndpointRouteBuilder MapDynaplexEndpoints(
        this IEndpointRouteBuilder endpoints)
    {
        // Core components
        endpoints.MapCoreDataEndpoints();
        endpoints.MapIdentityEndpoints();

        // Business components
        endpoints.MapCatalogEndpoints();
        endpoints.MapWorkflowEndpoints();
        endpoints.MapEventsEndpoints();

        // Utility components
        endpoints.MapFileServiceEndpoints();
        endpoints.MapLoggingEndpoints();

        return endpoints;
    }
}

// Single line in Program.cs
app.MapDynaplexEndpoints();

Conditional Registration

public static IEndpointRouteBuilder MapDevelopmentEndpoints(
    this IEndpointRouteBuilder endpoints,
    IWebHostEnvironment environment)
{
    if (environment.IsDevelopment())
    {
        endpoints.MapGet("/debug/routes", (EndpointDataSource source) =>
        {
            return source.Endpoints.Select(e => e.DisplayName);
        });
    }

    return endpoints;
}

Registration with Dependencies

public static IEndpointRouteBuilder MapHealthEndpoints(
    this IEndpointRouteBuilder endpoints,
    IServiceProvider serviceProvider)
{
    var healthService = serviceProvider.GetRequiredService<IHealthService>();

    endpoints.MapGet("/health", async () =>
    {
        var result = await healthService.CheckHealthAsync();
        return Results.Ok(result);
    }).WithTags("Health");

    return endpoints;
}

Documentation Standards

/// <summary>
/// Maps API endpoints for the Catalog component.
/// </summary>
/// <param name="endpoints">The endpoint route builder.</param>
/// <returns>The endpoint route builder for chaining.</returns>
/// <remarks>
/// Registers the following endpoint groups:
/// - /api/catalog/assets - Asset management
/// - /api/catalog/types - Asset type management
/// - /api/catalog/categories - Category management
/// </remarks>
public static IEndpointRouteBuilder MapCatalogEndpoints(
    this IEndpointRouteBuilder endpoints)
{
    // Implementation
}

Testing Extension Methods

[Test]
public void MapCoreDataEndpoints_RegistersExpectedRoutes()
{
    // Arrange
    var builder = WebApplication.CreateBuilder();
    var app = builder.Build();

    // Act
    app.MapCoreDataEndpoints();

    // Assert
    var dataSource = app.Services.GetRequiredService<EndpointDataSource>();
    var endpoints = dataSource.Endpoints;

    endpoints.Should().Contain(e =>
        e.DisplayName == "GET /api/core-data/languages");
}

Best Practices

  1. Return IEndpointRouteBuilder to enable method chaining
  2. Use XML documentation to describe registered endpoints
  3. Group related endpoints using MapGroup()
  4. Follow naming convention: MapEndpoints
  5. Keep registration logic simple - delegate complex logic
  6. Support configuration through optional parameters
  7. Log registration for debugging in development
  • ADR-013: Minimal APIs with Static Classes (where extensions are defined)
  • ADR-016: Endpoint Grouping and Tagging Strategy (how endpoints are organized)
  • ADR-023: Component Service Registration Pattern (overall registration pattern)