Documentation
adrs/023-component-service-registration.md
ADR-023: Component Service Registration Pattern
Status
Accepted
Context
The Dynaplex architecture requires a consistent pattern for registering component services, endpoints, and dependencies during application startup. With multiple components each having their own services, repositories, validators, and configurations, we need a standardized approach that maintains consistency while allowing flexibility.
Requirements:
- Clear separation of concerns between components
- Consistent registration pattern across all services
- Support for component-specific configuration
- Easy to understand and maintain
- Testable service registration
Decision
We will use the MapAcsisEndpoints convention as the standard pattern for component service registration, combining endpoint mapping with service configuration in a unified extension method approach.
Pattern:
// In Program.cs
app.MapAcsisEndpoints(endpoints =>
{
endpoints.MapCoreDataEndpoints();
endpoints.MapIdentityEndpoints();
endpoints.MapCatalogEndpoints();
});
Consequences
Positive
- Consistency: Single pattern for all component registration
- Discoverability: All registrations in one place
- Modularity: Components are self-contained
- Flexibility: Components control their own registration
- Testability: Registration can be tested in isolation
- Convention: Follows ASP.NET Core patterns
Negative
- Hidden Complexity: Service registration hidden in extension methods
- Order Dependencies: Some registrations may depend on order
- Debugging: Harder to debug registration issues
- Documentation: Must document what each registration does
Neutral
- Convention-Based: Relies on naming conventions
- Extension Methods: Uses C# extension method pattern
- Component Coupling: Components must follow the pattern
Implementation Notes
Core Registration Extension
namespace Acsis.Dynaplex.Projects.BbuRfid.ServiceDefaults;
public static class AcsisEndpointExtensions
{
/// <summary>
/// Maps all Acsis component endpoints and configures Scalar API documentation
/// </summary>
public static IEndpointRouteBuilder MapAcsisEndpoints(
this IEndpointRouteBuilder endpoints,
Action<IEndpointRouteBuilder> configureEndpoints)
{
// Configure component endpoints
configureEndpoints(endpoints);
// Add Scalar API documentation
endpoints.MapScalarApiReference(options =>
{
options.Title = "Dynaplex API";
options.Theme = ScalarTheme.Saturn;
options.DarkMode = true;
options.RoutePrefix = "/scalar";
});
// Add OpenAPI endpoint
endpoints.MapOpenApi();
// Add health checks
endpoints.MapHealthChecks("/health");
return endpoints;
}
}
Component Registration Pattern
namespace Acsis.Dynaplex.Engines.Catalog;
public static class CatalogServiceExtensions
{
/// <summary>
/// Adds Catalog component services to the DI container
/// </summary>
public static IServiceCollection AddCatalogServices(
this IServiceCollection services,
IConfiguration configuration)
{
// Add database context
services.AddDbContext<CatalogDb>(options =>
{
options.UseSqlServer(configuration.GetConnectionString("catalog"));
});
// Add repositories
services.AddScoped<IAssetRepository, AssetRepository>();
services.AddScoped<IAssetTypeRepository, AssetTypeRepository>();
// Add services
services.AddScoped<IAssetService, AssetService>();
services.AddScoped<IAssetTypeService, AssetTypeService>();
// Add validators
services.AddScoped<IValidator<CreateAssetRequest>, CreateAssetValidator>();
services.AddScoped<IValidator<UpdateAssetRequest>, UpdateAssetValidator>();
// Add API client for external calls
services.AddHttpClient<CatalogApiClient>(client =>
{
client.BaseAddress = new Uri(configuration["Services:Catalog:Url"]);
});
// Add caching
services.AddMemoryCache();
services.AddSingleton<ICatalogCache, CatalogCache>();
// Add background services
services.AddHostedService<AssetSyncService>();
return services;
}
/// <summary>
/// Maps Catalog component endpoints
/// </summary>
public static IEndpointRouteBuilder MapCatalogEndpoints(
this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/catalog")
.WithTags("Catalog")
.RequireAuthorization();
// Asset endpoints
group.MapAssetEndpoints();
// Asset Type endpoints
group.MapAssetTypeEndpoints();
// Category endpoints
group.MapCategoryEndpoints();
return endpoints;
}
}
Complete Program.cs Example
using Acsis.Dynaplex.Projects.BbuRfid.ServiceDefaults;
var builder = WebApplication.CreateBuilder(args);
// Add service defaults (Aspire integration)
builder.AddServiceDefaults();
// Add component services
builder.Services.AddCoreDataServices(builder.Configuration);
builder.Services.AddIdentityServices(builder.Configuration);
builder.Services.AddCatalogServices(builder.Configuration);
builder.Services.AddWorkflowServices(builder.Configuration);
// Add API services
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument();
// Add authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
builder.Services.AddAuthorization();
// Add CORS if needed
if (EnvironmentDetector.IsRunningInContainer)
{
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins(builder.Configuration["Cors:Origins"])
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
}
var app = builder.Build();
// Configure pipeline
app.UseAuthentication();
app.UseAuthorization();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
if (EnvironmentDetector.IsRunningInContainer)
{
app.UseCors();
}
// Map all component endpoints
app.MapAcsisEndpoints(endpoints =>
{
endpoints.MapCoreDataEndpoints();
endpoints.MapIdentityEndpoints();
endpoints.MapCatalogEndpoints();
endpoints.MapWorkflowEndpoints();
endpoints.MapEventsEndpoints();
endpoints.MapFileServiceEndpoints();
});
app.Run();
Service Registration Validation
public static class ServiceRegistrationValidator
{
public static IServiceCollection ValidateServices(
this IServiceCollection services)
{
var serviceProvider = services.BuildServiceProvider();
// Validate critical services are registered
ValidateService<CatalogDb>(serviceProvider, "CatalogDb");
ValidateService<IAssetService>(serviceProvider, "IAssetService");
ValidateService<IAssetRepository>(serviceProvider, "IAssetRepository");
return services;
}
private static void ValidateService<T>(IServiceProvider provider, string name)
{
var service = provider.GetService<T>();
if (service == null)
{
throw new InvalidOperationException(
$"Required service {name} is not registered");
}
}
}
// Use in Program.cs
builder.Services
.AddCatalogServices(builder.Configuration)
.ValidateServices();
Component Configuration Pattern
public class CatalogOptions
{
public const string SectionName = "Catalog";
public string ConnectionString { get; set; } = string.Empty;
public int CacheExpirationMinutes { get; set; } = 60;
public bool EnableBackgroundSync { get; set; } = true;
public int MaxPageSize { get; set; } = 100;
}
public static class CatalogServiceExtensions
{
public static IServiceCollection AddCatalogServices(
this IServiceCollection services,
IConfiguration configuration)
{
// Bind configuration
services.Configure<CatalogOptions>(
configuration.GetSection(CatalogOptions.SectionName));
// Register with options
services.AddSingleton<IValidateOptions<CatalogOptions>,
CatalogOptionsValidator>();
// Use options in services
services.AddScoped<IAssetService>(provider =>
{
var options = provider.GetRequiredService<IOptions<CatalogOptions>>();
var db = provider.GetRequiredService<CatalogDb>();
var cache = provider.GetRequiredService<IMemoryCache>();
return new AssetService(db, cache, options.Value);
});
return services;
}
}
Testing Service Registration
[TestFixture]
public class CatalogRegistrationTests
{
[Test]
public void AddCatalogServices_RegistersAllRequiredServices()
{
// Arrange
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["ConnectionStrings:catalog"] = "Server=test;Database=test;"
})
.Build();
// Act
services.AddCatalogServices(configuration);
var provider = services.BuildServiceProvider();
// Assert
provider.GetService<CatalogDb>().Should().NotBeNull();
provider.GetService<IAssetService>().Should().NotBeNull();
provider.GetService<IAssetRepository>().Should().NotBeNull();
}
[Test]
public void MapCatalogEndpoints_RegistersExpectedEndpoints()
{
// Arrange
var builder = WebApplication.CreateBuilder();
var app = builder.Build();
// Act
app.MapCatalogEndpoints();
// Assert
var dataSource = app.Services.GetRequiredService<EndpointDataSource>();
var endpoints = dataSource.Endpoints;
endpoints.Should().Contain(e => e.DisplayName.Contains("assets"));
endpoints.Should().Contain(e => e.DisplayName.Contains("types"));
}
}
Startup Diagnostics
public static class StartupDiagnostics
{
public static IApplicationBuilder UseStartupDiagnostics(
this IApplicationBuilder app,
ILogger<Program> logger)
{
var services = app.ApplicationServices;
// Log registered services
logger.LogInformation("=== Registered Services ===");
LogService<CatalogDb>(services, logger, "CatalogDb");
LogService<IAssetService>(services, logger, "AssetService");
// Log registered endpoints
var endpointDataSource = services.GetRequiredService<EndpointDataSource>();
logger.LogInformation("=== Registered Endpoints ===");
foreach (var endpoint in endpointDataSource.Endpoints)
{
if (endpoint is RouteEndpoint routeEndpoint)
{
logger.LogInformation("Endpoint: {Pattern} [{Methods}]",
routeEndpoint.RoutePattern.RawText,
string.Join(", ", routeEndpoint.Metadata
.OfType<HttpMethodMetadata>()
.SelectMany(m => m.HttpMethods)));
}
}
return app;
}
private static void LogService<T>(IServiceProvider services, ILogger logger, string name)
{
var service = services.GetService<T>();
logger.LogInformation("Service {Name}: {Status}",
name,
service != null ? "Registered" : "NOT REGISTERED");
}
}
Best Practices
- Group related registrations in component-specific extension methods
- Validate critical services during startup
- Use options pattern for component configuration
- Document dependencies in XML comments
- Test registration with unit tests
- Log registration in development for debugging
- Order matters - register dependencies before dependents
Related ADRs
- ADR-013: Minimal APIs with Static Classes (endpoint definition)
- ADR-015: Extension Methods for Endpoint Registration (registration pattern)
- ADR-007: Migration to .NET Aspire Microservices (service architecture)