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
- Return IEndpointRouteBuilder to enable method chaining
- Use XML documentation to describe registered endpoints
- Group related endpoints using MapGroup()
- Follow naming convention: MapEndpoints
- Keep registration logic simple - delegate complex logic
- Support configuration through optional parameters
- Log registration for debugging in development
Related ADRs
- 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)