docs: Update AGENTS.md and STATUS.md with RabbitMQ and Phase 2 completion

AGENTS.md:
- Add Section 7: RabbitMQ Command Bus Integration (IMPLEMENTED)
- Document ICommandPublisher, RabbitMqCommandPublisher, RabbitMqCommandConsumer
- Add configuration, DI setup, and usage examples
- Document benefits: async processing, horizontal scaling, retries, persistence

STATUS.md:
- Mark Phase 2 (Application Layer) as 100% complete
- Update Phase 3 (Infrastructure Layer) to 15% (RabbitMQ done)
- Document all completed components: DTOs, Commands, Queries, Validators, Mappings
- Update last modified date to 2026-07-14
This commit is contained in:
2026-07-14 16:37:22 +02:00
parent 1ed489532d
commit 0d22fe0b5c
2 changed files with 249 additions and 50 deletions

194
AGENTS.md
View File

@@ -82,18 +82,200 @@ public class CreateEmailProfileCommandHandler : IRequestHandler<CreateEmailProfi
- Commands: `{Verb}{Entity}Command.cs` (e.g., `CreateEmailProfileCommand.cs`)
- Queries: `{Verb}{Entity}Query.cs` (e.g., `GetEmailProfilesQuery.cs`)
### 6. Repository Pattern - NO UnitOfWork, Generic CRUD with AutoMapper
**CRITICAL**: DO NOT use IUnitOfWork pattern. Use generic repository pattern with AutoMapper-based CRUD operations.
**Key Principles**:
- ✅ Each operation auto-saves changes - NO explicit SaveChangesAsync needed
- ✅ Use `UpdateSingleAsync` / `DeleteSingleAsync` for single-record safety
- ✅ Use `UpdateAsync` / `DeleteAsync` only when intentionally modifying multiple records
- ✅ AutoMapper handles all DTO → Entity mappings
**Pattern**:
```csharp
// IRepository<T> generic interface
public interface IRepository<TEntity> where TEntity : class
{
// Query operations
Task<TEntity?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, ...);
// Create - auto-saves
Task<TEntity> CreateAsync<TDto>(TDto dto, CancellationToken cancellationToken = default);
// Update - auto-saves
Task<int> UpdateAsync<TDto>(Expression<...> predicate, TDto dto, ...); // Multiple records
Task UpdateSingleAsync<TDto>(Expression<...> predicate, TDto dto, ...); // SAFE: Single record only
// Delete - auto-saves
Task<int> DeleteAsync(Expression<...> predicate, ...); // Multiple records
Task DeleteSingleAsync(Expression<...> predicate, ...); // SAFE: Single record only
}
```
**Command Handler Examples**:
```csharp
// ✅ CORRECT - CreateAsync auto-saves
public class CreateEmailProfileCommandHandler(IRepository<EmailProfile> repository)
: IRequestHandler<CreateEmailProfileCommand, int>
{
public async Task<int> Handle(CreateEmailProfileCommand request, CancellationToken cancellationToken)
{
var profile = await repository.CreateAsync(request, cancellationToken);
return profile.Id; // NO SaveChangesAsync needed!
}
}
// ✅ CORRECT - UpdateSingleAsync for safety (throws if 0 or 2+ records match)
public class UpdateEmailProfileCommandHandler(IRepository<EmailProfile> repository)
: IRequestHandler<UpdateEmailProfileCommand, int>
{
public async Task<int> Handle(UpdateEmailProfileCommand request, CancellationToken cancellationToken)
{
await repository.UpdateSingleAsync(p => p.Id == request.Id, request, cancellationToken);
return request.Id; // NO SaveChangesAsync needed!
}
}
// ✅ CORRECT - DeleteSingleAsync for safety (throws if 0 or 2+ records match)
public class DeleteEmailProfileCommandHandler(IRepository<EmailProfile> repository)
: IRequestHandler<DeleteEmailProfileCommand, int>
{
public async Task<int> Handle(DeleteEmailProfileCommand request, CancellationToken cancellationToken)
{
await repository.DeleteSingleAsync(p => p.Id == request.Id, cancellationToken);
return request.Id; // NO SaveChangesAsync needed!
}
}
// ❌ WRONG - Manual entity creation (use AutoMapper instead)
var profile = new EmailProfile
{
ProfileName = request.ProfileName,
EmailAccountId = request.EmailAccountId,
// ... 15 more properties
};
// ❌ WRONG - Using IUnitOfWork (removed)
public CreateEmailProfileCommandHandler(IUnitOfWork unitOfWork) { ... }
// ❌ WRONG - Calling SaveChangesAsync (removed)
await repository.SaveChangesAsync(cancellationToken);
```
**Safety Rules**:
1. **UpdateSingleAsync** - Use for ID-based updates. Throws `InvalidOperationException` if:
- Zero records match (entity not found)
- Multiple records match (predicate too broad)
2. **DeleteSingleAsync** - Use for ID-based deletes. Throws `InvalidOperationException` if:
- Zero records match (entity not found)
- Multiple records match (predicate too broad)
3. **UpdateAsync / DeleteAsync** - Use ONLY when intentionally modifying multiple records:
```csharp
// ✅ CORRECT - Intentional bulk operation
await repository.UpdateAsync(
p => p.EmailAccountId == accountId,
new { IsActive = false },
cancellationToken);
// ✅ Returns count of updated/deleted records
var count = await repository.DeleteAsync(p => p.IsActive == false, cancellationToken);
```
**DTO Mapping Responsibility**:
- Each DTO creator must define their own AutoMapper profile
- Example: `CreateEmailProfileCommand` → `EmailProfile` mapping must be defined in `EmailProfileMappingProfile.cs`
- Repository implementation uses `IMapper.Map<TEntity>(dto)` internally
---
## Future Enhancements
### HIGH PRIORITY: RabbitMQ Queue Implementation
### 7. RabbitMQ Command Bus Integration (IMPLEMENTED)
**Current State**:
- Email queue is implemented using in-memory `Channel<T>` in `InMemoryEmailQueue.cs`
- Location: `src/DigitalData.EmailProfiler.Infrastructure/Queue/InMemoryEmailQueue.cs`
**Purpose**: Asynchronous command processing via RabbitMQ message broker for POST/PUT/DELETE operations.
**Future Enhancement**:
Replace the in-memory queue with **RabbitMQ** for production resilience and scalability.
**Architecture**:
- **GET Queries**: Synchronous (immediate response via MediatR)
- **POST/PUT/DELETE Commands**: Can be asynchronous (published to RabbitMQ, processed by background worker)
**RabbitMQ Server**:
- Management UI: `http://172.24.12.56:15672`
- AMQP Port: `5672` (default)
- Exchange: `emailprofiler.commands` (Direct)
- Queue: `emailprofiler.command.queue`
- Routing Key: `command`
**Implementation Components**:
1. **ICommandPublisher** (`Application/Common/Interfaces/ICommandPublisher.cs`):
- Interface for publishing commands to message broker
- Generic method: `PublishAsync<TCommand>(TCommand command, CancellationToken)`
2. **RabbitMqCommandPublisher** (`Infrastructure/Messaging/RabbitMqCommandPublisher.cs`):
- Implements `ICommandPublisher`
- Serializes command to JSON with metadata envelope (CommandType, Payload, CorrelationId, PublishedAt)
- Publishes to RabbitMQ exchange with persistent delivery mode
3. **RabbitMqCommandConsumer** (`Infrastructure/Messaging/RabbitMqCommandConsumer.cs`):
- BackgroundService that consumes commands from RabbitMQ
- Deserializes command envelope
- Resolves command type from assembly
- Executes command via MediatR in scoped service
- Acknowledges message on success, requeues on error
4. **RabbitMqConfiguration** (`Infrastructure/Messaging/RabbitMqConfiguration.cs`):
- Configuration model for RabbitMQ connection
- Binds to `appsettings.json` section: `RabbitMq`
**Configuration** (`appsettings.json`):
```json
{
"RabbitMq": {
"HostName": "172.24.12.56",
"Port": 5672,
"UserName": "guest",
"Password": "guest",
"VirtualHost": "/",
"ExchangeName": "emailprofiler.commands",
"QueueName": "emailprofiler.command.queue",
"RoutingKey": "command",
"AutomaticRecoveryEnabled": true,
"NetworkRecoveryIntervalSeconds": 10
}
}
```
**Dependency Injection** (`Infrastructure/DependencyInjection.cs`):
```csharp
services.Configure<RabbitMqConfiguration>(configuration.GetSection(RabbitMqConfiguration.SectionName));
services.AddSingleton<ICommandPublisher, RabbitMqCommandPublisher>();
services.AddHostedService<RabbitMqCommandConsumer>();
```
**Usage in API Controllers** (Future):
```csharp
// Option 1: Synchronous (immediate execution via MediatR)
var result = await _mediator.Send(new CreateEmailProfileCommand(...), cancellationToken);
return Ok(result);
// Option 2: Asynchronous (publish to RabbitMQ for background processing)
await _commandPublisher.PublishAsync(new CreateEmailProfileCommand(...), cancellationToken);
return Accepted(); // HTTP 202 - command queued for processing
```
**Benefits**:
- Decouples API from long-running command processing
- Improves API responsiveness (fire-and-forget)
- Enables horizontal scaling (multiple consumers)
- Automatic retries on failure (requeue mechanism)
- Message persistence (survives application restarts)
---
### HIGH PRIORITY: Email Queue for Outgoing Messages (Future)
**Implementation Steps**: