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
- Problem Statement
- Current State Analysis
- Root Cause Analysis
- Architectural Principles Review
- Solution Options
- Recommended Approach
- Implementation Plan
- Migration Strategy
- Long-term Implications
- 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 linesWithAppInsights(): 8 linesWithContainerAppEnvironment(): 14 linesWithDefaultDatabase(): 19 linesWithAppInsightsIfAvailable(): 7 lines
Component Registration (340 lines): 66% of code
WithCoreData(): 16 linesWithImporter(): 12 linesWithTransport(): 18 linesWithWorkflow(): 22 linesWithIntelligence(): 12 linesWithPrinting(): 18 linesWithCatalog(): 20 linesWithFileService(): 20 linesWithSpatial(): 24 linesWithEdge(): 26 linesWithEvents(): 18 linesWithIdentity(): 18 linesWithSystemEnvironment(): 18 linesWithDocumentation(): 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):
Required component set
- Foundation: Requires Edge component
- EdgeLite: Requires Catalog + Edge + BBU components
Component dependency graphs
- Spatial in EdgeLite must precede Identity (seeding requirement)
- Catalog in EdgeLite has MQTT configuration injection
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
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:
- Infrastructure Provider - Azure resources, databases, monitoring
- Component Orchestrator - Wiring components with dependencies
- Deployment Specification - Which components are required
- 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:
- FoundationBuilder created first (full-featured deployment)
- EdgeLiteBuilder created by copying FoundationBuilder
- Modifications made to EdgeLiteBuilder for edge-specific needs
- 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:
- DRY (Don't Repeat Yourself): Same logic in multiple places
- Single Responsibility: Bases doing too much
- Open/Closed: Can't extend without modifying existing bases
- 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.ApiClientprojects- .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 Option 1: Shared Base Infrastructure + Composition (RECOMMENDED)
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. Recommended Approach
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
Architectural Alignment
- Bases become true "thin entry points" per Polylith
- Clear separation: Infrastructure / Registry / Deployment Spec
- Composition over inheritance
Code Quality
- 38% reduction in total code
- 0% duplication
- Single source of truth for patterns
Developer Experience
- Project AppHost.cs is clear and concise
- Easy to see what's being deployed
- Type-safe dependency validation
Maintainability
- Bug fixes in one place
- New features benefit all bases
- Easier to explain to new team members
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:
Create
Acsis.Bases.Sharedproject- Location:
bases/shared/src/Acsis.Bases.Shared/ - Target: .NET 9.0
- References: Aspire.Hosting, Azure provisioning packages
- Location:
Implement
InfrastructureBuilder- Extract methods from FoundationBuilder
- Test with local Postgres and Azure emulators
- Verify Azure publish mode works
Implement
ComponentRegistry- Create registration pattern abstraction
- Implement dependency validation
- Add comprehensive error messages
Implement supporting classes
ComponentRegistrationComponentDependencies- Extension methods
Write unit tests
- Mock
IDistributedApplicationBuilder - Test infrastructure builder methods
- Test component registration patterns
- Test dependency validation
- Mock
Deliverables:
Acsis.Bases.Sharedproject 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:
Create
FoundationBuilder.v2.cs(parallel implementation)- Implement using
InfrastructureBuilderandComponentRegistry - Maintain same public API surface for projects
- Add XML documentation
- Implement using
Test with assettrak-classic project
- Update AppHost.cs to use new builder
- Verify local development works
- Test all components start successfully
Verify Azure deployment
- Test
azd publishwith new builder - Verify infrastructure provisioning
- Verify component deployment
- Test
Replace old FoundationBuilder
- Delete
FoundationBuilder.cs - Rename
FoundationBuilder.v2.cstoFoundationBuilder.cs - Update all references
- Delete
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:
Create
EdgeLiteBuilder.v2.cs- Implement using shared infrastructure
- Handle EdgeLite-specific customizations (MQTT)
- Maintain existing API
Test with bbu-rfid project
- Update AppHost.cs
- Verify MQTT configuration works
- Test BBU component integration
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:
Update architecture documentation
- Update
CLAUDE.mdwith new base pattern - Create
docs/architecture/BASES.md - Add examples to component development guide
- Update
Create migration guide
- Document for future bases
- Include examples
- Explain patterns
Remove old code
- Delete commented-out code
- Clean up unused files
- Remove
OldProgram.csfiles
Update slash commands/agents
- Update
dynaplex-architectwith new patterns - Update
component-scaffolderto use new base pattern - Add validation for base patterns
- Update
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.csfiles must be updated - Base builder API changes (property names)
- Build sequence changes
Mitigation:
- Create migration guide: Step-by-step for each project type
- Provide examples: Before/after for each base
- Incremental migration: Migrate one project at a time
- 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:
- Infrastructure methods grouped:
.WithStandardInfrastructure() - Components registered in batches:
.RegisterCoreComponents() - Component access via properties:
.CoreData.Componentinstead of.CoreDataComponent
8.3 Rollback Plan
If migration encounters issues:
Keep old builders in parallel
FoundationBuilder.cs(new)FoundationBuilder.Legacy.cs(old)
Feature flag in project
#if USE_LEGACY_BUILDER var foundation = builder.AddFoundationLegacy()... #else var foundation = builder.AddFoundation()... #endifGradual 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:
- Easier to explain: "Bases compose infrastructure and register components"
- Example for others: Can be referenced in .NET Aspire discussions
- Contribution-friendly: Clear separation makes PRs easier
- 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
- Technical Excellence: Best architectural alignment
- Long-term Value: Foundation for future growth
- Code Quality: Eliminates all duplication
- Developer Experience: Clear, composable, testable
- 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
- Review this document with team
- Get approval from architecture stakeholders
- Create GitHub issue tracking implementation
- Begin Phase 1 (Shared Infrastructure)
- Weekly check-ins during implementation
- 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
Polylith Architecture
Dynaplex Documentation
/docs/adrs/007-aspire-microservices-migration.mdCLAUDE.md(this repository)
.NET Aspire Documentation
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
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.