Metrics, Options and Testing Extensibility¶
This document explains how ConnectSoft templates enable extensibility through extension point patterns. It is written for architects and engineers who need to add domain-specific metrics, configuration options, or testing infrastructure to specialized templates without modifying the base template.
ConnectSoft's extensibility model ensures that base infrastructure remains domain-agnostic while allowing specialized templates to add their own metrics, options, and test patterns through well-defined extension points.
Important
Base infrastructure never contains domain-specific logic. All domain-specific functionality (metrics, options, tests) is added through extension points implemented in specialized templates.
Goals¶
The extensibility architecture is designed to achieve:
- Base Independence - Base template contains no domain-specific code
- Domain Flexibility - Specialized templates add domain logic without modifying base
- Plugin Architecture - Domain functionality plugs into base via interfaces
- Test Reuse - Common test patterns in base, domain-specific tests in specialized templates
- Maintainability - Changes to base don't break specialized templates
Extension Point Philosophy¶
ConnectSoft uses a plugin-style architecture where:
- Base provides infrastructure + extension points (interfaces, abstract classes, registration hooks)
- Specialized templates implement extension points (concrete implementations)
- Base discovers and registers implementations (via scanning or explicit registration)
flowchart TB
subgraph Base["Base Template"]
INFRA[Infrastructure]
EXT[Extension Points<br/>IMetricsFeature<br/>IConfigureOptions]
REG[Registration<br/>Auto-discovery]
end
subgraph Identity["Identity Template"]
IMPL1[IdentityMetricsFeature<br/>implements IMetricsFeature]
IMPL2[ConfigureIdentityOptions<br/>implements IConfigureOptions]
end
INFRA --> EXT
EXT --> REG
IMPL1 -->|Implements| EXT
IMPL2 -->|Implements| EXT
REG -->|Discovers| IMPL1
REG -->|Discovers| IMPL2
style Base fill:#BBDEFB
style Identity fill:#C8E6C9
style EXT fill:#FFE0B2
Metrics Infrastructure and IMetricsFeature¶
Base Metrics Infrastructure¶
The base template provides metrics infrastructure through ConnectSoft.Extensions.Metrics:
Base Setup:
// In ConnectSoft.Extensions.Metrics
public interface IMetricsFeature
{
void Register(IMeterFactory meterFactory);
}
public static class MetricsServiceCollectionExtensions
{
public static IServiceCollection AddConnectSoftMetrics(
this IServiceCollection services)
{
// Setup OpenTelemetry / Meter infrastructure
services.AddOpenTelemetryMetering();
// Auto-discover and register all IMetricsFeature implementations
services.Scan(scan => scan
.FromApplicationDependencies()
.AddClasses(c => c.AssignableTo<IMetricsFeature>())
.AsImplementedInterfaces()
.WithSingletonLifetime());
// Bootstrapper that calls Register() on all features
services.AddHostedService<MetricsFeatureBootstrapper>();
return services;
}
}
Registration in Base Host:
Domain-Specific Metrics Implementation¶
Specialized templates implement IMetricsFeature to add domain-specific metrics:
Identity Metrics Example:
// In Identity template: Identity.Infrastructure/Metrics/IdentityMetricsFeature.cs
using ConnectSoft.Extensions.Metrics;
namespace Identity.Infrastructure.Metrics;
public sealed class IdentityMetricsFeature : IMetricsFeature
{
private readonly Meter _meter;
private readonly Counter<long> _loginSuccessCounter;
private readonly Counter<long> _loginFailedCounter;
private readonly Histogram<double> _loginDuration;
public IdentityMetricsFeature(IMeterFactory meterFactory)
{
_meter = meterFactory.Create("ConnectSoft.Identity");
_loginSuccessCounter = _meter.CreateCounter<long>(
"identity.login.success",
"count",
"Number of successful login attempts");
_loginFailedCounter = _meter.CreateCounter<long>(
"identity.login.failed",
"count",
"Number of failed login attempts");
_loginDuration = _meter.CreateHistogram<double>(
"identity.login.duration",
"ms",
"Login operation duration");
}
public void Register(IMeterFactory meterFactory)
{
// Registration happens automatically via DI
// This method can be used for additional setup if needed
}
// Domain-specific methods
public void OnLoginSuccess(TimeSpan duration)
{
_loginSuccessCounter.Add(1);
_loginDuration.Record(duration.TotalMilliseconds);
}
public void OnLoginFailed(string reason)
{
_loginFailedCounter.Add(1, new KeyValuePair<string, object?>("reason", reason));
}
}
Registration in Identity Infrastructure:
// In Identity.Infrastructure/ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddConnectSoftIdentityInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// Register Identity metrics feature
services.AddSingleton<IMetricsFeature, IdentityMetricsFeature>();
// ... other Identity infrastructure registration
return services;
}
}
Usage in Identity Domain:
// In Identity.Application/UseCases/LoginUseCase.cs
public class LoginUseCase
{
private readonly IdentityMetricsFeature _metrics;
public LoginUseCase(IdentityMetricsFeature metrics)
{
_metrics = metrics;
}
public async Task<LoginResult> ExecuteAsync(LoginRequest request)
{
var stopwatch = Stopwatch.StartNew();
try
{
// ... login logic ...
_metrics.OnLoginSuccess(stopwatch.Elapsed);
return LoginResult.Success();
}
catch (Exception ex)
{
_metrics.OnLoginFailed(ex.Message);
throw;
}
}
}
Metrics Architecture Diagram¶
flowchart TB
subgraph Base["Base Template"]
METRICS[ConnectSoft.Extensions.Metrics]
INTERFACE[IMetricsFeature Interface]
REGISTRATION[Auto-discovery Registration]
end
subgraph Identity["Identity Template"]
IDENTITYMETRICS[IdentityMetricsFeature<br/>implements IMetricsFeature]
LOGINUSE[LoginUseCase<br/>uses IdentityMetricsFeature]
end
subgraph Auth["Auth Template"]
AUTHMETRICS[AuthMetricsFeature<br/>implements IMetricsFeature]
end
METRICS --> INTERFACE
INTERFACE --> REGISTRATION
REGISTRATION -->|Discovers| IDENTITYMETRICS
REGISTRATION -->|Discovers| AUTHMETRICS
IDENTITYMETRICS --> LOGINUSE
style Base fill:#BBDEFB
style Identity fill:#C8E6C9
style Auth fill:#FFF9C4
style INTERFACE fill:#FFE0B2
Options Infrastructure and IOptions¶
Base Options Infrastructure¶
The base template provides options infrastructure through ConnectSoft.Extensions.Options:
Base Setup:
// In ConnectSoft.Extensions.Options
public static class OptionsServiceCollectionExtensions
{
public static IServiceCollection AddConnectSoftOptions(
this IServiceCollection services,
IConfiguration configuration)
{
// Bind generic options
services.Configure<ObservabilityOptions>(
configuration.GetSection("ConnectSoft:Observability"));
services.Configure<MessagingOptions>(
configuration.GetSection("ConnectSoft:Messaging"));
services.Configure<PersistenceOptions>(
configuration.GetSection("ConnectSoft:Persistence"));
// Auto-discover and register all IConfigureOptions<T> implementations
services.Scan(scan => scan
.FromApplicationDependencies()
.AddClasses(c => c.AssignableTo(typeof(IConfigureOptions<>)))
.AsImplementedInterfaces()
.WithTransientLifetime());
return services;
}
}
Registration in Base Host:
Domain-Specific Options Implementation¶
Specialized templates define their own options and configure them:
Identity Options Example:
// In Identity template: Identity.Domain/Options/IdentitySecurityOptions.cs
namespace Identity.Domain.Options;
public sealed class IdentitySecurityOptions
{
public const string SectionName = "ConnectSoft:Identity:Security";
public bool RequireConfirmedEmail { get; set; } = true;
public int MaxFailedAccessAttempts { get; set; } = 5;
public TimeSpan LockoutTimeSpan { get; set; } = TimeSpan.FromMinutes(15);
public int PasswordMinLength { get; set; } = 8;
public bool RequireUppercase { get; set; } = true;
public bool RequireLowercase { get; set; } = true;
public bool RequireDigit { get; set; } = true;
public bool RequireSpecialCharacter { get; set; } = false;
}
Options Configuration:
// In Identity template: Identity.Infrastructure/Options/ConfigureIdentitySecurityOptions.cs
using Microsoft.Extensions.Options;
namespace Identity.Infrastructure.Options;
public sealed class ConfigureIdentitySecurityOptions
: IConfigureOptions<IdentitySecurityOptions>
{
private readonly IConfiguration _configuration;
public ConfigureIdentitySecurityOptions(IConfiguration configuration)
{
_configuration = configuration;
}
public void Configure(IdentitySecurityOptions options)
{
_configuration.GetSection(IdentitySecurityOptions.SectionName).Bind(options);
}
}
Registration in Identity Infrastructure:
// In Identity.Infrastructure/ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddConnectSoftIdentityInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// Options configuration is auto-discovered via IConfigureOptions<T>
// But we can also explicitly register if needed
services.Configure<IdentitySecurityOptions>(
configuration.GetSection(IdentitySecurityOptions.SectionName));
// ... other Identity infrastructure registration
return services;
}
}
Usage in Identity Domain:
// In Identity.Application/UseCases/LoginUseCase.cs
public class LoginUseCase
{
private readonly IOptions<IdentitySecurityOptions> _securityOptions;
public LoginUseCase(IOptions<IdentitySecurityOptions> securityOptions)
{
_securityOptions = securityOptions;
}
public async Task<LoginResult> ExecuteAsync(LoginRequest request)
{
var options = _securityOptions.Value;
// Check lockout
if (await IsLockedOutAsync(request.Email))
{
throw new AccountLockedException(
$"Account locked for {options.LockoutTimeSpan}");
}
// ... login logic ...
}
}
Configuration File:
{
"ConnectSoft": {
"Observability": {
// Base options
},
"Messaging": {
// Base options
},
"Identity": {
"Security": {
"RequireConfirmedEmail": true,
"MaxFailedAccessAttempts": 5,
"LockoutTimeSpan": "00:15:00",
"PasswordMinLength": 8,
"RequireUppercase": true,
"RequireLowercase": true,
"RequireDigit": true,
"RequireSpecialCharacter": false
}
}
}
}
Options Architecture Diagram¶
flowchart TB
subgraph Base["Base Template"]
OPTIONS[ConnectSoft.Extensions.Options]
BASECONFIG[Base Options<br/>ObservabilityOptions<br/>MessagingOptions]
SCAN[Auto-scan<br/>IConfigureOptions]
end
subgraph Identity["Identity Template"]
IDENTITYOPTIONS[IdentitySecurityOptions]
CONFIG[ConfigureIdentitySecurityOptions<br/>implements IConfigureOptions]
USE[LoginUseCase<br/>uses IOptions]
end
OPTIONS --> BASECONFIG
OPTIONS --> SCAN
SCAN -->|Discovers| CONFIG
CONFIG -->|Configures| IDENTITYOPTIONS
IDENTITYOPTIONS --> USE
style Base fill:#BBDEFB
style Identity fill:#C8E6C9
style SCAN fill:#FFE0B2
Base Testing Infrastructure¶
The base template provides reusable testing infrastructure that specialized templates can leverage.
ITestAppFactory Interface¶
Base Interface:
// In Base.Testing.Infrastructure/ITestAppFactory.cs
namespace Base.Testing.Infrastructure;
public interface ITestAppFactory
{
HttpClient CreateClient();
IServiceProvider Services { get; }
void ConfigureWebHost(Action<IWebHostBuilder> configure);
}
AcceptanceTestBase¶
Base Test Class:
// In Base.Testing.Infrastructure/AcceptanceTestBase.cs
namespace Base.Testing.Infrastructure;
[TestClass]
public abstract class AcceptanceTestBase
{
protected abstract ITestAppFactory AppFactory { get; }
[TestMethod]
public async Task Health_endpoint_returns_ok()
{
var client = AppFactory.CreateClient();
var response = await client.GetAsync("/health");
response.EnsureSuccessStatusCode();
}
[TestMethod]
public async Task Swagger_endpoint_is_accessible()
{
var client = AppFactory.CreateClient();
var response = await client.GetAsync("/swagger");
response.EnsureSuccessStatusCode();
}
}
AggregateTestBase¶
Base Aggregate Test Class:
// In Base.Testing.Infrastructure/AggregateTestBase.cs
namespace Base.Testing.Infrastructure;
[TestClass]
public abstract class AggregateTestBase<TAggregate>
where TAggregate : AggregateRoot
{
protected abstract TAggregate CreateAggregate();
[TestMethod]
public void Aggregate_creation_succeeds()
{
var aggregate = CreateAggregate();
Assert.IsNotNull(aggregate);
}
[TestMethod]
public void Aggregate_has_valid_id()
{
var aggregate = CreateAggregate();
Assert.IsNotNull(aggregate.Id);
Assert.AreNotEqual(Guid.Empty, aggregate.Id);
}
}
Identity Testing Strategy¶
Specialized templates implement the base test infrastructure for their domain.
IdentityTestAppFactory¶
Identity Factory Implementation:
// In Identity.AcceptanceTests/IdentityTestAppFactory.cs
using Base.Testing.Infrastructure;
using Microsoft.AspNetCore.Mvc.Testing;
namespace Identity.AcceptanceTests;
public sealed class IdentityTestAppFactory : ITestAppFactory
{
private WebApplicationFactory<Program>? _appFactory;
public IServiceProvider Services => _appFactory?.Services
?? throw new InvalidOperationException("Factory not initialized");
public IdentityTestAppFactory()
{
_appFactory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
// Override configuration for tests
builder.ConfigureAppConfiguration((context, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:DefaultConnection"] =
"Server=(localdb)\\mssqllocaldb;Database=IdentityTest;Trusted_Connection=true",
["ConnectSoft:Identity:Security:RequireConfirmedEmail"] = "false"
});
});
// Override services for tests
builder.ConfigureServices(services =>
{
// Replace real email service with mock
services.Remove(services.First(s =>
s.ServiceType == typeof(IEmailService)));
services.AddSingleton<IEmailService, MockEmailService>();
});
});
}
public HttpClient CreateClient()
{
return _appFactory?.CreateClient()
?? throw new InvalidOperationException("Factory not initialized");
}
public void ConfigureWebHost(Action<IWebHostBuilder> configure)
{
_appFactory = _appFactory?.WithWebHostBuilder(configure)
?? throw new InvalidOperationException("Factory not initialized");
}
}
Identity Acceptance Tests¶
Using Base Test Infrastructure:
// In Identity.AcceptanceTests/IdentityHealthChecksTests.cs
using Base.Testing.Infrastructure;
namespace Identity.AcceptanceTests;
[TestClass]
public class IdentityHealthChecksTests : AcceptanceTestBase
{
private static readonly IdentityTestAppFactory Factory = new();
protected override ITestAppFactory AppFactory => Factory;
[TestMethod]
public async Task Identity_health_endpoint_includes_database_check()
{
var client = AppFactory.CreateClient();
var response = await client.GetAsync("/health");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.IsTrue(content.Contains("database"));
}
}
Domain-Specific Tests:
// In Identity.AcceptanceTests/LoginTests.cs
namespace Identity.AcceptanceTests;
[TestClass]
public class LoginTests
{
private readonly IdentityTestAppFactory _factory = new();
[TestMethod]
public async Task Login_with_valid_credentials_succeeds()
{
var client = _factory.CreateClient();
var request = new LoginRequest
{
Email = "test@example.com",
Password = "ValidPassword123!"
};
var response = await client.PostAsJsonAsync("/api/identity/login", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.IsNotNull(result);
Assert.IsNotNull(result.AccessToken);
}
[TestMethod]
public async Task Login_with_invalid_credentials_fails()
{
var client = _factory.CreateClient();
var request = new LoginRequest
{
Email = "test@example.com",
Password = "WrongPassword"
};
var response = await client.PostAsJsonAsync("/api/identity/login", request);
Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
}
}
Aggregate Tests¶
Using Base Aggregate Test Infrastructure:
// In Identity.UnitTests/UserAggregateTests.cs
using Base.Testing.Infrastructure;
namespace Identity.UnitTests;
[TestClass]
public class UserAggregateTests : AggregateTestBase<User>
{
protected override User CreateAggregate()
{
return new User(
email: "test@example.com",
passwordHash: "hashed_password",
tenantId: Guid.NewGuid());
}
[TestMethod]
public void User_creation_with_valid_email_succeeds()
{
var user = CreateAggregate();
Assert.IsNotNull(user);
Assert.AreEqual("test@example.com", user.Email);
}
[TestMethod]
public void User_change_password_succeeds()
{
var user = CreateAggregate();
var newPasswordHash = "new_hashed_password";
user.ChangePassword(newPasswordHash);
Assert.AreEqual(newPasswordHash, user.PasswordHash);
}
}
Test Infrastructure Reuse Diagram¶
flowchart TB
subgraph Base["Base Testing Infrastructure"]
ITESTFACTORY[ITestAppFactory Interface]
ACCEPTANCE[AcceptanceTestBase]
AGGREGATE[AggregateTestBase]
end
subgraph Identity["Identity Tests"]
IDENTITYFACTORY[IdentityTestAppFactory<br/>implements ITestAppFactory]
IDENTITYACCEPTANCE[IdentityHealthChecksTests<br/>extends AcceptanceTestBase]
USERAGGREGATE[UserAggregateTests<br/>extends AggregateTestBase]
end
ITESTFACTORY --> IDENTITYFACTORY
ACCEPTANCE --> IDENTITYACCEPTANCE
AGGREGATE --> USERAGGREGATE
IDENTITYFACTORY --> IDENTITYACCEPTANCE
style Base fill:#BBDEFB
style Identity fill:#C8E6C9
Rules and Anti-Patterns¶
Do's¶
✅ Implement extension point interfaces (IMetricsFeature, IConfigureOptions<T>)
✅ Use auto-discovery - Let base infrastructure discover your implementations
✅ Extend base test classes - Reuse AcceptanceTestBase, AggregateTestBase
✅ Keep base domain-agnostic - Never add domain logic to base
✅ Use dependency injection - Register implementations via DI
Don'ts¶
❌ Don't modify base code - Never add domain-specific code to base template
❌ Don't duplicate base infrastructure - Use base test infrastructure, don't copy it
❌ Don't bypass extension points - Use interfaces, don't modify base directly
❌ Don't hard-code domain logic - Use options pattern for configuration
❌ Don't create base dependencies on domains - Base should not reference Identity/Audit/etc.
Extension Point Summary¶
| Extension Point | Interface/Pattern | Purpose | Example |
|---|---|---|---|
| Metrics | IMetricsFeature |
Add domain-specific metrics | IdentityMetricsFeature |
| Options | IConfigureOptions<T> |
Configure domain-specific options | ConfigureIdentitySecurityOptions |
| Testing | ITestAppFactory |
Create test HTTP clients | IdentityTestAppFactory |
| Testing | AcceptanceTestBase |
Reuse acceptance test patterns | IdentityHealthChecksTests |
| Testing | AggregateTestBase<T> |
Reuse aggregate test patterns | UserAggregateTests |
Related Documents¶
- Template Layering and Reuse - Overview of the three-layer template architecture
- Template Metadata Composition - How template metadata is composed
- Microservice Template Architecture - Architecture details of the microservice template
- Testing Strategy - General testing strategies and patterns