books/backend/Books.Api/EventFlow/Infrastructure/ReadStoresResilienceStrategy.cs
Nicolaj Hartmann 66f6fa138d Initial commit: Books accounting system with EventFlow CQRS
Backend (.NET 10):
- EventFlow CQRS/Event Sourcing with PostgreSQL
- GraphQL.NET API with mutations and queries
- Custom ReadModelSqlGenerator for snake_case PostgreSQL columns
- Hangfire for background job processing
- Integration tests with isolated test databases

Frontend (React/Vite):
- Initial project structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 02:52:30 +01:00

95 lines
3.2 KiB
C#

using Books.Api.Infrastructure;
using EventFlow.Aggregates;
using EventFlow.ReadStores;
using Hangfire;
namespace Books.Api.EventFlow.Infrastructure;
public class ReadStoresResilienceStrategy(
ILogger<ReadStoresResilienceStrategy> logger,
IScheduler scheduler) : IDispatchToReadStoresResilienceStrategy
{
public Task BeforeUpdateAsync(
IReadStoreManager readStoreManager,
IReadOnlyCollection<IDomainEvent> domainEvents,
CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task<bool> HandleUpdateFailedAsync(
IReadStoreManager readStoreManager,
IReadOnlyCollection<IDomainEvent> domainEvents,
Exception exception,
CancellationToken cancellationToken)
{
var firstEvent = domainEvents.FirstOrDefault();
var aggregateId = firstEvent?.GetIdentity()?.Value ?? "unknown";
var readModelType = readStoreManager.ReadModelType.Name;
logger.LogError(exception,
"[RESILIENCE] Failed to update read model {ReadModelType} for aggregate {AggregateId}. " +
"Events: {EventCount}. This may cause data inconsistency.",
readModelType,
aggregateId,
domainEvents.Count);
// Schedule a retry job to repopulate the read model
if (firstEvent != null)
{
var aggregateType = firstEvent.AggregateType.Name;
scheduler.EnqueueJob<ReadModelRepopulationJob>(
job => job.RepopulateReadModelAsync(
aggregateType,
aggregateId,
readModelType),
TimeSpan.FromSeconds(30));
logger.LogWarning(
"[RESILIENCE] Scheduled read model repopulation job for {ReadModelType} / {AggregateId}",
readModelType,
aggregateId);
}
// Return false to indicate we handled the failure (don't rethrow)
return Task.FromResult(false);
}
public Task UpdateSucceededAsync(
IReadStoreManager readStoreManager,
IReadOnlyCollection<IDomainEvent> domainEvents,
CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
public class ReadModelRepopulationJob(ILogger<ReadModelRepopulationJob> logger)
{
[AutomaticRetry(Attempts = 3, DelaysInSeconds = [30, 60, 120])]
public Task RepopulateReadModelAsync(
string aggregateType,
string aggregateId,
string readModelType)
{
logger.LogInformation(
"[REPOPULATION] Starting read model repopulation for {ReadModelType} / {AggregateId}",
readModelType,
aggregateId);
// For now, we log the repopulation attempt
// A full implementation would:
// 1. Load all events for the aggregate from the event store
// 2. Clear the existing read model entry
// 3. Replay all events to rebuild the read model
logger.LogWarning(
"[REPOPULATION] Read model repopulation for {ReadModelType} / {AggregateId} " +
"requires manual intervention. Check data consistency.",
readModelType,
aggregateId);
return Task.CompletedTask;
}
}