Skip to content

ADR-0003: Metrics, Options and Testing Extensibility

  • Status: accepted
  • Deciders: ConnectSoft Architecture Team
  • Date: 2026-01-15

Context and Problem Statement

ConnectSoft templates need to support domain-specific functionality (metrics, configuration options, testing patterns) without modifying the base template. Specialized templates (Identity, Auth, Audit, etc.) need to:

  • Add domain-specific metrics (login success/failure counters, authentication duration, etc.)
  • Define domain-specific configuration options (password policies, lockout settings, etc.)
  • Implement domain-specific tests while reusing base test infrastructure

The base template must remain domain-agnostic and never contain Identity, Auth, or Audit-specific logic.

Decision Drivers

  • Base template must remain domain-agnostic
  • Specialized templates need to add domain functionality without modifying base
  • Need for plugin-style architecture where domains plug into base infrastructure
  • Requirement to reuse common test patterns while allowing domain-specific tests
  • Need for auto-discovery of domain implementations

Considered Options

Option 1: Modify Base Template for Each Domain

Approach: Add Identity/Auth/Audit-specific code directly to base template.

Pros: - Simple to implement - All functionality in one place

Cons: - Base template becomes domain-specific - Violates separation of concerns - Base template grows unbounded - New domains require base template changes - Rejected - Violates core principle of domain-agnostic base

Option 2: Copy Base Infrastructure in Each Template

Approach: Each specialized template copies and modifies base metrics/options infrastructure.

Pros: - Templates are independent - No base template modifications

Cons: - Code duplication - Base improvements don't propagate - Maintenance burden - Rejected - Creates duplication problem

Option 3: Extension Point Interfaces (Chosen)

Approach: Base provides extension point interfaces, specialized templates implement them.

Pros: - Base remains domain-agnostic - Domain functionality plugs into base via interfaces - Auto-discovery via dependency injection scanning - Base improvements benefit all domains - Clear separation of concerns

Cons: - Requires understanding of extension point pattern - Slightly more complex than direct modification

Option 4: Configuration-Based Plugins

Approach: Domain functionality configured via JSON/YAML, loaded dynamically.

Pros: - Very flexible - No code changes for new domains

Cons: - Complex runtime loading - Type safety issues - Difficult to test - Overkill for our use case

Decision Outcome

Chosen option: Option 3: Extension Point Interfaces, because it provides:

  • Domain-agnostic base template
  • Plugin-style architecture
  • Type-safe extensions
  • Auto-discovery via DI scanning
  • Reusable test infrastructure

Metrics Extension Point: IMetricsFeature

Base Infrastructure:

public interface IMetricsFeature
{
    void Register(IMeterFactory meterFactory);
}

Base Registration:

services.Scan(scan => scan
    .FromApplicationDependencies()
    .AddClasses(c => c.AssignableTo<IMetricsFeature>())
    .AsImplementedInterfaces()
    .WithSingletonLifetime());

Domain Implementation:

public sealed class IdentityMetricsFeature : IMetricsFeature
{
    public void Register(IMeterFactory meterFactory)
    {
        // Register Identity-specific metrics
    }
}

Options Extension Point: IConfigureOptions

Base Infrastructure:

services.Scan(scan => scan
    .FromApplicationDependencies()
    .AddClasses(c => c.AssignableTo(typeof(IConfigureOptions<>)))
    .AsImplementedInterfaces());

Domain Implementation:

public sealed class ConfigureIdentitySecurityOptions 
    : IConfigureOptions<IdentitySecurityOptions>
{
    public void Configure(IdentitySecurityOptions options)
    {
        // Configure Identity-specific options
    }
}

Testing Extension Point: ITestAppFactory

Base Infrastructure:

public interface ITestAppFactory
{
    HttpClient CreateClient();
    IServiceProvider Services { get; }
}

Base Test Classes:

public abstract class AcceptanceTestBase
{
    protected abstract ITestAppFactory AppFactory { get; }
    // Base test methods
}

Domain Implementation:

public sealed class IdentityTestAppFactory : ITestAppFactory
{
    // Identity-specific test setup
}

public class IdentityHealthChecksTests : AcceptanceTestBase
{
    protected override ITestAppFactory AppFactory => new IdentityTestAppFactory();
}

Positive Consequences

  • Domain-Agnostic Base - Base template contains no Identity/Auth/Audit logic
  • Plugin Architecture - Domains plug into base via well-defined interfaces
  • Auto-Discovery - Base infrastructure automatically discovers domain implementations
  • Type Safety - Extension points are type-safe interfaces
  • Test Reuse - Base test infrastructure reused by all domains
  • Maintainability - Base improvements benefit all domains automatically
  • Flexibility - New domains can be added without modifying base

Negative Consequences

  • Learning Curve - Team must understand extension point pattern
  • Interface Design - Extension point interfaces must be well-designed
  • DI Scanning - Requires dependency injection scanning (performance consideration)
  • Documentation - Must document extension points clearly

Implementation Notes

Metrics Extension Pattern

  1. Base provides IMetricsFeature interface
  2. Base auto-discovers implementations via DI scanning
  3. Domain implements IMetricsFeature with domain-specific metrics
  4. Base bootstrapper calls Register() on all implementations

Options Extension Pattern

  1. Base provides IConfigureOptions<T> scanning
  2. Domain defines options class (e.g., IdentitySecurityOptions)
  3. Domain implements IConfigureOptions<IdentitySecurityOptions>
  4. Base auto-discovers and registers configuration

Testing Extension Pattern

  1. Base provides ITestAppFactory interface and AcceptanceTestBase class
  2. Domain implements ITestAppFactory with domain-specific test setup
  3. Domain tests extend AcceptanceTestBase and provide factory
  4. Base test methods (health checks, etc.) work automatically

Rules

  • Base never references domains - Base should not have using Identity.* or similar
  • Domains implement extension points - All domain functionality via interfaces
  • Auto-discovery preferred - Use DI scanning, not manual registration
  • Test infrastructure reusable - Base provides test helpers, domains use them

Alternatives Considered

See "Considered Options" section above. We evaluated modifying base template, copying infrastructure, and configuration-based plugins, but extension point interfaces provided the best balance.