Documentation

adrs/003-dynamic-component-loading.md

ADR-003: Dynamic Component Loading with Assembly Load Context

Status

Accepted

Context

In traditional .NET applications, all assemblies are loaded at compile time through project references. However, our Polylith architecture requires components to be loaded dynamically at runtime to achieve true modularity and flexibility.

The challenge was how to implement component loading that would:

  • Maintain loose coupling: Components should only know about interfaces, not implementations
  • Support hot-swapping: Potentially replace components without restarting the application
  • Handle dependencies: Manage transitive dependencies and version conflicts
  • Provide discoverability: Automatically find and load available components
  • Integrate with DI: Register component implementations with the dependency injection container

Several approaches were considered:

  1. Compile-time linking: Traditional project references (ruled out - not modular)
  2. MEF (Managed Extensibility Framework): .NET's built-in composition framework
  3. Plugin architecture: Custom assembly loading with interfaces
  4. Assembly Load Context: .NET Core's modern assembly loading mechanism
  5. Reflection-based discovery: Scan assemblies for implementations

Decision

We implemented dynamic component loading using .NET's AssemblyLoadContext combined with reflection-based discovery and dependency injection registration.

Key Design Decisions:

  1. Single Load Context: Use one ComponentLoadContext for all components to avoid isolation issues
  2. Build-time preparation: Components are built and copied to bin/components/ directory
  3. Runtime discovery: Scan component assemblies for interface implementations
  4. Automatic registration: Register implementations with DI container automatically
  5. Interface-driven loading: Only load components for explicitly requested interfaces

Implementation Architecture:

// ComponentLoader.cs - Main loading logic
public static void LoadComponents(this IServiceCollection services, params Type[] requestedComponents)

// ComponentLoadContext.cs - Custom assembly load context  
public class ComponentLoadContext : AssemblyLoadContext

// Program.cs - Usage
services.LoadComponents(
    typeof(ICoreDataApi),
    typeof(IIdentityApi),
    typeof(IEventsApi)
);

Build Integration:

  • Each component includes MSBuild target to copy assemblies to bin/components/
  • Foundation base copies components to its output directory
  • Runtime loader scans the components directory

Consequences

Positive

Architectural Benefits:

  • True modularity: Components can be added/removed without recompilation
  • Loose coupling: Foundation base only depends on component interfaces
  • Flexible composition: Different applications can load different component sets
  • Development simplicity: New components are automatically discovered

Runtime Flexibility:

  • Dynamic configuration: Can potentially load components based on configuration
  • Plugin architecture: External components can be added by placing assemblies in folder
  • Testing benefits: Can load different implementations for testing
  • Deployment options: Can deploy different component combinations

Development Experience:

  • Automatic registration: No manual DI registration for each component
  • Clear separation: Physical separation enforces architectural boundaries
  • Hot reload potential: Foundation for hot-swapping components (future enhancement)

Negative

Runtime Complexity:

  • Late binding errors: Component loading failures only discovered at runtime
  • Debugging complexity: Dynamic loading can complicate debugging and profiling
  • Startup overhead: Assembly scanning and loading adds application startup time
  • Memory usage: All component assemblies loaded into memory regardless of usage

Dependency Management:

  • Version conflicts: Multiple components might depend on different versions of same library
  • Transitive dependencies: Complex dependency graphs can cause loading issues
  • Assembly resolution: Custom resolution logic needed for component dependencies
  • GAC conflicts: Potential conflicts with Global Assembly Cache

Operational Challenges:

  • Deployment complexity: Must ensure all component assemblies are deployed
  • Security concerns: Loading arbitrary assemblies could be security risk
  • Diagnostic complexity: Harder to diagnose assembly loading issues
  • Performance unpredictability: Loading time depends on number and size of components

Development Challenges:

  • Build coordination: Components must be built before foundation base
  • IDE support: Dynamic references not visible in IDE dependency graphs
  • Refactoring tools: Some refactoring tools don't understand dynamic loading
  • Static analysis: Code analysis tools might miss dynamically loaded dependencies

Mitigation Strategies

Runtime Error Handling:

// Detailed logging for troubleshooting
logger.LogInformation("Loading component {ComponentName}", componentName);
logger.LogError("Failed to load component {ComponentName}: {Error}", componentName, error);

// Graceful fallback for missing components
if (implementation == null) {
    logger.LogWarning("No implementation found for {InterfaceName}", interfaceType.Name);
    // Could register null object pattern or throw descriptive exception
}

Build System Integration:

<!-- Automatic component copying -->
<Target Name="PublishComponent" AfterTargets="Build">
    <ItemGroup>
        <ComponentFiles Include="$(OutputPath)*.dll;$(OutputPath)*.pdb" />
    </ItemGroup>
    <Copy SourceFiles="@(ComponentFiles)" DestinationFolder="$(ComponentsDir)"/>
</Target>

Development Tooling:

  • Use development.slnx to include all components for IDE support
  • Detailed logging during component loading
  • Health checks to verify component loading

Testing Strategy:

// Integration tests that verify component loading
[Test]
public void Should_Load_All_Required_Components() {
    var services = new ServiceCollection();
    services.LoadComponents(typeof(ICoreDataApi), typeof(IIdentityApi));
    
    var provider = services.BuildServiceProvider();
    
    Assert.NotNull(provider.GetService<ICoreDataApi>());
    Assert.NotNull(provider.GetService<IIdentityApi>());
}

Implementation Details

Component Discovery Logic:

private static Type? FindImplementation(List<Assembly> assemblies, Type interfaceType) {
    foreach (var assembly in assemblies) {
        var types = assembly.GetTypes();
        var implementation = types.FirstOrDefault(t => 
            !t.IsInterface && 
            !t.IsAbstract && 
            interfaceType.IsAssignableFrom(t));
        
        if (implementation != null) {
            return implementation;
        }
    }
    return null;
}

Assembly Loading:

public class ComponentLoadContext : AssemblyLoadContext {
    protected override Assembly? Load(AssemblyName assemblyName) {
        // Custom loading logic for component dependencies
        var assemblyPath = Path.Combine(ComponentsPath, $"{assemblyName.Name}.dll");
        return File.Exists(assemblyPath) ? LoadFromAssemblyPath(assemblyPath) : null;
    }
}
  • ADR-001: Overall architecture requiring component loading
  • ADR-002: Interface naming that enables clean loading
  • ADR-004: Compile-time enforcement of interface-only references

Alternatives Considered

MEF (Managed Extensibility Framework)

Pros: Built-in .NET framework, attribute-based composition, mature
Cons: Heavy, complex configuration, not well-suited for our interface-driven approach

Traditional Plugin Architecture

Pros: Simple, well-understood pattern
Cons: Requires explicit plugin interfaces, more boilerplate code

Compile-time Composition

Pros: Better IDE support, earlier error detection
Cons: Loses modularity benefits, requires recompilation for component changes

Future Enhancements

  1. Hot Reload: Support for replacing components without application restart
  2. Lazy Loading: Load components only when first used
  3. Health Monitoring: Component health checks and automatic restart
  4. Version Management: Support for multiple component versions and selection
  5. Security: Assembly signing and verification for component loading