Skip to content

Extensibility Guide

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.

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;

    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");
    }

    public void Register(IMeterFactory meterFactory)
    {
        // Registration happens automatically via DI
    }

    // Domain-specific methods
    public void OnLoginSuccess()
    {
        _loginSuccessCounter.Add(1);
    }

    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;
    }
}

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"));

        // Auto-discover and register all IConfigureOptions<T> implementations
        services.Scan(scan => scan
            .FromApplicationDependencies()
            .AddClasses(c => c.AssignableTo(typeof(IConfigureOptions<>)))
            .AsImplementedInterfaces()
            .WithTransientLifetime());

        return services;
    }
}

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;
}

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);
    }
}

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();
    }
}

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"
                    });
                });
            });
    }

    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"));
    }
}

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