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
sealedclasses unless inheritance is needed - Use
initproperties 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
.Abstractionswith 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
- Component Development How-to - Step-by-step creation
- Modifying Components - Adding features
- Architecture Decision Records - Design decisions
- Dynaplex Architecture - Overall system design