Skip to content

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:

  1. Base provides infrastructure + extension points (interfaces, abstract classes, registration hooks)
  2. Specialized templates implement extension points (concrete implementations)
  3. 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
Hold "Alt" / "Option" to enable pan & zoom

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:

// In base template Program.cs
builder.Services.AddConnectSoftMetrics();

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
Hold "Alt" / "Option" to enable pan & zoom

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:

// In base template Program.cs
builder.Services.AddConnectSoftOptions(builder.Configuration);

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
Hold "Alt" / "Option" to enable pan & zoom

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
Hold "Alt" / "Option" to enable pan & zoom

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