Documentation

inbox/Base Duplication Analysis.md

Base Duplication Analysis: Architectural Review and Proposed Solution

Date: 2025-10-18
Status: Draft for Review
Author: Claude (Architectural Analysis)
Stakeholders: Dynaplex Architecture Team


Executive Summary

The current Dynaplex base implementation suffers from significant code duplication between FoundationBuilder and EdgeLiteBuilder, with approximately 85% overlap in infrastructure code and 95% overlap in component orchestration patterns. This duplication violates DRY principles, creates maintenance burden, and indicates a fundamental misalignment with Polylith architectural principles.

Core Issue: Bases have become "orchestration frameworks" rather than thin entry points that compose components.

Recommended Solution: Extract shared infrastructure and component registration patterns into a composable BaseBuilder utility, allowing bases to remain thin orchestrators while eliminating duplication.

Impact:

  • Reduces base code by ~70%
  • Simplifies addition of new deployment scenarios
  • Aligns with Polylith architectural intent
  • Maintains flexibility for deployment-specific customization

Table of Contents

  1. Problem Statement
  2. Current State Analysis
  3. Root Cause Analysis
  4. Architectural Principles Review
  5. Solution Options
  6. Recommended Approach
  7. Implementation Plan
  8. Migration Strategy
  9. Long-term Implications
  10. Decision Criteria

1. Problem Statement

1.1 The Duplication Issue

Current Dynaplex architecture has two base implementations:

  • Foundation: Full-featured deployment with 13+ components
  • EdgeLite: Edge-focused deployment with 7 components + BBU/RFID integration

These bases share:

  • 100% identical infrastructure setup code (~150 lines)
  • 95% identical component registration patterns (~400 lines)
  • 100% identical helper methods
  • 95% identical configuration structures

1.2 Concrete Example

Infrastructure Setup (Identical in both):

// FoundationBuilder.cs:119-144
public FoundationBuilder WithAppConfiguration() {
    if(_isAzPublish) {
        AppConfig = _builder.AddAzureAppConfiguration("config")
            .ConfigureInfrastructure(infra => {
                var appConfigStore = infra.GetProvisionableResources()
                    .OfType<AppConfigurationStore>().Single();
                appConfigStore.SkuName = "developer";
            });
    } else {
        AppConfig = _builder.AddAzureAppConfiguration("config")
            .ConfigureInfrastructure(infra => {
                var appConfigStore = infra.GetProvisionableResources()
                    .OfType<AppConfigurationStore>().Single();
                appConfigStore.SkuName = "developer";
            })
            .RunAsEmulator(settings => { settings.WithDataVolume(); });
    }
    _appConfig = AppConfig;
    return this;
}

// EdgeLiteBuilder.cs:160-183
// EXACT SAME CODE - only difference is return type EdgeLiteBuilder

Component Registration Pattern (95% identical):

// Pattern repeated 13 times in FoundationBuilder, 7 times in EdgeLiteBuilder
public XXXBuilder WithCoreData<TCom, TDbMan>() {
    CoreDataDbMigrator = WithAppInsightsIfAvailable(
        _builder.AddProject<TDbMan>("core-data-migrator")
            .WithReference(Database)
            .WaitFor(Database))
        .PublishAsAzureContainerApp((infra, app) => {
            app.Name = $"ca-{GroupIdentifier}-core-dbm";
        });

    CoreDataComponent = WithAppInsightsIfAvailable(
        _builder.AddProject<TCom>("core-data")
            .WithReference(Database)
            .WaitFor(Database))
        .WithReference(CoreDataDbMigrator)
        .WaitFor(CoreDataDbMigrator)
        .WithHttpEndpoint(name: "core-data-http")
        .PublishAsAzureContainerApp((infra, app) => {
            app.Name = $"ca-{GroupIdentifier}-core-com";
        });

    return this;
}

1.3 Maintenance Impact

This duplication creates:

  • Change Amplification: Bug fixes must be applied to 2+ locations
  • Testing Burden: Identical code paths tested multiple times
  • Inconsistency Risk: Fixes/improvements may diverge between bases
  • Cognitive Load: Developers must understand why duplication exists
  • Onboarding Friction: New team members see duplication and question architecture

1.4 Future Scalability Concerns

If we add more deployment scenarios (e.g., "MicroLite" for minimal deployments, "Analytics" for reporting-focused deployments), we'll have N×M duplication:

  • N deployment types × M infrastructure methods
  • N deployment types × M component patterns
  • Growing maintenance burden

2. Current State Analysis

2.1 Code Structure Breakdown

FoundationBuilder.cs (512 lines)

  • Infrastructure Methods (145 lines): 28% of code

    • WithAppConfiguration(): 26 lines
    • WithAppInsights(): 8 lines
    • WithContainerAppEnvironment(): 14 lines
    • WithDefaultDatabase(): 19 lines
    • WithAppInsightsIfAvailable(): 7 lines
  • Component Registration (340 lines): 66% of code

    • WithCoreData(): 16 lines
    • WithImporter(): 12 lines
    • WithTransport(): 18 lines
    • WithWorkflow(): 22 lines
    • WithIntelligence(): 12 lines
    • WithPrinting(): 18 lines
    • WithCatalog(): 20 lines
    • WithFileService(): 20 lines
    • WithSpatial(): 24 lines
    • WithEdge(): 26 lines
    • WithEvents(): 18 lines
    • WithIdentity(): 18 lines
    • WithSystemEnvironment(): 18 lines
    • WithDocumentation(): 28 lines
  • Validation/Build (27 lines): 5% of code

    • Build() method with required component checks

EdgeLiteBuilder.cs (485 lines)

  • Infrastructure Methods (140 lines): 29% of code (identical to Foundation)
  • Component Registration (310 lines): 64% of code (95% structurally identical)
  • Validation/Build (35 lines): 7% of code (similar structure, different requirements)

2.2 Duplication Categorization

Category Lines Duplicated % of Total Type
Infrastructure Setup ~145 28% Exact Copy
Component Pattern ~320 62% Structural with Variation
Helper Methods ~10 2% Exact Copy
Configuration Classes ~20 4% Near Identical
Validation Logic ~20 4% Similar Structure
Total ~515 100%

2.3 Variation Analysis

Legitimate Differences (code that SHOULD differ):

  1. Required component set

    • Foundation: Requires Edge component
    • EdgeLite: Requires Catalog + Edge + BBU components
  2. Component dependency graphs

    • Spatial in EdgeLite must precede Identity (seeding requirement)
    • Catalog in EdgeLite has MQTT configuration injection
  3. Deployment naming

    • Foundation uses "foundation" identifier
    • EdgeLite uses "edge-lite" identifier

Key Insight: Only ~5% of code represents legitimate variation. The other 95% is incidental duplication.


3. Root Cause Analysis

3.1 Primary Causes

3.1.1 Misunderstanding of "Base" Responsibility

I understand what you're saying in here but you do have to remember that Dynaplex is _inspired_ by Polylith, it's not aiming to _replicate_ it. If I wanted to do that then I would have just used the Polylith architecture.

The whole "base as orchestration engine" idea came from the fact that we know we have "stuff" that is reusuable that we want to put into components, and we know we have customer engagements (projects) that use those things in certain ways. So instead of having to either piece together components every time or duplicate projects and heavily modify them, the bases are supposed to proivde something in the middle that helps devs with that sort of thing and makes their lives a lot easier.

Polylith Definition of Base:

A base is a thin building block that exposes an API to the outside world. It doesn't expose an interface for other bricks to use. Bases expose public APIs (REST, GraphQL, message queues, CLIs) and use components and libraries assembled as a service.

Current Implementation:
Our bases have become orchestration frameworks that:

  • Manage infrastructure provisioning (Azure resources)
  • Define component wiring patterns
  • Handle deployment configuration
  • Manage service dependencies
  • Validate deployment composition

Analysis: We've conflated four distinct responsibilities into bases:

  1. Infrastructure Provider - Azure resources, databases, monitoring
  2. Component Orchestrator - Wiring components with dependencies
  3. Deployment Specification - Which components are required
  4. Configuration Manager - Naming, environment specifics

Only #3 (Deployment Specification) truly belongs in a base per Polylith principles.

I don't think that the bases are being an "infrastructure provider" at all. I think that is very much the domain of the projects and we've made the projects that by being the app host. I also don't think they orchestrate components, they just provide tools for helping you do that. I don't think that's enough of a reason to justify their prominent existance in our architecture but thats why I'm having this discussion lol.

3.1.2 Lack of Abstraction Layer

We jumped directly from "zero Aspire abstraction" to "full builder pattern in each base" without considering:

  • Shared infrastructure patterns
  • Component registration conventions
  • Reusable orchestration helpers

This created a gap where common patterns had no home, forcing duplication.

3.1.3 Aspire API Surface Design

.NET Aspire's IDistributedApplicationBuilder API encourages fluent chaining:

builder.AddProject<T>("name")
    .WithReference(db)
    .WaitFor(db)
    .WithHttpEndpoint()
    .PublishAsAzureContainerApp(...)

This fluent pattern is highly repeatable, making duplication feel natural. Without deliberate abstraction, each base naturally duplicates the pattern.

3.1.4 Incremental Development

Timeline suggests:

  1. FoundationBuilder created first (full-featured deployment)
  2. EdgeLiteBuilder created by copying FoundationBuilder
  3. Modifications made to EdgeLiteBuilder for edge-specific needs
  4. Both evolved independently

Classic "copy-paste-modify" pattern that creates technical debt.

3.2 Contributing Factors

  • Time Pressure: Faster to copy than to abstract properly
  • Unclear Patterns: No established Dynaplex pattern for base abstraction
  • Framework Novelty: .NET Aspire is new; best practices still emerging
  • Legacy Mindset: Coming from Assembly Load Context (ALC) model, may have brought old patterns

3.3 Why This Matters

Violates Key Architectural Principles:

  1. DRY (Don't Repeat Yourself): Same logic in multiple places
  2. Single Responsibility: Bases doing too much
  3. Open/Closed: Can't extend without modifying existing bases
  4. Separation of Concerns: Infrastructure mixed with orchestration

Creates Organizational Risk:

  • Hard to explain to new team members
  • Difficult to share with open-source community
  • Embarrassing when showing architecture to stakeholders
  • Suggests lack of architectural maturity

4. Architectural Principles Review

4.1 Polylith Base Principles

Let's examine how Polylith bases should work:

Principle 1: Thin Entry Points

Polylith: "Bases are thin building blocks that expose APIs to the outside world."

Current State: Our bases are ~500 lines of orchestration logic.

Assessment: ❌ Violated - Bases are fat, not thin

Lol idk I mean if 500 lines is fat then what do you call like literally anything else?

Principle 2: Consumers, Not Providers

Polylith: "Bases don't expose interfaces for other bricks; they use component interfaces."

Current State: Bases provide infrastructure and orchestration patterns to projects.

Assessment: ⚠️ Partially Violated - Bases have become service providers

Principle 3: Composition of Components

Polylith: "Bases use components and libraries assembled as a service."

Current State: Bases orchestrate components via Aspire, but also manage infrastructure.

Assessment: ⚠️ Partially Aligned - Composition exists but obscured by infrastructure concerns

4.2 Dynaplex-Specific Principles

From CLAUDE.md:

Dynaplex encompasses:

  • Component structure with .Abstractions, service implementations, and .ApiClient projects
  • .NET Aspire orchestration for service management
  • Auto-generated API clients using Microsoft Kiota
  • Database-per-service with coordinated migrations
  • Type-safe inter-service communication

Key Question: Where does "Aspire orchestration" live in the Dynaplex architecture?

Current Answer: Inside bases (FoundationBuilder, EdgeLiteBuilder)

Problem: This makes orchestration patterns non-reusable and ties them to specific deployments.

Better Answer: Orchestration patterns should be reusable utilities that bases compose.

There is something about this that is very much resonating with me. Yes, this is kind of where I'm trying to go. The idea is that if, for instance, we want to put together some type of product that is for tracking things using rfid and it doesnt require our whole stack, we might want a base that defines the stuff that is required to make this happen, and then the project would contain the actual app host and the deployment and everything I think. If you're seeing it differently let me know.

4.3 SOLID Principles Applied to Bases

Single Responsibility Principle

Violation: Each base has 4 responsibilities (infrastructure, orchestration, deployment spec, configuration)

Fix: Extract infrastructure and orchestration into separate concerns

Open/Closed Principle

Violation: Adding new deployment types requires copying entire base structure

Fix: Make bases open for extension (composition) but closed for modification

Liskov Substitution Principle

Not Applicable: No inheritance hierarchy (yet)

Interface Segregation Principle

Violation: Bases expose all infrastructure methods whether projects need them or not

Fix: Compose only what's needed for each deployment

Dependency Inversion Principle

Violation: Bases depend on concrete Aspire APIs directly

Fix: Introduce abstraction for infrastructure and component registration

This is a particularly nice inclusion because I do realize that it's a very real possibility that Microsoft abandons aspire. If they do I need a way to migrate us into something else, which shouldn't be terribly difficult since we're essentially just doing container orchestration anyway, but I like the idea of thinking ahead and taking care of this in case it becomes a problem


5. Solution Options

5.1.1 Concept

Extract all shared infrastructure and component patterns into reusable builders that bases compose:

┌─────────────────────────────────────────────────────────────┐
│  Project: Acsis.Dynaplex.Projects.AssetTrakClassic                   │
│  AppHost.cs (Entry Point - ~30 lines)                       │
│  ─────────────────────────────────────────────────────────  │
│  var builder = DistributedApplication.CreateBuilder(args);  │
│  var base = builder.AddFoundationBase()                     │
│                    .WithStandardInfrastructure()            │
│                    .RegisterCoreComponents()                │
│                    .RegisterBusinessComponents()             │
│                    .Build();                                 │
└─────────────────────────────────────────────────────────────┘
                          ↓ composes
┌─────────────────────────────────────────────────────────────┐
│  FoundationBuilder (Deployment Specification - ~100 lines)  │
│  ─────────────────────────────────────────────────────────  │
│  - Declares required components (CoreData, Identity, etc.)  │
│  - Specifies dependency order                               │
│  - Validates deployment composition                         │
│  - Uses InfrastructureBuilder & ComponentRegistry           │
└─────────────────────────────────────────────────────────────┘
          ↓ uses                           ↓ uses
┌───────────────────────────┐    ┌─────────────────────────────┐
│  InfrastructureBuilder    │    │  ComponentRegistry          │
│  (~150 lines)             │    │  (~200 lines)               │
│  ─────────────────────    │    │  ───────────────────────    │
│  - WithAppConfiguration() │    │  - RegisterComponent<T>()   │
│  - WithAppInsights()      │    │  - RegisterWithMigrator<T>()│
│  - WithDatabase()         │    │  - ApplyDependencies()      │
│  - WithContainerEnv()     │    │  - BuildRegistry()          │
└───────────────────────────┘    └─────────────────────────────┘

5.1.2 Code Architecture

New Project: bases/shared/src/Acsis.Bases.Shared/

Acsis.Bases.Shared/
├── InfrastructureBuilder.cs      # Azure/Database/Monitoring setup
├── ComponentRegistry.cs          # Component registration patterns
├── ComponentRegistration.cs      # Fluent component configuration
├── ComponentDependencies.cs      # Dependency graph builder
├── DeploymentValidator.cs        # Validates required components
└── Extensions/
    ├── AspireExtensions.cs       # Aspire helper methods
    └── NamingExtensions.cs       # Azure naming conventions

Yes, I think the idea of shared base infrastructure is nice. It's a good way to get rid of a lot of the duplication.

5.1.3 Implementation Example

InfrastructureBuilder.cs:

namespace Acsis.Bases.Shared;

public class InfrastructureBuilder {
    private readonly IDistributedApplicationBuilder _builder;
    private readonly string _groupIdentifier;
    private readonly bool _isAzurePublish;

    public IResourceBuilder<AzureAppConfigurationResource>? AppConfig { get; private set; }
    public IResourceBuilder<AzureApplicationInsightsResource>? AppInsights { get; private set; }
    public IResourceBuilder<IResourceWithConnectionString>? Database { get; private set; }

    internal InfrastructureBuilder(
        IDistributedApplicationBuilder builder,
        string groupIdentifier)
    {
        _builder = builder;
        _groupIdentifier = groupIdentifier;
        _isAzurePublish = builder.ExecutionContext.IsPublishMode;
    }

    public InfrastructureBuilder WithAppConfiguration() {
        if (_isAzurePublish) {
            AppConfig = _builder.AddAzureAppConfiguration("config")
                .ConfigureInfrastructure(infra => {
                    var appConfigStore = infra.GetProvisionableResources()
                        .OfType<AppConfigurationStore>().Single();
                    appConfigStore.SkuName = "developer";
                });
        } else {
            AppConfig = _builder.AddAzureAppConfiguration("config")
                .ConfigureInfrastructure(infra => {
                    var appConfigStore = infra.GetProvisionableResources()
                        .OfType<AppConfigurationStore>().Single();
                    appConfigStore.SkuName = "developer";
                })
                .RunAsEmulator(settings => { settings.WithDataVolume(); });
        }
        return this;
    }

    public InfrastructureBuilder WithAppInsights() {
        if (_isAzurePublish) {
            AppInsights = _builder.AddAzureApplicationInsights("app-insights");
        }
        return this;
    }

    public InfrastructureBuilder WithContainerAppEnvironment() {
        _builder.AddAzureContainerAppEnvironment("aspire-env")
            .ConfigureInfrastructure(infra => {
                var env = infra.GetProvisionableResources()
                    .OfType<ContainerAppManagedEnvironment>().Single();
                env.Name = $"cae-{_groupIdentifier}";

                var workspace = infra.GetProvisionableResources()
                    .OfType<OperationalInsightsWorkspace>()
                    .SingleOrDefault();

                if (workspace != null) {
                    workspace.Name = $"log-{_groupIdentifier}";
                }
            });
        return this;
    }

    public InfrastructureBuilder WithDefaultDatabase() {
        if (_isAzurePublish) {
            Database = _builder.AddAzurePostgresFlexibleServer("postgres")
                .ConfigureInfrastructure(infra => {
                    var server = infra.GetProvisionableResources()
                        .OfType<PostgreSqlFlexibleServer>().Single();
                    server.Name = $"psql-{_groupIdentifier}-db";
                })
                .AddDatabase("acsis");
        } else {
            Database = _builder.AddPostgres("postgres")
                .WithHostPort(15432)
                .WithDataVolume()
                .WithLifetime(ContainerLifetime.Persistent)
                .AddDatabase("acsis");
        }
        return this;
    }

    /// <summary>
    /// Helper to conditionally add AppInsights reference when deploying to Azure
    /// </summary>
    public IResourceBuilder<T> WithAppInsightsIfAvailable<T>(IResourceBuilder<T> builder)
        where T : IResourceWithEnvironment
    {
        if (AppInsights != null) {
            return builder.WithReference(AppInsights);
        }
        return builder;
    }

    /// <summary>
    /// Creates a ComponentRegistry for registering components
    /// </summary>
    public ComponentRegistry CreateComponentRegistry() {
        if (Database == null) {
            throw new InvalidOperationException(
                "Database must be configured before creating ComponentRegistry. " +
                "Call WithDefaultDatabase() or WithDatabase() first.");
        }

        return new ComponentRegistry(_builder, _groupIdentifier, this);
    }
}

ComponentRegistry.cs:

namespace Acsis.Bases.Shared;

public class ComponentRegistry {
    private readonly IDistributedApplicationBuilder _builder;
    private readonly string _groupIdentifier;
    private readonly InfrastructureBuilder _infrastructure;
    private readonly Dictionary<string, ComponentRegistration> _components = new();

    internal ComponentRegistry(
        IDistributedApplicationBuilder builder,
        string groupIdentifier,
        InfrastructureBuilder infrastructure)
    {
        _builder = builder;
        _groupIdentifier = groupIdentifier;
        _infrastructure = infrastructure;
    }

    /// <summary>
    /// Registers a component with database migrator following standard Dynaplex pattern
    /// </summary>
    public ComponentRegistration RegisterWithMigrator<TComponent, TMigrator>(
        string name,
        string abbreviation,
        Action<ComponentDependencies>? configureDependencies = null)
        where TComponent : IProjectMetadata, new()
        where TMigrator : IProjectMetadata, new()
    {
        // 1. Register migrator
        var migrator = _infrastructure.WithAppInsightsIfAvailable(
            _builder.AddProject<TMigrator>($"{name}-migrator")
                .WithReference(_infrastructure.Database!)
                .WaitFor(_infrastructure.Database!))
            .PublishAsAzureContainerApp((infra, app) => {
                app.Name = $"ca-{_groupIdentifier}-{abbreviation}-dbm";
            });

        // 2. Start component builder
        var componentBuilder = _infrastructure.WithAppInsightsIfAvailable(
            _builder.AddProject<TComponent>(name)
                .WithReference(_infrastructure.Database!)
                .WaitFor(_infrastructure.Database!))
            .WithReference(migrator)
            .WaitFor(migrator);

        // 3. Apply dependencies
        var deps = new ComponentDependencies();
        configureDependencies?.Invoke(deps);

        foreach (var dep in deps.GetDependencies()) {
            if (!_components.TryGetValue(dep.Name, out var depRegistration)) {
                throw new InvalidOperationException(
                    $"Component '{name}' depends on '{dep.Name}', but '{dep.Name}' " +
                    $"has not been registered yet. Register dependencies first.");
            }

            componentBuilder = componentBuilder
                .WithReference(depRegistration.Component)
                .WaitFor(depRegistration.Component);
        }

        // 4. Add HTTP endpoint and Azure publishing
        var component = componentBuilder
            .WithHttpEndpoint(name: $"{abbreviation}-http")
            .PublishAsAzureContainerApp((infra, app) => {
                app.Name = $"ca-{_groupIdentifier}-{abbreviation}-com";
            });

        // 5. Store registration
        var registration = new ComponentRegistration(name, abbreviation, component, migrator);
        _components[name] = registration;

        return registration;
    }

    /// <summary>
    /// Registers a component without database migrator
    /// </summary>
    public ComponentRegistration Register<TComponent>(
        string name,
        string abbreviation,
        Action<ComponentDependencies>? configureDependencies = null)
        where TComponent : IProjectMetadata, new()
    {
        // 1. Start component builder
        var componentBuilder = _infrastructure.WithAppInsightsIfAvailable(
            _builder.AddProject<TComponent>(name)
                .WithReference(_infrastructure.Database!)
                .WaitFor(_infrastructure.Database!));

        // 2. Apply dependencies
        var deps = new ComponentDependencies();
        configureDependencies?.Invoke(deps);

        foreach (var dep in deps.GetDependencies()) {
            if (!_components.TryGetValue(dep.Name, out var depRegistration)) {
                throw new InvalidOperationException(
                    $"Component '{name}' depends on '{dep.Name}', but '{dep.Name}' " +
                    $"has not been registered yet. Register dependencies first.");
            }

            componentBuilder = componentBuilder
                .WithReference(depRegistration.Component)
                .WaitFor(depRegistration.Component);
        }

        // 3. Add HTTP endpoint and Azure publishing
        var component = componentBuilder
            .WithHttpEndpoint(name: $"{abbreviation}-http")
            .PublishAsAzureContainerApp((infra, app) => {
                app.Name = $"ca-{_groupIdentifier}-{abbreviation}-com";
            });

        // 4. Store registration
        var registration = new ComponentRegistration(name, abbreviation, component, null);
        _components[name] = registration;

        return registration;
    }

    /// <summary>
    /// Gets a registered component by name
    /// </summary>
    public ComponentRegistration Get(string name) {
        if (!_components.TryGetValue(name, out var registration)) {
            throw new InvalidOperationException(
                $"Component '{name}' has not been registered.");
        }
        return registration;
    }

    /// <summary>
    /// Validates that required components have been registered
    /// </summary>
    public void ValidateRequiredComponents(params string[] requiredComponents) {
        var missing = requiredComponents.Where(c => !_components.ContainsKey(c)).ToList();

        if (missing.Any()) {
            throw new InvalidOperationException(
                $"Required components not registered: {string.Join(", ", missing)}");
        }
    }
}

/// <summary>
/// Represents a registered component
/// </summary>
public class ComponentRegistration {
    public string Name { get; }
    public string Abbreviation { get; }
    public IResourceBuilder<ProjectResource> Component { get; }
    public IResourceBuilder<ProjectResource>? Migrator { get; }

    internal ComponentRegistration(
        string name,
        string abbreviation,
        IResourceBuilder<ProjectResource> component,
        IResourceBuilder<ProjectResource>? migrator)
    {
        Name = name;
        Abbreviation = abbreviation;
        Component = component;
        Migrator = migrator;
    }
}

/// <summary>
/// Fluent builder for component dependencies
/// </summary>
public class ComponentDependencies {
    private readonly List<(string Name, bool Required)> _dependencies = new();

    public ComponentDependencies DependsOn(string componentName, bool required = true) {
        _dependencies.Add((componentName, required));
        return this;
    }

    internal IEnumerable<(string Name, bool Required)> GetDependencies() => _dependencies;
}

Refactored FoundationBuilder.cs:

namespace Acsis.Bases.Foundation;

public class FoundationBuilder {
    private readonly InfrastructureBuilder _infrastructure;
    private readonly ComponentRegistry _registry;

    // Expose registered components for project usage
    public ComponentRegistration CoreData => _registry.Get("core-data");
    public ComponentRegistration SystemEnvironment => _registry.Get("system-environment");
    public ComponentRegistration Identity => _registry.Get("identity");
    public ComponentRegistration Events => _registry.Get("events");
    public ComponentRegistration Edge => _registry.Get("edge");
    public ComponentRegistration Catalog => _registry.Get("catalog");
    public ComponentRegistration FileService => _registry.Get("file-service");
    public ComponentRegistration Spatial => _registry.Get("spatial");
    public ComponentRegistration Printing => _registry.Get("printing");
    public ComponentRegistration Transport => _registry.Get("transport");
    public ComponentRegistration Workflow => _registry.Get("workflow");
    public ComponentRegistration Intelligence => _registry.Get("intelligence");
    public ComponentRegistration Importer => _registry.Get("importer");

    // Expose infrastructure
    public IResourceBuilder<IResourceWithConnectionString>? Database =>
        _infrastructure.Database;

    internal FoundationBuilder(IDistributedApplicationBuilder builder) {
        var groupIdentifier = "acselrnd"; // Default, can be overridden by config

        _infrastructure = new InfrastructureBuilder(builder, groupIdentifier);
    }

    public FoundationBuilder WithStandardInfrastructure() {
        _infrastructure
            .WithAppConfiguration()
            .WithAppInsights()
            .WithContainerAppEnvironment()
            .WithDefaultDatabase();

        // Create registry after infrastructure is configured
        var registry = _infrastructure.CreateComponentRegistry();
        typeof(FoundationBuilder)
            .GetField("_registry", BindingFlags.NonPublic | BindingFlags.Instance)!
            .SetValue(this, registry);

        return this;
    }

    public FoundationBuilder RegisterCoreComponents() {
        // Core infrastructure components
        _registry.RegisterWithMigrator<
            Acsis_Components_CoreData,
            Acsis_Components_CoreData_DbMigrator>(
            "core-data", "core");

        _registry.RegisterWithMigrator<
            Acsis_Components_SystemEnvironment,
            Acsis_Components_SystemEnvironment_DbMigrator>(
            "system-environment", "senv");

        _registry.RegisterWithMigrator<
            Acsis_Components_Identity,
            Acsis_Components_Identity_DbMigrator>(
            "identity", "iden", deps => deps.DependsOn("core-data"));

        _registry.RegisterWithMigrator<
            Acsis_Components_Events,
            Acsis_Components_Events_DbMigrator>(
            "events", "evnt");

        _registry.RegisterWithMigrator<
            Acsis_Components_Edge,
            Acsis_Components_Edge_DbMigrator>(
            "edge", "edge");

        return this;
    }

    public FoundationBuilder RegisterBusinessComponents() {
        // Business components with dependencies
        _registry.Register<Acsis_Components_Importer>(
            "importer", "impo",
            deps => deps.DependsOn("core-data"));

        _registry.RegisterWithMigrator<
            Acsis_Components_Catalog,
            Acsis_Components_Catalog_DbMigrator>(
            "catalog", "ctlg",
            deps => deps.DependsOn("core-data"));

        _registry.RegisterWithMigrator<
            Acsis_Components_FileService,
            Acsis_Components_FileService_DbMigrator>(
            "file-service", "fsrv",
            deps => deps.DependsOn("core-data"));

        _registry.RegisterWithMigrator<
            Acsis_Components_Spatial,
            Acsis_Components_Spatial_DbMigrator>(
            "spatial", "sptl",
            deps => deps
                .DependsOn("core-data")
                .DependsOn("importer")
                .DependsOn("identity"));

        _registry.RegisterWithMigrator<
            Acsis_Components_Printing,
            Acsis_Components_Printing_DbMigrator>(
            "printing", "prnt");

        _registry.RegisterWithMigrator<
            Acsis_Components_Transport,
            Acsis_Components_Transport_DbMigrator>(
            "transport", "tspo");

        _registry.RegisterWithMigrator<
            Acsis_Components_Workflow,
            Acsis_Components_Workflow_DbMigrator>(
            "workflow", "wflo",
            deps => deps
                .DependsOn("core-data")
                .DependsOn("catalog"));

        _registry.Register<Acsis_Components_Intelligence>(
            "intelligence", "intl");

        return this;
    }

    public IDistributedApplicationBuilder Build() {
        // Validate required components
        _registry.ValidateRequiredComponents(
            "core-data",
            "system-environment",
            "identity",
            "events",
            "edge"
        );

        return _infrastructure._builder; // Expose for project to continue building
    }
}

Refactored EdgeLiteBuilder.cs:

namespace Acsis.Bases.EdgeLite;

public class EdgeLiteBuilder {
    private readonly InfrastructureBuilder _infrastructure;
    private readonly ComponentRegistry _registry;

    // Expose registered components
    public ComponentRegistration CoreData => _registry.Get("core-data");
    public ComponentRegistration SystemEnvironment => _registry.Get("system-environment");
    public ComponentRegistration Identity => _registry.Get("identity");
    public ComponentRegistration Events => _registry.Get("events");
    public ComponentRegistration Edge => _registry.Get("edge");
    public ComponentRegistration Catalog => _registry.Get("catalog");
    public ComponentRegistration Spatial => _registry.Get("spatial");
    public ComponentRegistration Bbu => _registry.Get("bbu");

    public IResourceBuilder<IResourceWithConnectionString>? Database =>
        _infrastructure.Database;

    internal EdgeLiteBuilder(IDistributedApplicationBuilder builder) {
        var groupIdentifier = "acselrnd"; // Default, can be overridden

        _infrastructure = new InfrastructureBuilder(builder, groupIdentifier);
    }

    public EdgeLiteBuilder WithStandardInfrastructure() {
        _infrastructure
            .WithAppConfiguration()
            .WithAppInsights()
            .WithContainerAppEnvironment()
            .WithDefaultDatabase();

        var registry = _infrastructure.CreateComponentRegistry();
        typeof(EdgeLiteBuilder)
            .GetField("_registry", BindingFlags.NonPublic | BindingFlags.Instance)!
            .SetValue(this, registry);

        return this;
    }

    public EdgeLiteBuilder RegisterEdgeLiteComponents() {
        // Core components
        _registry.RegisterWithMigrator<
            Acsis_Components_CoreData,
            Acsis_Components_CoreData_DbMigrator>(
            "core-data", "core");

        _registry.RegisterWithMigrator<
            Acsis_Components_SystemEnvironment,
            Acsis_Components_SystemEnvironment_DbMigrator>(
            "system-environment", "senv");

        // Spatial BEFORE Identity (seeding requirement for EdgeLite)
        _registry.RegisterWithMigrator<
            Acsis_Components_Spatial,
            Acsis_Components_Spatial_DbMigrator>(
            "spatial", "sptl",
            deps => deps
                .DependsOn("core-data")
                .DependsOn("identity"));

        _registry.RegisterWithMigrator<
            Acsis_Components_Identity,
            Acsis_Components_Identity_DbMigrator>(
            "identity", "iden");

        // Events with MQTT configuration (EdgeLite-specific)
        _registry.RegisterWithMigrator<
            Acsis_Components_Events,
            Acsis_Components_Events_DbMigrator>(
            "events", "evnt");

        // Apply EdgeLite-specific MQTT configuration
        ConfigureEventsForMqtt();

        _registry.RegisterWithMigrator<
            Acsis_Components_Catalog,
            Acsis_Components_Catalog_DbMigrator>(
            "catalog", "ctlg",
            deps => deps.DependsOn("core-data"));

        _registry.RegisterWithMigrator<
            Acsis_Components_Edge,
            Acsis_Components_Edge_DbMigrator>(
            "edge", "edge");

        _registry.RegisterWithMigrator<
            Acsis_Components_Bbu,
            Acsis_Components_Bbu_DbMigrator>(
            "bbu", "bbu");

        return this;
    }

    private void ConfigureEventsForMqtt() {
        var eventsComponent = _registry.Get("events").Component;

        // Read MQTT broker configuration from AppHost config
        var mqttConfig = _infrastructure._builder.Configuration.GetSection("MqttBroker");

        if (mqttConfig.Exists()) {
            var mqttName = mqttConfig["Name"];

            if (!string.IsNullOrEmpty(mqttName) && mqttName != "_example") {
                eventsComponent
                    .WithEnvironment("MqttBroker__Name", mqttName)
                    .WithEnvironment("MqttBroker__Port", mqttConfig["Port"] ?? "1883")
                    .WithEnvironment("MqttBroker__EnableWebSockets",
                        mqttConfig["EnableWebSockets"] ?? "false")
                    .WithEnvironment("MqttBroker__WebSocketPort",
                        mqttConfig["WebSocketPort"] ?? "443")
                    .WithEnvironment("MqttBroker__EnableTls",
                        mqttConfig["EnableTls"] ?? "false")
                    .WithEnvironment("MqttBroker__AllowAnonymous",
                        mqttConfig["AllowAnonymous"] ?? "true");
            }
        }

        // Make HTTPS endpoint external for WebSocket MQTT
        eventsComponent.WithExternalHttpEndpoints();
    }

    public IDistributedApplicationBuilder Build() {
        // Validate EdgeLite required components
        _registry.ValidateRequiredComponents(
            "core-data",
            "system-environment",
            "identity",
            "events",
            "catalog",
            "edge"
        );

        return _infrastructure._builder;
    }
}

Project Usage (Simplified):

// projects/assettrak-classic/src/Acsis.Dynaplex.Projects.AssetTrakClassic/AppHost.cs
using Acsis.Bases.Foundation;

var builder = DistributedApplication.CreateBuilder(args);

var foundation = builder.AddFoundation()
    .WithStandardInfrastructure()
    .RegisterCoreComponents()
    .RegisterBusinessComponents();

// Add UI that references foundation components
var frontend = builder.AddNpmApp("assettrak-ui", "../../components/ui", "dev")
    .WithReference(foundation.Database)
    .WithReference(foundation.CoreData.Component)
    .WithReference(foundation.Identity.Component)
    // ... other references
    .WithEnvironment("NEXT_PUBLIC_API_URL", "https://api.example.com");

foundation.Build();
builder.Build().Run();
// projects/bbu-rfid/src/Acsis.Dynaplex.Projects.BbuRfid/AppHost.cs
using Acsis.Bases.EdgeLite;

var builder = DistributedApplication.CreateBuilder(args);

builder.AddEdgeLite()
    .WithStandardInfrastructure()
    .RegisterEdgeLiteComponents()
    .Build();

builder.Build().Run();

5.1.4 Metrics

Before:

  • FoundationBuilder: 512 lines
  • EdgeLiteBuilder: 485 lines
  • Total: 997 lines
  • Duplication: ~85% (~850 lines)

After:

  • InfrastructureBuilder: 150 lines (shared)
  • ComponentRegistry: 200 lines (shared)
  • ComponentRegistration: 50 lines (shared)
  • FoundationBuilder: 120 lines
  • EdgeLiteBuilder: 100 lines
  • Total: 620 lines
  • Duplication: 0%
  • Code Reduction: 38%
  • Shared Code: 65%

5.1.5 Pros

Eliminates all duplication: Infrastructure and patterns extracted once
Aligns with Polylith: Bases become thin orchestrators (~100-120 lines)
Explicit and clear: Project AppHost.cs shows exactly what's being built
Flexible for customization: EdgeLite can still customize (MQTT config example)
Easy to extend: New deployment types compose existing builders
Testable: Infrastructure and registry can be unit tested independently
Maintainable: Bug fixes in one place
Discoverable: Developers see clear separation of concerns
Type-safe: Component dependencies validated at build time

5.1.6 Cons

⚠️ Requires significant refactoring: Both bases need rewrite
⚠️ Learning curve: Team must understand new composition pattern
⚠️ More projects: Adds Acsis.Bases.Shared to solution
⚠️ Breaking change: Existing projects must update their AppHost.cs
⚠️ Initial investment: ~2-3 days of development work


5.2 Option 2: Base Class Inheritance

5.2.1 Concept

Extract shared code to abstract base class:

// bases/core/src/Acsis.Bases.Core/CoreBaseBuilder.cs
public abstract class CoreBaseBuilder {
    protected readonly IDistributedApplicationBuilder _builder;
    protected readonly string _groupIdentifier;
    protected IResourceBuilder<IResourceWithConnectionString>? Database { get; set; }

    // All shared methods
    public CoreBaseBuilder WithAppConfiguration() { /* shared impl */ }
    public CoreBaseBuilder WithAppInsights() { /* shared impl */ }

    protected IResourceBuilder<ProjectResource> RegisterComponentWithMigrator<TCom, TDbMan>(
        string name, string abbreviation) { /* shared impl */ }
}

// FoundationBuilder inherits
public class FoundationBuilder : CoreBaseBuilder {
    public FoundationBuilder WithCoreData<TCom, TDbMan>() {
        CoreDataComponent = RegisterComponentWithMigrator<TCom, TDbMan>("core-data", "core");
        return this;
    }
}

5.2.2 Pros

Simple refactor: Just extract to base class
Familiar pattern: Standard OOP inheritance
Reduces duplication: Infrastructure code shared

5.2.3 Cons

Violates "composition over inheritance": Creates tight coupling
Fragile base class problem: Changes to base affect all derived classes
Return type issues: Base methods return CoreBaseBuilder, not specific type
Hard to test: Must test through derived classes
Less flexible: Can't mix and match infrastructure pieces
Doesn't align with Polylith: Still fat bases

5.2.4 Assessment

⚠️ Not Recommended: Solves duplication but creates new architectural problems

Lol yea I hate this one

---

5.3 Option 3: Convention-Over-Configuration

5.3.1 Concept

Use attributes and reflection for component registration:

[Component("core-data", Abbreviation = "core")]
[RequiresDatabase]
public class CoreData : IProjectMetadata { }

[Component("catalog", Abbreviation = "ctlg")]
[RequiresDatabase]
[DependsOn(typeof(CoreData))]
public class Catalog : IProjectMetadata { }

// Base becomes minimal
var builder = DistributedApplication.CreateBuilder(args);
builder.ScanAndRegisterComponents()
    .RequireComponents("core-data", "identity", "catalog")
    .Build();

5.3.2 Pros

Minimal base code: ~20 lines per base
Self-documenting components: Metadata on component classes
Maximum DRY: Pattern defined once, applied everywhere

5.3.3 Cons

Too much magic: Hard to understand what's happening
Debugging nightmare: Reflection-based registration is hard to debug
Loss of control: Can't customize individual components easily
Doesn't fit all cases: EdgeLite MQTT config doesn't fit pattern
Performance: Reflection at startup
Against .NET trends: Modern .NET favors explicit over implicit

5.3.4 Assessment

Not Recommended: Too clever, sacrifices clarity for brevity

Agreed on this one. I think it's slick but probably more trouble than it's worth.


5.4 Option 4: Do Nothing (Status Quo)

5.4.1 Concept

Accept duplication as cost of explicit control.

5.4.2 Pros

No work required: Zero investment
No risk: No breaking changes
Explicit control: Each base fully controls its orchestration

5.4.3 Cons

Ongoing maintenance burden: Every change duplicated
Architectural debt: Problem compounds with each new base
Poor example: Can't showcase architecture to community
Team confusion: Developers will question why duplication exists
Scalability issues: N×M problem with more deployment types

5.4.4 Assessment

Not Acceptable: Technical debt will compound

Agreed.


6.1 Selected Option

Option 1: Shared Base Infrastructure + Composition

6.2 Rationale

This option best satisfies our decision criteria:

Criterion Score (1-5) Justification
Eliminates Duplication 5 100% of infrastructure shared
Aligns with Polylith 5 Bases become thin orchestrators
Maintains Flexibility 5 Composition allows customization
Easy to Understand 4 Clear separation of concerns
Testability 5 Components testable independently
Extensibility 5 New deployment types compose builders
Migration Effort 3 Requires 2-3 days work
Breaking Changes 2 Projects must update AppHost.cs
Long-term Value 5 Foundation for future growth
TOTAL 39/45 87% score

6.3 Key Benefits

  1. Architectural Alignment

    • Bases become true "thin entry points" per Polylith
    • Clear separation: Infrastructure / Registry / Deployment Spec
    • Composition over inheritance
  2. Code Quality

    • 38% reduction in total code
    • 0% duplication
    • Single source of truth for patterns
  3. Developer Experience

    • Project AppHost.cs is clear and concise
    • Easy to see what's being deployed
    • Type-safe dependency validation
  4. Maintainability

    • Bug fixes in one place
    • New features benefit all bases
    • Easier to explain to new team members
  5. Future Readiness

    • New deployment types trivial to add
    • Can evolve infrastructure without breaking bases
    • Supports advanced scenarios (gradual rollout, A/B testing, etc.)

I agree that this is probably the best of the things that you outlined. I can't help but think that there is some better way to do this. I just really want Dynaplex to embody this dynamic system that is still easy to use, and I think maybe with the additional commentary here you might be able to see where I'm coming from.


7. Implementation Plan

7.1 Phase 1: Foundation (Week 1)

Goal: Create shared infrastructure without breaking existing code

Tasks:

  1. Create Acsis.Bases.Shared project

    • Location: bases/shared/src/Acsis.Bases.Shared/
    • Target: .NET 9.0
    • References: Aspire.Hosting, Azure provisioning packages
  2. Implement InfrastructureBuilder

    • Extract methods from FoundationBuilder
    • Test with local Postgres and Azure emulators
    • Verify Azure publish mode works
  3. Implement ComponentRegistry

    • Create registration pattern abstraction
    • Implement dependency validation
    • Add comprehensive error messages
  4. Implement supporting classes

    • ComponentRegistration
    • ComponentDependencies
    • Extension methods
  5. Write unit tests

    • Mock IDistributedApplicationBuilder
    • Test infrastructure builder methods
    • Test component registration patterns
    • Test dependency validation

Deliverables:

  • Acsis.Bases.Shared project building
  • Unit tests passing (90%+ coverage)
  • Documentation in README

7.2 Phase 2: Foundation Migration (Week 1-2)

Goal: Refactor FoundationBuilder to use shared infrastructure

Tasks:

  1. Create FoundationBuilder.v2.cs (parallel implementation)

    • Implement using InfrastructureBuilder and ComponentRegistry
    • Maintain same public API surface for projects
    • Add XML documentation
  2. Test with assettrak-classic project

    • Update AppHost.cs to use new builder
    • Verify local development works
    • Test all components start successfully
  3. Verify Azure deployment

    • Test azd publish with new builder
    • Verify infrastructure provisioning
    • Verify component deployment
  4. Replace old FoundationBuilder

    • Delete FoundationBuilder.cs
    • Rename FoundationBuilder.v2.cs to FoundationBuilder.cs
    • Update all references

Deliverables:

  • FoundationBuilder using shared infrastructure
  • assettrak-classic project working
  • Azure deployment verified

7.3 Phase 3: EdgeLite Migration (Week 2)

Goal: Refactor EdgeLiteBuilder to use shared infrastructure

Tasks:

  1. Create EdgeLiteBuilder.v2.cs

    • Implement using shared infrastructure
    • Handle EdgeLite-specific customizations (MQTT)
    • Maintain existing API
  2. Test with bbu-rfid project

    • Update AppHost.cs
    • Verify MQTT configuration works
    • Test BBU component integration
  3. Replace old EdgeLiteBuilder

    • Delete old implementation
    • Rename v2 to primary

Deliverables:

  • EdgeLiteBuilder using shared infrastructure
  • bbu-rfid project working
  • MQTT functionality verified

7.4 Phase 4: Documentation & Cleanup (Week 2)

Goal: Document new architecture and clean up

Tasks:

  1. Update architecture documentation

    • Update CLAUDE.md with new base pattern
    • Create docs/architecture/BASES.md
    • Add examples to component development guide
  2. Create migration guide

    • Document for future bases
    • Include examples
    • Explain patterns
  3. Remove old code

    • Delete commented-out code
    • Clean up unused files
    • Remove OldProgram.cs files
  4. Update slash commands/agents

    • Update dynaplex-architect with new patterns
    • Update component-scaffolder to use new base pattern
    • Add validation for base patterns

Deliverables:

  • Updated documentation
  • Clean repository
  • Migration guide

7.5 Timeline

Week 1:
  Mon-Tue: Phase 1 (Shared Infrastructure)
  Wed-Thu: Phase 2 Part 1 (FoundationBuilder v2)
  Fri:     Phase 2 Part 2 (Testing)

Week 2:
  Mon-Tue: Phase 3 (EdgeLiteBuilder)
  Wed:     Phase 4 (Documentation)
  Thu-Fri: Buffer / Polish

Estimated Effort: 8-10 days (1 developer)


8. Migration Strategy

8.1 Backward Compatibility

Breaking Changes:

  • Project AppHost.cs files must be updated
  • Base builder API changes (property names)
  • Build sequence changes

Mitigation:

  1. Create migration guide: Step-by-step for each project type
  2. Provide examples: Before/after for each base
  3. Incremental migration: Migrate one project at a time
  4. Parallel implementations: Keep old builders during transition

8.2 Project Migration Steps

For each project using FoundationBuilder:

Before:

var foundation = builder.AddFoundation()
    .WithAppConfiguration()
    .WithAppInsights()
    .WithContainerAppEnvironment()
    .WithDefaultDatabase()
    .WithCoreData<CoreData, CoreDataMigrator>()
    .WithSystemEnvironment<SysEnv, SysEnvMigrator>()
    .WithIdentity<Identity, IdentityMigrator>()
    // ... 10 more components
    .Build();

var ui = builder.AddNpmApp("ui", "path", "dev")
    .WithReference(foundation.CoreDataComponent)
    .WithReference(foundation.IdentityComponent);

After:

var foundation = builder.AddFoundation()
    .WithStandardInfrastructure()
    .RegisterCoreComponents()
    .RegisterBusinessComponents()
    .Build();

var ui = builder.AddNpmApp("ui", "path", "dev")
    .WithReference(foundation.CoreData.Component)
    .WithReference(foundation.Identity.Component);

Changes:

  1. Infrastructure methods grouped: .WithStandardInfrastructure()
  2. Components registered in batches: .RegisterCoreComponents()
  3. Component access via properties: .CoreData.Component instead of .CoreDataComponent

8.3 Rollback Plan

If migration encounters issues:

  1. Keep old builders in parallel

    • FoundationBuilder.cs (new)
    • FoundationBuilder.Legacy.cs (old)
  2. Feature flag in project

    #if USE_LEGACY_BUILDER
    var foundation = builder.AddFoundationLegacy()...
    #else
    var foundation = builder.AddFoundation()...
    #endif
    
  3. Gradual rollout

    • Start with one project
    • Monitor for issues
    • Roll out to remaining projects

9. Long-term Implications

9.1 Extensibility Scenarios

The new architecture enables future scenarios:

9.1.1 New Deployment Types

Example: MicroLite (Minimal deployment for demos)

public class MicroLiteBuilder {
    public MicroLiteBuilder RegisterMinimalComponents() {
        _registry.RegisterWithMigrator<CoreData, CoreDataMigrator>("core-data", "core");
        _registry.RegisterWithMigrator<Identity, IdentityMigrator>("identity", "iden");
        return this;
    }
}

// Usage
builder.AddMicroLite()
    .WithStandardInfrastructure()
    .RegisterMinimalComponents()
    .Build();

Effort: ~30 minutes to create new base

9.1.2 Environment-Specific Infrastructure

Example: Different database for staging vs production

public InfrastructureBuilder WithEnvironmentDatabase() {
    var env = _builder.Configuration["Environment"];

    if (env == "Production") {
        Database = _builder.AddAzurePostgresFlexibleServer("postgres")
            .ConfigureInfrastructure(/* production config */);
    } else {
        Database = _builder.AddPostgres("postgres")
            .WithDataVolume(); // local dev
    }

    return this;
}

9.1.3 Feature Flags for Components

Example: Conditionally include analytics

var foundation = builder.AddFoundation()
    .WithStandardInfrastructure()
    .RegisterCoreComponents();

if (builder.Configuration.GetValue<bool>("Features:Analytics")) {
    foundation.RegisterAnalyticsComponents();
}

foundation.Build();

9.2 Testing Improvements

9.2.1 Unit Testing Infrastructure

[Fact]
public void InfrastructureBuilder_WithAppConfiguration_ConfiguresCorrectly() {
    // Arrange
    var mockBuilder = new Mock<IDistributedApplicationBuilder>();
    var infra = new InfrastructureBuilder(mockBuilder.Object, "test");

    // Act
    infra.WithAppConfiguration();

    // Assert
    Assert.NotNull(infra.AppConfig);
    mockBuilder.Verify(b => b.AddAzureAppConfiguration("config"), Times.Once);
}

9.2.2 Integration Testing

[Fact]
public async Task ComponentRegistry_RegisterWithMigrator_CreatesComponentAndMigrator() {
    // Arrange
    var builder = DistributedApplication.CreateBuilder(args);
    var infra = new InfrastructureBuilder(builder, "test")
        .WithDefaultDatabase();
    var registry = infra.CreateComponentRegistry();

    // Act
    var registration = registry.RegisterWithMigrator<TestComponent, TestMigrator>(
        "test-component", "test");

    // Assert
    Assert.NotNull(registration.Component);
    Assert.NotNull(registration.Migrator);
}

9.3 Performance Considerations

Current Approach (duplication):

  • Runtime: No impact (same code, just duplicated)
  • Build time: Slightly slower (more code to compile)
  • Memory: Minimal difference

New Approach (composition):

  • Runtime: No impact (same generated IL, just different source)
  • Build time: Slightly faster (less total code)
  • Memory: Minimal difference

Conclusion: Performance impact is negligible

9.4 Community & Open Source

With clean architecture:

  1. Easier to explain: "Bases compose infrastructure and register components"
  2. Example for others: Can be referenced in .NET Aspire discussions
  3. Contribution-friendly: Clear separation makes PRs easier
  4. Documentation: Can create blog posts / talks about pattern

10. Decision Criteria

10.1 Must-Have Requirements

Eliminates code duplication
Maintains deployment flexibility
Aligns with Polylith principles
Type-safe component dependencies
Supports Azure and local development

10.2 Should-Have Requirements

Clear and understandable
Easy to extend with new deployments
Testable in isolation
Good error messages
⚠️ Backward compatible (Breaking change acceptable with migration guide)

10.3 Nice-to-Have Requirements

Reduces total line count
Enables advanced scenarios
Performance neutral
Community-shareable pattern


11. Recommendation

11.1 Proceed with Option 1

Implement Shared Base Infrastructure + Composition

11.2 Decision Factors

  1. Technical Excellence: Best architectural alignment
  2. Long-term Value: Foundation for future growth
  3. Code Quality: Eliminates all duplication
  4. Developer Experience: Clear, composable, testable
  5. Acceptable Trade-offs: Breaking changes manageable with migration guide

11.3 Success Metrics

After implementation:

  • Zero code duplication in bases
  • FoundationBuilder < 150 lines
  • EdgeLiteBuilder < 150 lines
  • All projects migrated
  • Azure deployment verified
  • Documentation complete
  • Team trained on new pattern

11.4 Risk Mitigation

Risk Likelihood Impact Mitigation
Migration breaks projects Medium High Parallel implementations, incremental rollout
Team doesn't understand Low Medium Documentation, examples, training session
Azure deployment issues Low High Test early, verify with azd publish
Timeline overrun Medium Low 2-week buffer built in
Performance regression Very Low Medium Performance testing in Phase 2

12. Next Steps

  1. Review this document with team
  2. Get approval from architecture stakeholders
  3. Create GitHub issue tracking implementation
  4. Begin Phase 1 (Shared Infrastructure)
  5. Weekly check-ins during implementation
  6. Document learnings for future reference

Appendix A: Comparison Table

Aspect Current (Duplication) Option 1 (Composition) Option 2 (Inheritance) Option 3 (Convention)
Lines of Code 997 620 (-38%) 750 (-25%) 300 (-70%)
Duplication % 85% 0% 20% 0%
Clarity ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐
Flexibility ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐
Testability ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐
Extensibility ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
Learning Curve ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
Migration Effort N/A ⭐⭐⭐ (2 weeks) ⭐⭐⭐⭐ (1 week) ⭐⭐ (3 weeks)
Polylith Alignment ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐
Overall Score 18/35 32/35 24/35 19/35

Appendix B: References

  1. Polylith Architecture

  2. Dynaplex Documentation

    • /docs/adrs/007-aspire-microservices-migration.md
    • CLAUDE.md (this repository)
  3. .NET Aspire Documentation

  4. Design Patterns

    • Builder Pattern: Gang of Four
    • Composition over Inheritance: Design Patterns literature
    • Dependency Injection: Martin Fowler

Document Status: Ready for Review
Last Updated: 2025-10-18
Next Review: After team feedback