Documentation

adrs/033-base-orchestration-pattern.md

ADR-033: Base Orchestration Pattern - Shared Infrastructure and Component Composition

Status

Accepted and Implemented

Context

The Dynaplex architecture uses "bases" as deployment scenario helpers that compose components into complete deployments. Initially, bases like FoundationBuilder and EdgeLiteBuilder contained significant code duplication (~85% overlap), with each base implementing:

  • Infrastructure setup (Azure resources, databases, monitoring)
  • Component registration and dependency management
  • Service orchestration and health checks
  • Validation logic

Initial Problems

  1. Code Duplication: FoundationBuilder and EdgeLiteBuilder shared 85% identical code
  2. Maintenance Burden: Infrastructure changes required updates in multiple bases
  3. Unclear Responsibility: Mixed concerns between infrastructure management and deployment orchestration
  4. Limited Composability: Difficult to create new deployment scenarios without copying large amounts of code
  5. Hidden Infrastructure: Developers couldn't easily see what infrastructure was being configured

Design Philosophy Clarifications

Through analysis and discussion, key principles emerged:

  • Bases are deployment scenario helpers, not infrastructure providers
  • Infrastructure should be explicit, not hidden behind abstractions
  • Component groupings should be reusable utilities that projects compose
  • The project AppHost is the infrastructure provider, bases provide tools for orchestration
  • Dynaplex is inspired by Polylith, not replicating it

Decision

Introduce a shared orchestration layer (Acsis.Dynaplex.Strata.Orchestration) that:

  1. Abstracts Aspire Infrastructure: Centralizes Azure/database/monitoring setup
  2. Provides Component Registry: Standardizes component registration with dependency validation
  3. Defines Base Scenarios: Reusable component groupings for common deployment patterns
  4. Enables Explicit Configuration: Bases expose infrastructure methods individually
  5. Supports Easy Composition: New bases compose scenarios without duplicating code

Architecture Components

1. InfrastructureBuilder

Centralizes Aspire infrastructure setup with explicit methods:

public class InfrastructureBuilder {
    public IDistributedApplicationBuilder Builder { get; }
    public IResourceBuilder<IResourceWithConnectionString>? Database { get; }
    public IResourceBuilder<AzureAppConfigurationResource>? AppConfig { get; }

    public InfrastructureBuilder WithDatabase() { }
    public InfrastructureBuilder WithAppConfiguration() { }
    public InfrastructureBuilder WithAppInsights() { }
    public InfrastructureBuilder WithContainerAppEnvironment() { }
    public ComponentRegistry CreateComponentRegistry() { }
}

Benefits:

  • Single source of truth for infrastructure
  • Enables migration away from Aspire if needed
  • Consistent naming and configuration across all bases

2. ComponentRegistry

Manages component registration with dependency validation:

public class ComponentRegistry {
    public ComponentRegistration RegisterWithMigrator<TComponent, TMigrator>(
        string name,
        string abbreviation,
        Action<ComponentDependencies>? configureDependencies = null);

    public ComponentRegistration Register<TComponent>(
        string name,
        string abbreviation,
        Action<ComponentDependencies>? configureDependencies = null);

    public ComponentRegistration Get(string name);
    public void ValidateRequiredComponents(params string[] requiredComponents);
}

Features:

  • Dependency declaration via fluent API
  • Automatic dependency ordering
  • Validation of required components
  • Consistent naming (component name + abbreviation)

3. BaseScenarios

Pre-defined component groupings for composition:

public static class BaseScenarios {
    // Core infrastructure (required for all deployments)
    public static void CoreInfrastructure<...>(ComponentRegistry registry);

    // Full catalog stack (asset management)
    public static void CatalogStack<...>(ComponentRegistry registry);

    // Edge/RFID components
    public static void EdgeComponents<...>(ComponentRegistry registry);

    // Analytics and reporting
    public static void AnalyticsComponents<...>(ComponentRegistry registry);

    // Data import and integration
    public static void IntegrationComponents<...>(ComponentRegistry registry);
}

Purpose:

  • Standardized component groupings
  • Reusable across multiple bases
  • Documents common deployment patterns
  • Easy to extend with new scenarios

Refactored Base Pattern

Before (512 lines):

public class FoundationBuilder {
    // 500+ lines of infrastructure setup, component registration,
    // validation, and orchestration code
}

After (130 lines - 75% reduction):

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

    public FoundationBuilder WithDatabase() {
        _infrastructure.WithDatabase();
        _registry = _infrastructure.CreateComponentRegistry();
        return this;
    }

    // Explicit infrastructure methods (developers see what's configured)
    public FoundationBuilder WithAppConfiguration() { }
    public FoundationBuilder WithAppInsights() { }
    public FoundationBuilder WithContainerAppEnvironment() { }

    public IDistributedApplicationBuilder Build() {
        _registry!.ValidateRequiredComponents(
            "core-data", "system-environment", "identity", "events", "edge");
        return _infrastructure.Builder;
    }
}

Project Usage:

var foundation = builder.AddFoundation()
    .WithAppConfiguration()      // Explicit - developers see what's configured
    .WithAppInsights()
    .WithContainerAppEnvironment()
    .WithDatabase();

// Component registration happens in project code
BaseScenarios.CoreInfrastructure<...>(foundation.Components);
BaseScenarios.CatalogStack<...>(foundation.Components);
BaseScenarios.EdgeComponents<...>(foundation.Components);

foundation.Build();

New Deployment Scenarios

The refactoring enabled creation of distinct deployment scenarios:

Foundation (Complete Stack)

  • Purpose: Full Dynaplex experience with all components
  • Includes: Core + Catalog + Edge + Analytics + Integration
  • Use Case: Enterprise deployments, complete AssetTrak installations
  • Required Components: core-data, system-environment, identity, events, edge

RfidLite (RFID-Focused)

  • Purpose: Lightweight RFID tracking
  • Includes: Core + Edge + Basic catalog
  • Use Case: IoT edge deployments, simple RFID tracking
  • Required Components: core-data, system-environment, identity, events, edge
  • Note: BBU can be optionally layered on

WarehouseHub (Operations-Focused)

  • Purpose: Warehouse operations without RFID
  • Includes: Core + Full catalog + Analytics + Integration
  • Use Case: Manual inventory, barcode scanning, traditional workflows
  • Required Components: core-data, system-environment, identity, events, catalog, spatial, file-service, printing, transport, intelligence, workflow, importer
  • Note: No Edge/RFID components

Consequences

Positive

Code Quality:

  • 75% reduction in base code size (512 → 130 lines)
  • Eliminated duplication across all bases
  • Clear separation of concerns (infrastructure vs. orchestration)
  • Improved testability through smaller, focused components

Developer Experience:

  • Explicit configuration: Developers see exactly what infrastructure is configured
  • Discoverability: BaseScenarios document standard component groupings
  • Flexibility: Easy to create custom scenarios by composing scenarios
  • Consistency: All bases follow the same pattern

Maintainability:

  • Single source of truth for infrastructure setup
  • Centralized validation logic in ComponentRegistry
  • Easier to update: Change once in orchestration layer
  • Clear documentation of deployment patterns

Extensibility:

  • New bases are trivial: ~130 lines following established pattern
  • Aspire migration path: InfrastructureBuilder isolates Aspire-specific code
  • Easy scenario composition: Mix and match BaseScenarios
  • Custom scenarios: Projects can register components manually if needed

Neutral

Learning Curve:

  • Developers need to understand the distinction between:
    • Bases: Deployment scenario helpers with validation
    • Orchestration: Shared infrastructure and component utilities
    • Projects: Actual infrastructure providers (AppHost)

Component Registration:

  • Component registration moved from base to project code
  • More explicit, but requires additional code in AppHost

Negative

None Identified:

  • The refactoring maintains all existing functionality
  • Builds successfully with no regressions
  • Existing projects (assettrak-classic) migrated successfully

Implementation Details

File Structure

strata/
├── orchestration/
│   └── src/Acsis.Dynaplex.Strata.Orchestration/
│       ├── InfrastructureBuilder.cs        # Aspire abstraction
│       ├── ComponentRegistry.cs            # Registration & validation
│       ├── ComponentRegistration.cs        # Component metadata
│       ├── ComponentDependencies.cs        # Dependency declaration
│       └── BaseScenarios.cs               # Pre-defined scenarios
projects/
├── bbu-rfid/
│   └── src/Acsis.Dynaplex.Projects.BbuRfid/
│       ├── AppHost.cs                     # Aspire AppHost entry point
│       └── ...
├── rfid-lite/
│   └── src/Acsis.Bases.RfidLite/
│       ├── RfidLiteBuilder.cs             # 130 lines
│       ├── RfidLiteOptions.cs
│       └── RfidLiteExtensions.cs
└── warehouse-hub/
    └── src/Acsis.Bases.WarehouseHub/
        ├── WarehouseHubBuilder.cs         # 145 lines
        ├── WarehouseHubOptions.cs
        └── WarehouseHubExtensions.cs

Migration Path

Projects using old FoundationBuilder pattern:

Before:

var foundation = builder.AddFoundation()
    .WithDefaultDatabase()
    .WithCoreData<T1, T2>()
    .WithIdentity<T3, T4>()
    // ... more WithXxx() calls

After:

var foundation = builder.AddFoundation()
    .WithDatabase();  // Changed name

BaseScenarios.CoreInfrastructure<...>(foundation.Components);
BaseScenarios.CatalogStack<...>(foundation.Components);

foundation.Build();

Migration completed for:

  • ✅ assettrak-classic project (builds successfully)

Roslyn Analyzer Update

Updated AbstractionReferenceRequirementAnalyzer to allow the orchestration project:

private static readonly string[] AllowedProjects = [
    "Acsis.Dynaplex",
    "Acsis.Dynaplex.Strata.ServiceDefaults",
    "Acsis.Dynaplex.Strata.Orchestration",  // ← Added
    "Acsis.RoslynAnalyzers",
    "Acsis.EntLibCompatShim",
    "Acsis.Encoding.Gs1"
];

This allows bases to reference the shared orchestration utilities while maintaining architectural boundaries.

  • ADR-007: Aspire Microservices Migration - Established .NET Aspire as orchestration framework
  • ADR-004: Roslyn Analyzer Enforcement - Enforces architectural rules at compile time
  • ADR-001: Polylith Architecture Adoption - Initial inspiration for component architecture

Future Considerations

  1. EdgeLite Migration: Refactor existing EdgeLite base using new pattern (currently 495 lines)
  2. Additional Scenarios: Create bases for other common deployment patterns
  3. Aspire Alternatives: If Microsoft abandons Aspire, InfrastructureBuilder provides migration path
  4. Documentation Base: Consider base for doc-web-only deployments
  5. Component Templates: Generate new bases using templates/scaffolding

References

  • Implementation: /strata/orchestration/src/Acsis.Dynaplex.Strata.Orchestration/
  • Refactored projects: /projects/bbu-rfid/, /projects/rfid-lite/, /projects/warehouse-hub/
  • Analysis document: /docs/analysis/BASE_DUPLICATION_ANALYSIS.md
  • Project migration: /projects/assettrak-classic/src/Acsis.Dynaplex.Projects.AssetTrakClassic/AppHost.cs

Evolution: Aspire-Native Extensions (November 2025)

Status: Implemented and superseding InfrastructureBuilder pattern

What Changed

The InfrastructureBuilder + ComponentRegistry pattern has evolved to Aspire-native extension methods that are simpler, more discoverable, and follow Microsoft's standard patterns.

Before (InfrastructureBuilder):

var infrastructure = new InfrastructureBuilder(builder);
infrastructure.WithDatabase();
infrastructure.WithAppConfiguration();

var registry = infrastructure.CreateComponentRegistry();
registry.Register<ComponentA>(metadata).WithDependencies(/* ... */);

After (Extension Methods):

builder.UseDynaplex("elbbudev");
builder.AddDynaplexDatabase();
builder.AddDynaplexAppConfiguration();

// Centralized db-manager handles all migrations
var dbManager = builder.AddDynaplexDbManager()
       .WaitFor(database);

builder.AddComponent<ComponentA>(ComponentIndex.ComponentA.Metadata)
       .WaitFor(dbManager);  // All services wait for db-manager

Note (2026-01-08): The original per-component .WithMigrator<>() pattern has been replaced by a centralized db-manager component. See ADR-036 for details.

Why This Evolution?

  1. Aspire Alignment: Native builder extension methods (feels like built-in Aspire)
  2. Discoverability: IntelliSense shows all available methods naturally
  3. No Wrapper Objects: Direct builder extension, no InfrastructureBuilder wrapper
  4. Metadata as Annotations: Uses Aspire's IResourceAnnotation pattern
  5. Context via DI: Stateful sharing through singleton context (clean)
  6. Familiar to .NET Developers: Follows standard Microsoft patterns

Key Implementation Details

  • DynaplexContext: Registered as DI singleton in UseDynaplex()
  • Component Metadata: Attached as IResourceAnnotation to resources
  • Extension Methods: All static methods in Aspire.Hosting namespace
  • Hidden Registry: ComponentRegistry lives internal in DynaplexContext
  • Dependency Flow: Metadata-driven, extracted from annotations

Migration from InfrastructureBuilder

Pattern change:

// OLD: Wrapper object
var infrastructure = new InfrastructureBuilder(builder);
var component = infrastructure.WithDatabase();

// NEW: Direct extension
builder.UseDynaplex();
builder.AddDynaplexDatabase();
var component = builder.AddComponent<T>(metadata);

Benefits of migration:

  • Cleaner AppHost code
  • Better IntelliSense support
  • No wrapper object ceremony
  • Easier to understand (more explicit)

Documentation of New Pattern

Complete documentation available in:

  • /strata/orchestration/src/Acsis.Dynaplex.Strata.Orchestration/README.md - Overview and usage
  • /strata/orchestration/src/Acsis.Dynaplex.Strata.Orchestration/architecture.md - Detailed design decisions
  • /strata/orchestration/src/Acsis.Dynaplex.Strata.Orchestration/patterns.md - Common patterns and examples

Backward Compatibility

The InfrastructureBuilder classes remain in the codebase for any legacy uses, but new development should use the extension method pattern. Existing AppHosts can migrate incrementally (extension methods coexist with old pattern).


Date: 2025-01-18 (Original)
Updated: 2025-11-12 (Evolution to Aspire-Native Extensions)
Updated: 2026-01-08 (db-manager consolidation)
Author: System Architecture Team
Reviewers: Development Team