dotnet-cqrs/Svrnty.CQRS.Events/Sagas/InMemorySagaStateStore.cs

105 lines
3.4 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Svrnty.CQRS.Events.Abstractions.Sagas;
namespace Svrnty.CQRS.Events.Sagas;
/// <summary>
/// In-memory implementation of saga state store.
/// Suitable for development and testing. Not for production use.
/// </summary>
public sealed class InMemorySagaStateStore : ISagaStateStore
{
private readonly ConcurrentDictionary<string, SagaStateSnapshot> _statesBySagaId = new();
private readonly ConcurrentDictionary<string, List<string>> _sagaIdsByCorrelationId = new();
/// <inheritdoc />
public Task SaveStateAsync(SagaStateSnapshot state, CancellationToken cancellationToken = default)
{
if (state == null)
throw new ArgumentNullException(nameof(state));
// Store by saga ID
_statesBySagaId[state.SagaId] = state;
// Index by correlation ID
_sagaIdsByCorrelationId.AddOrUpdate(
state.CorrelationId,
_ => new List<string> { state.SagaId },
(_, list) =>
{
if (!list.Contains(state.SagaId))
list.Add(state.SagaId);
return list;
});
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<SagaStateSnapshot?> LoadStateAsync(string sagaId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(sagaId))
throw new ArgumentException("Saga ID cannot be null or empty", nameof(sagaId));
_statesBySagaId.TryGetValue(sagaId, out var state);
return Task.FromResult(state);
}
/// <inheritdoc />
public Task<List<SagaStateSnapshot>> GetByCorrelationIdAsync(
string correlationId,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(correlationId))
throw new ArgumentException("Correlation ID cannot be null or empty", nameof(correlationId));
if (!_sagaIdsByCorrelationId.TryGetValue(correlationId, out var sagaIds))
return Task.FromResult(new List<SagaStateSnapshot>());
var states = sagaIds
.Select(id => _statesBySagaId.TryGetValue(id, out var state) ? state : null)
.Where(s => s != null)
.Cast<SagaStateSnapshot>()
.ToList();
return Task.FromResult(states);
}
/// <inheritdoc />
public Task<List<SagaStateSnapshot>> GetByStateAsync(
SagaState state,
CancellationToken cancellationToken = default)
{
var states = _statesBySagaId.Values
.Where(s => s.State == state)
.ToList();
return Task.FromResult(states);
}
/// <inheritdoc />
public Task DeleteStateAsync(string sagaId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(sagaId))
throw new ArgumentException("Saga ID cannot be null or empty", nameof(sagaId));
if (_statesBySagaId.TryRemove(sagaId, out var state))
{
// Remove from correlation ID index
if (_sagaIdsByCorrelationId.TryGetValue(state.CorrelationId, out var list))
{
list.Remove(sagaId);
if (list.Count == 0)
_sagaIdsByCorrelationId.TryRemove(state.CorrelationId, out _);
}
}
return Task.CompletedTask;
}
}