Documentation

reference/patterns/component-patterns.md

Component Patterns Reference

This reference guide documents best practices, common pitfalls, and advanced patterns for Dynaplex components.

Naming Conventions

Type Pattern Example
Component Folder kebab-case core-data, file-service
Abstractions Project Acsis.Dynaplex.Engines.{Component}.Abstractions Acsis.Dynaplex.Engines.CoreData.Abstractions
Service Project Acsis.Dynaplex.Engines.{Component} Acsis.Dynaplex.Engines.CoreData
Main Interface I{Component}Api ICoreDataApi
Implementation Class {Component}Api CoreDataApi
Models PascalCase with suffix AssetModel, UserResponse, CreateAssetRequest

Best Practices

Interface Design

// ✅ GOOD - Clear, focused interface
public interface IMyFeatureApi
{
    Task<FeatureItem?> GetByIdAsync(int id);
    Task<FeatureItem> CreateAsync(CreateFeatureRequest request);
    Task UpdateAsync(int id, UpdateFeatureRequest request);
    Task DeleteAsync(int id);
}

// ❌ BAD - Too many responsibilities
public interface IMyFeatureApi
{
    Task<FeatureItem> GetByIdAsync(int id);
    Task SendEmailAsync(string email);     // Should be in IEmailApi
    Task LogEventAsync(string message);    // Should be in ILoggingApi
    Task<User> GetUserAsync(int userId);   // Should be in IUserApi
}

Principles:

  • Single Responsibility - One clear purpose
  • Async by default - All operations should be async
  • Nullable return types - Use ? for operations that may not find data
  • Clear naming - Method names describe exactly what they do

Model Design

// ✅ GOOD - Immutable, well-documented, validated
/// <summary>
/// Represents a feature item in the system
/// </summary>
public sealed class FeatureItemModel
{
    /// <summary>
    /// The unique identifier for the feature item
    /// </summary>
    public int Id { get; init; }

    /// <summary>
    /// The display name of the feature item
    /// </summary>
    [Required]
    [StringLength(100, MinimumLength = 1)]
    public string Name { get; init; } = string.Empty;

    /// <summary>
    /// Optional description
    /// </summary>
    [StringLength(500)]
    public string? Description { get; init; }

    /// <summary>
    /// When the item was created (UTC)
    /// </summary>
    public DateTime CreatedAt { get; init; }
}

// ❌ BAD - Mutable, no validation, no documentation
public class FeatureItemModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public DateTime CreatedAt { get; set; }
}

Model Guidelines:

  • Use sealed classes unless inheritance is needed
  • Use init properties for immutability
  • Add XML documentation comments
  • Use data annotations for validation
  • Use nullable types (?) appropriately
  • Initialize non-nullable strings to string.Empty

Error Handling

// ✅ GOOD - Specific exceptions, proper logging, validation
public async Task<FeatureItem> GetByIdAsync(int id)
{
    if (id <= 0)
    {
        throw new ArgumentException("ID must be positive", nameof(id));
    }

    _logger.LogInformation("Retrieving feature item {Id}", id);

    var item = await _repository.GetByIdAsync(id);
    if (item == null)
    {
        _logger.LogWarning("Feature item {Id} not found", id);
        throw new FeatureItemNotFoundException($"Feature item {id} not found");
    }

    return item;
}

// ❌ BAD - Generic exceptions, no logging, poor validation
public async Task<FeatureItem> GetByIdAsync(int id)
{
    var item = await _repository.GetByIdAsync(id);
    if (item == null)
    {
        throw new Exception("Not found");
    }
    return item;
}

Error Handling Principles:

  • Validate inputs at API boundary
  • Use specific exception types
  • Log errors with context
  • Never swallow exceptions
  • Return meaningful error messages

Dependency Injection

// ✅ GOOD - Constructor injection, null checks, minimal dependencies
public sealed class MyFeatureApi : IMyFeatureApi
{
    private readonly IMyFeatureRepository _repository;
    private readonly ILogger<MyFeatureApi> _logger;
    private readonly IOptions<MyFeatureOptions> _options;

    public MyFeatureApi(
        IMyFeatureRepository repository,
        ILogger<MyFeatureApi> logger,
        IOptions<MyFeatureOptions> options)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _options = options ?? throw new ArgumentNullException(nameof(options));
    }
}

// ❌ BAD - Service locator anti-pattern
public sealed class MyFeatureApi : IMyFeatureApi
{
    private readonly IServiceProvider _serviceProvider;

    public MyFeatureApi(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task DoSomething()
    {
        // DON'T DO THIS - tight coupling, hard to test
        var repository = _serviceProvider.GetService<IMyFeatureRepository>();
        var logger = _serviceProvider.GetService<ILogger>();
    }
}

DI Best Practices:

  • Use constructor injection
  • Inject interfaces, not implementations
  • Validate dependencies (null checks)
  • Keep dependency count reasonable (<10)
  • Use IServiceProvider only for scoped services in singletons

Common Pitfalls

1. Referencing Implementation Projects

// ❌ WRONG - This triggers ACSIS0001 analyzer error
using Acsis.Dynaplex.Engines.CoreData;

// ✅ CORRECT - Only reference .Abstractions projects
using Acsis.Dynaplex.Engines.CoreData.Abstractions;

Why? Services run in separate processes and communicate via HTTP. You can only depend on contracts, not implementations.

2. Circular Dependencies

// ❌ WRONG - Component A depends on Component B, and B depends on A
// Component A references B.Abstractions
// Component B references A.Abstractions
// This creates a circular dependency

// ✅ CORRECT - Break the cycle
// Option 1: Use events to decouple
// Option 2: Move shared models to CoreData.Abstractions
// Option 3: Extract common interface to a third component

3. Leaking Implementation Details

// ❌ WRONG - Exposing EF Core entities in API
public interface IMyFeatureApi
{
    // FeatureDbEntity is an EF Core entity - implementation detail!
    Task<FeatureDbEntity> GetByIdAsync(int id);
}

// ✅ CORRECT - Use dedicated DTOs/models
public interface IMyFeatureApi
{
    // FeatureItemModel is a clean DTO in .Abstractions
    Task<FeatureItemModel> GetByIdAsync(int id);
}

4. Not Following Naming Conventions

// ❌ WRONG - Inconsistent naming
public interface IMyFeature { }                    // Missing "Api" suffix
public class MyFeatureService : IMyFeature { }     // Wrong class name
public class FeatureManager : IMyFeature { }       // Wrong class name

// ✅ CORRECT - Consistent pattern
public interface IMyFeatureApi { }
public sealed class MyFeatureApi : IMyFeatureApi { }

5. Improper Service Lifetimes

// ❌ WRONG - Scoped service in singleton hosted service
public class MyProcessor : BackgroundService
{
    private readonly DbContext _db; // DbContext is scoped!

    public MyProcessor(DbContext db)
    {
        _db = db; // Runtime error!
    }
}

// ✅ CORRECT - Use IServiceProvider to create scopes
public class MyProcessor : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public MyProcessor(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await using var scope = _serviceProvider.CreateAsyncScope();
        var db = scope.ServiceProvider.GetRequiredService<DbContext>();
        // Use db...
    }
}

Advanced Patterns

Configuration Options Pattern

// In .Abstractions/Configuration/MyFeatureOptions.cs
public sealed class MyFeatureOptions
{
    public const string SectionName = "MyFeature";

    [Required]
    public string ApiKey { get; set; } = string.Empty;

    [Range(1, 10)]
    public int MaxRetries { get; set; } = 3;

    public bool EnableCaching { get; set; } = true;

    public TimeSpan CacheExpiration { get; set; } = TimeSpan.FromMinutes(5);
}

// In Program.cs
builder.Services.AddOptions<MyFeatureOptions>()
    .BindConfiguration(MyFeatureOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart();

// In implementation
public class MyFeatureApi : IMyFeatureApi
{
    private readonly MyFeatureOptions _options;

    public MyFeatureApi(IOptions<MyFeatureOptions> options)
    {
        _options = options.Value;
    }
}

Background Service Pattern

// In Acsis.Dynaplex.Engines.MyFeature/Services/MyBackgroundService.cs
public sealed class MyBackgroundService : BackgroundService
{
    private readonly ILogger<MyBackgroundService> _logger;
    private readonly IServiceProvider _serviceProvider;
    private readonly PeriodicTimer _timer;

    public MyBackgroundService(
        ILogger<MyBackgroundService> logger,
        IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
        _timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (await _timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                await DoWorkAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in background service");
            }
        }
    }

    private async Task DoWorkAsync(CancellationToken cancellationToken)
    {
        await using var scope = _serviceProvider.CreateAsyncScope();
        var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();

        // Do work with scoped services
    }

    public override void Dispose()
    {
        _timer?.Dispose();
        base.Dispose();
    }
}

// Register in Program.cs
builder.Services.AddHostedService<MyBackgroundService>();

Event Publishing Pattern

// Define event in .Abstractions/Events/
public sealed record FeatureItemCreatedEvent
{
    public int Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public DateTime CreatedAt { get; init; }
}

// Publish events in implementation
public sealed class MyFeatureApi : IMyFeatureApi
{
    private readonly IEventsApiClient _events;

    public async Task<FeatureItem> CreateAsync(CreateFeatureRequest request)
    {
        var item = await CreateItemInDatabase(request);

        // Publish event for other components
        await _events.PublishAsync(new FeatureItemCreatedEvent
        {
            Id = item.Id,
            Name = item.Name,
            CreatedAt = item.CreatedAt
        });

        return item;
    }
}

Repository Pattern (Optional)

// In .Abstractions/Data/IMyFeatureRepository.cs
public interface IMyFeatureRepository
{
    Task<FeatureItem?> GetByIdAsync(int id);
    Task<List<FeatureItem>> GetAllAsync();
    Task<FeatureItem> CreateAsync(FeatureItem item);
    Task UpdateAsync(FeatureItem item);
    Task DeleteAsync(int id);
}

// In MyFeature/Data/MyFeatureRepository.cs
public sealed class MyFeatureRepository : IMyFeatureRepository
{
    private readonly MyFeatureDbContext _db;

    public MyFeatureRepository(MyFeatureDbContext db)
    {
        _db = db;
    }

    public async Task<FeatureItem?> GetByIdAsync(int id)
    {
        return await _db.FeatureItems
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.Id == id);
    }

    // Other methods...
}

// Register in Program.cs
builder.Services.AddScoped<IMyFeatureRepository, MyFeatureRepository>();

Component Checklist

Before considering a component complete:

Structure

  • Follows standard project structure (.Abstractions, .Database, service, .ApiClient)
  • Uses consistent naming conventions
  • Organized into logical folders (Models/, Services/, Entities/)
  • If using database: registered with db-manager

Interface & Models

  • Interface in .Abstractions with XML documentation
  • All public types have XML documentation
  • Models use data annotations for validation
  • Nullable types used appropriately

Implementation

  • All dependencies injected via constructor
  • Null checks on all injected dependencies
  • Structured logging used throughout
  • Error handling with specific exceptions
  • Async/await used correctly

Configuration

  • Registered in Directory.Build.props
  • Added to development solution
  • Registered with Aspire AppHost
  • Configuration options validated

Testing

  • Unit tests for core functionality
  • Integration tests for HTTP endpoints
  • Test coverage >70%

Quality

  • Builds without warnings
  • No analyzer violations
  • Passes all tests
  • OpenAPI documentation complete

Feature Documentation

For major features spanning multiple components, create comprehensive documentation in docs/features/feature-name/.

Structure

docs/features/feature-name/
├── README.md                # Overview and navigation
├── technical-design.md      # Architecture and implementation
├── entity-model.md          # Database schema if applicable
├── migration-guide.md       # Upgrade path from previous version
├── usage-examples.md        # Code examples and patterns
└── reference-data.md        # Standard configurations/data

When to Create Feature Documentation

  • Implementing a major enhancement affecting multiple components
  • Introducing new architectural patterns
  • Adding complex business logic requiring detailed explanation
  • Creating features other developers will maintain

When to Extract Code to a Library

Some code doesn't belong in components—it should be extracted to a library project. Libraries are pure, stateless utilities that can be directly referenced.

Good Candidates for Libraries

Extract to library when:

Type Examples
Parsing/encoding GS1 barcodes, data format conversions
Validation helpers Custom validation attributes, FluentValidation extensions
Strongly-typed IDs PTIDs, PassportId, domain value objects
Unit conversions Measurement units, currency formatting
Extension methods BCL type extensions used across components
Pure algorithms Calculations, transformations with no I/O

Keep in Component

Keep in component when:

Type Where it belongs
Business rules for one domain Component's .Abstractions
Code that needs database access Component's .Database project
Code that calls other services Component's service project
Orchestration or workflow logic Component service
Code specific to one component That component's projects

Example: Extracting Shared Validation

Before (duplicated in multiple components):

// In Acsis.Dynaplex.Engines.CoreData.Abstractions
public static class ValidationHelpers {
    public static bool IsValidBarcode(string barcode) { /* ... */ }
}

// In Acsis.Dynaplex.Engines.Catalog.Abstractions (copy-pasted)
public static class ValidationHelpers {
    public static bool IsValidBarcode(string barcode) { /* ... */ }
}

After (extracted to library):

// In Acsis.Dynaplex.Strata.Validation
namespace Acsis.Dynaplex.Strata.Validation;

public static class BarcodeValidation {
    public static bool IsValid(string barcode) { /* ... */ }
}

Both components now reference Acsis.Dynaplex.Strata.Validation.

Decision Flowchart

Does the code need to own data or expose endpoints?
  ├─ YES → Keep in COMPONENT
  │
  NO
  ↓
Does it implement business workflows or domain policies?
  ├─ YES → Keep in COMPONENT (.Abstractions + service)
  │
  NO
  ↓
Is it used by only ONE component?
  ├─ YES → Keep in that component's .Abstractions
  │
  NO (used by 2+ components)
  ↓
Is the code pure/stateless (no I/O)?
  ├─ NO → Keep in COMPONENT (reconsider design)
  │
  YES
  ↓
✓ LIBRARY CANDIDATE

See ADR-046: Library Projects and How to Create a Library.

References