diff --git a/src/JKang.EventSourcing.Abstractions.Tests/AggregateTest.cs b/src/JKang.EventSourcing.Abstractions.Tests/Domain/AggregateTest.cs similarity index 100% rename from src/JKang.EventSourcing.Abstractions.Tests/AggregateTest.cs rename to src/JKang.EventSourcing.Abstractions.Tests/Domain/AggregateTest.cs diff --git a/src/JKang.EventSourcing.Abstractions.Tests/JKang.EventSourcing.Abstractions.Tests.csproj b/src/JKang.EventSourcing.Abstractions.Tests/JKang.EventSourcing.Abstractions.Tests.csproj index cd45a2e..39da758 100644 --- a/src/JKang.EventSourcing.Abstractions.Tests/JKang.EventSourcing.Abstractions.Tests.csproj +++ b/src/JKang.EventSourcing.Abstractions.Tests/JKang.EventSourcing.Abstractions.Tests.csproj @@ -9,6 +9,7 @@ + all diff --git a/src/JKang.EventSourcing.Abstractions.Tests/Persistence/AggregateRepositoryTest.cs b/src/JKang.EventSourcing.Abstractions.Tests/Persistence/AggregateRepositoryTest.cs new file mode 100644 index 0000000..05b2210 --- /dev/null +++ b/src/JKang.EventSourcing.Abstractions.Tests/Persistence/AggregateRepositoryTest.cs @@ -0,0 +1,151 @@ +using AutoFixture.Xunit2; +using JKang.EventSourcing.Events; +using JKang.EventSourcing.Persistence; +using JKang.EventSourcing.Snapshotting.Persistence; +using JKang.EventSourcing.TestingFixtures; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace JKang.EventSourcing.Abstractions.Tests.Persistence +{ + public class AggregateRepositoryTest + { + private readonly Mock> _eventStore; + private readonly Mock> _snapshotStore; + private readonly GiftCardRepository _sut; + + public AggregateRepositoryTest() + { + _eventStore = new Mock>(); + _snapshotStore = new Mock>(); + _sut = new GiftCardRepository(_eventStore.Object, _snapshotStore.Object); + } + + [Theory, AutoData] + public async Task FindAggregateAsync_NoEvent_ReturnNull(Guid id) + { + // arrange + _eventStore + .Setup(x => x.GetEventsAsync(id, 1, int.MaxValue, It.IsAny())) + .ReturnsAsync(new IAggregateEvent[] { }); + + // act + GiftCard actual = await _sut.FindGiftCardAsync(id); + + // assert + Assert.Null(actual); + } + + [Theory, AutoData] + public async Task FindAggregateAsync_NoSnapshot_HappyPath(Guid id) + { + // arrange + _eventStore + .Setup(x => x.GetEventsAsync(id, 1, int.MaxValue, It.IsAny())) + .ReturnsAsync(new IAggregateEvent[] { + new GiftCardCreated(id, DateTime.UtcNow.AddDays(-2), 100), + new GiftCardDebited(id, 2, DateTime.UtcNow.AddDays(-1), 30) + }); + + // act + GiftCard actual = await _sut.FindGiftCardAsync(id); + + // assert + Assert.NotNull(actual); + Assert.Equal(2, actual.Version); + Assert.Null(actual.Snapshot); + Assert.Equal(70, actual.Balance); + } + + [Theory, AutoData] + public async Task FindAggregateAsync_IgnoreSnapshot_HappyPath(Guid id) + { + _eventStore + .Setup(x => x.GetEventsAsync(id, 1, int.MaxValue, It.IsAny())) + .ReturnsAsync(new IAggregateEvent[] { + new GiftCardCreated(id, DateTime.UtcNow.AddDays(-2), 100), + new GiftCardDebited(id, 2, DateTime.UtcNow.AddDays(-1), 30) + }); + + // act + GiftCard actual = await _sut.FindGiftCardAsync(id, ignoreSnapshot: true); + + // assert + Assert.NotNull(actual); + Assert.Equal(70, actual.Balance); + _snapshotStore.Verify(x => x.FindLastSnapshotAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Theory, AutoData] + public async Task FindAggregateAsync_WithSnapshot_HappyPath(Guid id, int snapshotVersion) + { + // arrange + _snapshotStore + .Setup(x => x.FindLastSnapshotAsync(id, int.MaxValue, It.IsAny())) + .ReturnsAsync(new GiftCardSnapshot(id, snapshotVersion, 100)); + _eventStore + .Setup(x => x.GetEventsAsync(id, snapshotVersion + 1, int.MaxValue, It.IsAny())) + .ReturnsAsync(new IAggregateEvent[] { + new GiftCardDebited(id, snapshotVersion + 1, DateTime.UtcNow.AddDays(-1), 30) + }); + + // act + GiftCard actual = await _sut.FindGiftCardAsync(id); + + // assert + Assert.NotNull(actual); + Assert.Equal(snapshotVersion + 1, actual.Version); + Assert.NotNull(actual.Snapshot); + Assert.Equal(70, actual.Balance); + } + + [Theory, AutoData] + public async Task FindAggregateAsync_WithVersion_WithSnapshot_HappyPath(Guid id, int version) + { + // arrange + _snapshotStore + .Setup(x => x.FindLastSnapshotAsync(id, version, It.IsAny())) + .ReturnsAsync(new GiftCardSnapshot(id, version - 2, 100)); + _eventStore + .Setup(x => x.GetEventsAsync(id, version - 1, version, It.IsAny())) + .ReturnsAsync(new IAggregateEvent[] { + new GiftCardDebited(id, version - 1, DateTime.UtcNow.AddDays(-1), 30), + new GiftCardDebited(id, version, DateTime.UtcNow, 30) + }); + + // act + GiftCard actual = await _sut.FindGiftCardAsync(id, version: version); + + // assert + Assert.NotNull(actual); + Assert.Equal(40, actual.Balance); + } + + [Theory, AutoData] + public async Task FindAggregateAsync_WithVersion_WithoutSnapshot_HappyPath(Guid id) + { + // arrange + _eventStore + .Setup(x => x.GetEventsAsync(id, 1, 3, It.IsAny())) + .ReturnsAsync(new IAggregateEvent[] { + new GiftCardCreated(id, DateTime.UtcNow.AddDays(-2), 100), + new GiftCardDebited(id, 2, DateTime.UtcNow.AddDays(-1), 30), + new GiftCardDebited(id, 3, DateTime.UtcNow, 30) + }); + + // act + GiftCard actual = await _sut.FindGiftCardAsync(id, version: 3); + + // assert + Assert.NotNull(actual); + Assert.Null(actual.Snapshot); + Assert.Equal(40, actual.Balance); + } + } +} diff --git a/src/JKang.EventSourcing.Abstractions/Persistence/AggregateRepository.cs b/src/JKang.EventSourcing.Abstractions/Persistence/AggregateRepository.cs index d35c1d3..0394fbd 100644 --- a/src/JKang.EventSourcing.Abstractions/Persistence/AggregateRepository.cs +++ b/src/JKang.EventSourcing.Abstractions/Persistence/AggregateRepository.cs @@ -53,18 +53,22 @@ protected virtual Task GetAggregateIdsAsync() protected virtual async Task FindAggregateAsync(TKey id, bool ignoreSnapshot = false, + int version = -1, CancellationToken cancellationToken = default) { + int maxVersion = version <= 0 ? int.MaxValue : version; + IAggregateSnapshot snapshot = null; if (!ignoreSnapshot) { snapshot = await _snapshotStore - .FindLastSnapshotAsync(id, cancellationToken) + .FindLastSnapshotAsync(id, maxVersion, cancellationToken) .ConfigureAwait(false); } + int minVersion = snapshot == null ? 1 : snapshot.AggregateVersion + 1; IAggregateEvent[] events = await _eventStore - .GetEventsAsync(id, snapshot == null ? 0 : snapshot.AggregateVersion, cancellationToken) + .GetEventsAsync(id, minVersion, maxVersion, cancellationToken) .ConfigureAwait(false); if (snapshot == null) diff --git a/src/JKang.EventSourcing.Abstractions/Persistence/IEventStore.cs b/src/JKang.EventSourcing.Abstractions/Persistence/IEventStore.cs index 22feb88..ecab1bd 100644 --- a/src/JKang.EventSourcing.Abstractions/Persistence/IEventStore.cs +++ b/src/JKang.EventSourcing.Abstractions/Persistence/IEventStore.cs @@ -12,7 +12,7 @@ Task AddEventAsync(IAggregateEvent e, CancellationToken cancellationToken = default); Task[]> GetEventsAsync(TKey aggregateId, - int skip = 0, + int minVersion, int maxVersion, CancellationToken cancellationToken = default); Task GetAggregateIdsAsync( diff --git a/src/JKang.EventSourcing.Abstractions/Snapshotting/Persistence/DefaultSnapshotStore.cs b/src/JKang.EventSourcing.Abstractions/Snapshotting/Persistence/DefaultSnapshotStore.cs index 4367d6e..20420bc 100644 --- a/src/JKang.EventSourcing.Abstractions/Snapshotting/Persistence/DefaultSnapshotStore.cs +++ b/src/JKang.EventSourcing.Abstractions/Snapshotting/Persistence/DefaultSnapshotStore.cs @@ -13,7 +13,8 @@ public Task AddSnapshotAsync(IAggregateSnapshot snapshot, CancellationToke return Task.CompletedTask; } - public Task> FindLastSnapshotAsync(TKey aggregateId, CancellationToken cancellationToken = default) + public Task> FindLastSnapshotAsync(TKey aggregateId, int maxVersion, + CancellationToken cancellationToken = default) { return Task.FromResult(null as IAggregateSnapshot); } diff --git a/src/JKang.EventSourcing.Abstractions/Snapshotting/Persistence/ISnapshotStore.cs b/src/JKang.EventSourcing.Abstractions/Snapshotting/Persistence/ISnapshotStore.cs index 2142ada..ff634e9 100644 --- a/src/JKang.EventSourcing.Abstractions/Snapshotting/Persistence/ISnapshotStore.cs +++ b/src/JKang.EventSourcing.Abstractions/Snapshotting/Persistence/ISnapshotStore.cs @@ -10,7 +10,7 @@ public interface ISnapshotStore Task AddSnapshotAsync(IAggregateSnapshot snapshot, CancellationToken cancellationToken = default); - Task> FindLastSnapshotAsync(TKey aggregateId, + Task> FindLastSnapshotAsync(TKey aggregateId, int maxVersion, CancellationToken cancellationToken = default); } } diff --git a/src/JKang.EventSourcing.Persistence.Caching/CachedAggregateRepository.cs b/src/JKang.EventSourcing.Persistence.Caching/CachedAggregateRepository.cs index a252fe2..5d6337b 100644 --- a/src/JKang.EventSourcing.Persistence.Caching/CachedAggregateRepository.cs +++ b/src/JKang.EventSourcing.Persistence.Caching/CachedAggregateRepository.cs @@ -42,19 +42,20 @@ protected CachedAggregateRepository( protected override async Task FindAggregateAsync( TKey id, bool ignoreSnapshot = false, + int version = -1, CancellationToken cancellationToken = default) { - string key = GetCacheKey(id); + string key = GetCacheKey(id, version); string serialized = await _cache.GetStringAsync(key, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(serialized)) { return DeserializeAggregate(serialized); } - TAggregate aggregate = await base.FindAggregateAsync(id, ignoreSnapshot, cancellationToken) + TAggregate aggregate = await base.FindAggregateAsync(id, ignoreSnapshot, version, cancellationToken) .ConfigureAwait(false); - await CacheAsync(aggregate, cancellationToken).ConfigureAwait(false); + await CacheAsync(aggregate, version, cancellationToken).ConfigureAwait(false); return aggregate; } @@ -71,20 +72,20 @@ protected override async Task> SaveAggregateAsync( IAggregateChangeset changeset = await base.SaveAggregateAsync(aggregate, cancellationToken) .ConfigureAwait(false); - await CacheAsync(aggregate, cancellationToken).ConfigureAwait(false); + await CacheAsync(aggregate, -1, cancellationToken).ConfigureAwait(false); return changeset; } - private async Task CacheAsync(TAggregate aggregate, + private async Task CacheAsync(TAggregate aggregate, int version, CancellationToken cancellationToken) { string serialized = SerializeAggregate(aggregate); - string key = GetCacheKey(aggregate.Id); + string key = GetCacheKey(aggregate.Id, version); await _cache.SetStringAsync(key, serialized, _cacheOptions, cancellationToken).ConfigureAwait(false); } - protected virtual string GetCacheKey(TKey id) => $"{typeof(TAggregate).FullName}_{id}"; + protected virtual string GetCacheKey(TKey id, int version) => $"{typeof(TAggregate).FullName}_{id}_{version}"; protected virtual TAggregate DeserializeAggregate(string serialized) { diff --git a/src/JKang.EventSourcing.Persistence.CosmosDB/CosmosDBEventStore.cs b/src/JKang.EventSourcing.Persistence.CosmosDB/CosmosDBEventStore.cs index 1eef94a..e39ea8d 100644 --- a/src/JKang.EventSourcing.Persistence.CosmosDB/CosmosDBEventStore.cs +++ b/src/JKang.EventSourcing.Persistence.CosmosDB/CosmosDBEventStore.cs @@ -74,13 +74,16 @@ public async Task GetAggregateIdsAsync( public async Task[]> GetEventsAsync( TKey aggregateId, - int skip = 0, + int minVersion, + int maxVersion, CancellationToken cancellationToken = default) { string query = $@" SELECT VALUE c.data FROM c -WHERE c.data.aggregateId = '{aggregateId}' AND c.data.aggregateVersion > {skip} +WHERE c.data.aggregateId = '{aggregateId}' AND + c.data.aggregateVersion >= {minVersion} AND + c.data.aggregateVersion <= {maxVersion} ORDER BY c.data.aggregateVersion"; FeedIterator> iterator = _container .GetItemQueryIterator>(new QueryDefinition(query)); diff --git a/src/JKang.EventSourcing.Persistence.CosmosDB/Snapshotting/CosmosDBSnapshotStore.cs b/src/JKang.EventSourcing.Persistence.CosmosDB/Snapshotting/CosmosDBSnapshotStore.cs index 13c4282..0433123 100644 --- a/src/JKang.EventSourcing.Persistence.CosmosDB/Snapshotting/CosmosDBSnapshotStore.cs +++ b/src/JKang.EventSourcing.Persistence.CosmosDB/Snapshotting/CosmosDBSnapshotStore.cs @@ -49,13 +49,13 @@ await _container .ConfigureAwait(false); } - public async Task> FindLastSnapshotAsync(TKey aggregateId, + public async Task> FindLastSnapshotAsync(TKey aggregateId, int maxVersion, CancellationToken cancellationToken = default) { string query = $@" SELECT TOP 1 VALUE c.data FROM c -WHERE c.data.aggregateId = '{aggregateId}' +WHERE c.data.aggregateId = '{aggregateId}' AND c.data.aggregateVersion <= {maxVersion} ORDER BY c.data.aggregateVersion DESC"; FeedIterator> iterator = _container diff --git a/src/JKang.EventSourcing.Persistence.DynamoDB/DynamoDBEventStore.cs b/src/JKang.EventSourcing.Persistence.DynamoDB/DynamoDBEventStore.cs index cc20b74..35060db 100644 --- a/src/JKang.EventSourcing.Persistence.DynamoDB/DynamoDBEventStore.cs +++ b/src/JKang.EventSourcing.Persistence.DynamoDB/DynamoDBEventStore.cs @@ -64,10 +64,13 @@ public async Task GetAggregateIdsAsync( public async Task[]> GetEventsAsync( TKey aggregateId, - int skip = 0, + int minVersion, + int maxVersion, CancellationToken cancellationToken = default) { - Search search = _table.Query(aggregateId as dynamic, new QueryFilter("aggregateVersion", QueryOperator.GreaterThan, skip)); + var filter = new QueryFilter("aggregateVersion", QueryOperator.GreaterThanOrEqual, minVersion); + filter.AddCondition("aggregateVersion", QueryOperator.LessThanOrEqual, maxVersion); + Search search = this._table.Query(aggregateId as dynamic, filter); var events = new List>(); do diff --git a/src/JKang.EventSourcing.Persistence.DynamoDB/Snapshotting/DynamoDBSnapshotStore.cs b/src/JKang.EventSourcing.Persistence.DynamoDB/Snapshotting/DynamoDBSnapshotStore.cs index d420d13..6b1e4ed 100644 --- a/src/JKang.EventSourcing.Persistence.DynamoDB/Snapshotting/DynamoDBSnapshotStore.cs +++ b/src/JKang.EventSourcing.Persistence.DynamoDB/Snapshotting/DynamoDBSnapshotStore.cs @@ -42,13 +42,15 @@ public async Task AddSnapshotAsync(IAggregateSnapshot snapshot, await _table.PutItemAsync(item, cancellationToken).ConfigureAwait(false); } - public async Task> FindLastSnapshotAsync(TKey aggregateId, + public async Task> FindLastSnapshotAsync(TKey aggregateId, int maxVersion, CancellationToken cancellationToken = default) { + var filter = new QueryFilter("aggregateId", QueryOperator.Equal, aggregateId as dynamic); + filter.AddCondition("aggregateVersion", QueryOperator.LessThanOrEqual, maxVersion); Search search = _table.Query(new QueryOperationConfig { - Filter = new QueryFilter("aggregateId", QueryOperator.Equal, aggregateId as dynamic), + Filter = filter, Limit = 1, BackwardSearch = true, }); diff --git a/src/JKang.EventSourcing.Persistence.EfCore/EfCoreEventStore.cs b/src/JKang.EventSourcing.Persistence.EfCore/EfCoreEventStore.cs index e0366c3..5b12685 100644 --- a/src/JKang.EventSourcing.Persistence.EfCore/EfCoreEventStore.cs +++ b/src/JKang.EventSourcing.Persistence.EfCore/EfCoreEventStore.cs @@ -51,13 +51,14 @@ public Task GetAggregateIdsAsync( public async Task[]> GetEventsAsync( TKey aggregateId, - int skip = 0, + int minVersion, int maxVersion, CancellationToken cancellationToken = default) { List serializedEvents = await _context.GetEventDbSet() .Where(x => x.AggregateId.Equals(aggregateId)) + .Where(x => x.AggregateVersion >= minVersion) + .Where(x => x.AggregateVersion <= maxVersion) .OrderBy(x => x.AggregateVersion) - .Skip(skip) .Select(x => x.Serialized) .ToListAsync(cancellationToken) .ConfigureAwait(false); diff --git a/src/JKang.EventSourcing.Persistence.EfCore/Snapshotting/EfCoreSnapshotStore.cs b/src/JKang.EventSourcing.Persistence.EfCore/Snapshotting/EfCoreSnapshotStore.cs index 4122296..9162ab8 100644 --- a/src/JKang.EventSourcing.Persistence.EfCore/Snapshotting/EfCoreSnapshotStore.cs +++ b/src/JKang.EventSourcing.Persistence.EfCore/Snapshotting/EfCoreSnapshotStore.cs @@ -39,11 +39,12 @@ public async Task AddSnapshotAsync(IAggregateSnapshot snapshot, await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } - public async Task> FindLastSnapshotAsync(TKey aggregateId, + public async Task> FindLastSnapshotAsync(TKey aggregateId, int maxVersion, CancellationToken cancellationToken = default) { string serialized = await _context.GetSnapshotDbSet() .Where(x => x.AggregateId.Equals(aggregateId)) + .Where(x => x.AggregateVersion <= maxVersion) .OrderByDescending(x => x.AggregateVersion) .Select(x => x.Serialized) .FirstOrDefaultAsync(cancellationToken) diff --git a/src/JKang.EventSourcing.Persistence.FileSystem/Snapshotting/TextFileSnapshotStore.cs b/src/JKang.EventSourcing.Persistence.FileSystem/Snapshotting/TextFileSnapshotStore.cs index 5fb5364..66246b0 100644 --- a/src/JKang.EventSourcing.Persistence.FileSystem/Snapshotting/TextFileSnapshotStore.cs +++ b/src/JKang.EventSourcing.Persistence.FileSystem/Snapshotting/TextFileSnapshotStore.cs @@ -41,7 +41,7 @@ public Task AddSnapshotAsync(IAggregateSnapshot snapshot, return Task.CompletedTask; } - public Task> FindLastSnapshotAsync(TKey aggregateId, + public Task> FindLastSnapshotAsync(TKey aggregateId, int maxVersion, CancellationToken cancellationToken = default) { if (!Directory.Exists(_options.Folder)) @@ -54,6 +54,7 @@ public Task> FindLastSnapshotAsync(TKey aggregateId, .Select(x => Path.GetFileNameWithoutExtension(x)) .Select(x => x.Split('.').LastOrDefault()) .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out int version) ? version : -1) + .Where(x => x <= maxVersion) .OrderByDescending(x => x) .FirstOrDefault(); @@ -70,6 +71,6 @@ public Task> FindLastSnapshotAsync(TKey aggregateId, } private string GetFilePath(TKey aggregateId, int version) - => Path.Combine(_options.Folder, $"{aggregateId}.{version.ToString(CultureInfo.InvariantCulture)}.snapshot"); + => Path.Combine(_options.Folder, $"{aggregateId}.{version.ToString(CultureInfo.InvariantCulture)}.snapshot"); } } diff --git a/src/JKang.EventSourcing.Persistence.FileSystem/TextFileEventStore.cs b/src/JKang.EventSourcing.Persistence.FileSystem/TextFileEventStore.cs index c35b4af..ed27501 100644 --- a/src/JKang.EventSourcing.Persistence.FileSystem/TextFileEventStore.cs +++ b/src/JKang.EventSourcing.Persistence.FileSystem/TextFileEventStore.cs @@ -73,8 +73,7 @@ public Task GetAggregateIdsAsync( } public async Task[]> GetEventsAsync(TKey aggregateId, - int skip = 0, - CancellationToken cancellationToken = default) + int minVersion, int maxVersion, CancellationToken cancellationToken = default) { string filePath = GetFilePath(aggregateId); if (!File.Exists(filePath)) @@ -89,13 +88,18 @@ public async Task[]> GetEventsAsync(TKey aggregateId, string serialized = await sr.ReadLineAsync().ConfigureAwait(false); while (!string.IsNullOrEmpty(serialized)) { - if (skip > 0) + if (minVersion > 1) { - skip--; + minVersion--; } else { - events.Add(JsonConvert.DeserializeObject>(serialized, Defaults.JsonSerializerSettings)); + IAggregateEvent @event = JsonConvert.DeserializeObject>(serialized, Defaults.JsonSerializerSettings); + events.Add(@event); + if (@event.AggregateVersion >= maxVersion) + { + break; + } } serialized = await sr.ReadLineAsync().ConfigureAwait(false); } diff --git a/src/JKang.EventSourcing.TestingFixtures/CachedGiftCardRepository.cs b/src/JKang.EventSourcing.TestingFixtures/CachedGiftCardRepository.cs index 3a33533..fb277cf 100644 --- a/src/JKang.EventSourcing.TestingFixtures/CachedGiftCardRepository.cs +++ b/src/JKang.EventSourcing.TestingFixtures/CachedGiftCardRepository.cs @@ -20,7 +20,10 @@ public CachedGiftCardRepository( public Task SaveGiftCardAsync(GiftCard giftCard) => SaveAggregateAsync(giftCard); - public Task FindGiftCardAsync(Guid id) => FindAggregateAsync(id); + public Task FindGiftCardAsync(Guid id, + bool ignoreSnapshot = false, + int version = -1) => + FindAggregateAsync(id, ignoreSnapshot, version); public Task GetGiftCardIdsAsync() => GetAggregateIdsAsync(); } diff --git a/src/JKang.EventSourcing.TestingFixtures/GiftCardRepository.cs b/src/JKang.EventSourcing.TestingFixtures/GiftCardRepository.cs index 26367de..d93fc7f 100644 --- a/src/JKang.EventSourcing.TestingFixtures/GiftCardRepository.cs +++ b/src/JKang.EventSourcing.TestingFixtures/GiftCardRepository.cs @@ -15,7 +15,10 @@ public GiftCardRepository( public Task SaveGiftCardAsync(GiftCard giftCard) => SaveAggregateAsync(giftCard); - public Task FindGiftCardAsync(Guid id) => FindAggregateAsync(id); + public Task FindGiftCardAsync(Guid id, + bool ignoreSnapshot = false, + int version = -1) => + FindAggregateAsync(id, ignoreSnapshot, version); public Task GetGiftCardIdsAsync() => GetAggregateIdsAsync(); } diff --git a/src/JKang.EventSourcing.TestingFixtures/IGiftCardRepository.cs b/src/JKang.EventSourcing.TestingFixtures/IGiftCardRepository.cs index 37a0811..ad6e309 100644 --- a/src/JKang.EventSourcing.TestingFixtures/IGiftCardRepository.cs +++ b/src/JKang.EventSourcing.TestingFixtures/IGiftCardRepository.cs @@ -6,7 +6,9 @@ namespace JKang.EventSourcing.TestingFixtures public interface IGiftCardRepository { Task SaveGiftCardAsync(GiftCard giftCard); - Task FindGiftCardAsync(Guid id); + Task FindGiftCardAsync(Guid id, + bool ignoreSnapshot = false, + int version = -1); Task GetGiftCardIdsAsync(); } } diff --git a/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Create.cshtml.cs b/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Create.cshtml.cs index 703aff8..66d26a6 100644 --- a/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Create.cshtml.cs +++ b/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Create.cshtml.cs @@ -10,7 +10,7 @@ public class CreateModel : PageModel private readonly IGiftCardRepository _repository; [BindProperty] - public decimal InitialCredit { get; set; } = 100; + public decimal InitialCredit { get; set; } = 500; public CreateModel(IGiftCardRepository repository) { diff --git a/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Details.cshtml b/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Details.cshtml index b8a31ba..35244c7 100644 --- a/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Details.cshtml +++ b/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Details.cshtml @@ -6,6 +6,13 @@

@ViewData["Title"]

+@if (Model.Error != null) +{ + +} +

Balance: @Model.GiftCard.Balance.ToString("0.00") €

Version: @Model.GiftCard.Version @@ -32,8 +39,17 @@ asp-page-handler="TakeSnapshot" asp-route-id="@Model.GiftCard.Id" /> + + + +

History

diff --git a/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Details.cshtml.cs b/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Details.cshtml.cs index 8fb09c2..089af61 100644 --- a/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Details.cshtml.cs +++ b/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/Details.cshtml.cs @@ -17,6 +17,9 @@ public DetailsModel(IGiftCardRepository repository) public GiftCard GiftCard { get; private set; } + [TempData] + public string Error { get; set; } + [BindProperty] public decimal Amount { get; set; } = 30; @@ -30,11 +33,18 @@ public async Task OnGetAsync(Guid id) public async Task OnPostDebitAsync(Guid id) { - GiftCard = await _repository.FindGiftCardAsync(id) - ?? throw new InvalidOperationException("Gift card not found"); - - GiftCard.Debit(Amount); - await _repository.SaveGiftCardAsync(GiftCard); + try + { + GiftCard = await _repository.FindGiftCardAsync(id) + ?? throw new InvalidOperationException("Gift card not found"); + + GiftCard.Debit(Amount); + await _repository.SaveGiftCardAsync(GiftCard); + } + catch (InvalidOperationException ex) + { + Error = ex.Message; + } return RedirectToPage(new { id }); } diff --git a/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/PreviousVersion.cshtml b/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/PreviousVersion.cshtml new file mode 100644 index 0000000..9ef7bff --- /dev/null +++ b/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/PreviousVersion.cshtml @@ -0,0 +1,83 @@ +@page +@model JKang.EventSourcing.TestingWebApp.Pages.GiftCards.PreviousVersionModel +@{ + ViewData["Title"] = $"Gift card #{Model.GiftCard.Id}"; +} + +

@ViewData["Title"]

+ +
+ +
+
+
+
+ + + + + + +
+ +
+ @if (Model.IgnoreSnapshot) + { + + } + else + { + + } + + +
+
+ +

Balance: @Model.GiftCard.Balance.ToString("0.00") €

+
+ +
+

History

+ + @if (Model.GiftCard.Snapshot != null) + { + + } + + + + + + + + + + + @foreach (var @e in Model.GiftCard.Events.OrderByDescending(x => x.AggregateVersion)) + { + + + + + + } + +
VersionTimestampEvent
@e.AggregateVersion@e.Timestamp@e
+
+
+ diff --git a/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/PreviousVersion.cshtml.cs b/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/PreviousVersion.cshtml.cs new file mode 100644 index 0000000..131c3da --- /dev/null +++ b/src/JKang.EventSourcing.TestingWebApp/Pages/GiftCards/PreviousVersion.cshtml.cs @@ -0,0 +1,32 @@ +using JKang.EventSourcing.TestingFixtures; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System; +using System.Threading.Tasks; + +namespace JKang.EventSourcing.TestingWebApp.Pages.GiftCards +{ + public class PreviousVersionModel : PageModel + { + private readonly IGiftCardRepository _repository; + + public PreviousVersionModel(IGiftCardRepository repository) + { + _repository = repository; + } + + public int MaxVersion { get; private set; } + public bool IgnoreSnapshot { get; private set; } + public GiftCard GiftCard { get; private set; } + + public async Task OnGetAsync(Guid id, int maxVersion, string ignoreSnapshot = "off", int version = -1) + { + MaxVersion = maxVersion; + IgnoreSnapshot = ignoreSnapshot == "on"; + GiftCard = await _repository.FindGiftCardAsync(id, IgnoreSnapshot, version) + ?? throw new InvalidOperationException("Gift card not found"); + + return Page(); + } + } +} \ No newline at end of file diff --git a/src/JKang.EventSourcing.TestingWebApp/Pages/Index.cshtml b/src/JKang.EventSourcing.TestingWebApp/Pages/Index.cshtml index cca35fc..a45cae9 100644 --- a/src/JKang.EventSourcing.TestingWebApp/Pages/Index.cshtml +++ b/src/JKang.EventSourcing.TestingWebApp/Pages/Index.cshtml @@ -4,9 +4,7 @@ ViewData["Title"] = "Home page"; } -New gift card - -

Existing gift cards

+

Gift cards

    @foreach (var id in Model.GiftCardIds) { @@ -14,4 +12,9 @@ @id } -
\ No newline at end of file + + + + diff --git a/src/JKang.EventSourcing.TestingWebApp/Startup.cs b/src/JKang.EventSourcing.TestingWebApp/Startup.cs index 34930e1..62f9b50 100644 --- a/src/JKang.EventSourcing.TestingWebApp/Startup.cs +++ b/src/JKang.EventSourcing.TestingWebApp/Startup.cs @@ -30,7 +30,7 @@ public void ConfigureServices(IServiceCollection services) services.AddRazorPages(); services - .AddScoped(); + .AddScoped(); // change the following value to switch persistence mode PersistenceMode persistenceMode = PersistenceMode.EfCore;