diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index 0a7dade8a..2770d4e1a 100644 --- a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -20,8 +20,7 @@ namespace Squidex.Domain.Apps.Entities IEntityWithVersion, IUpdateableEntity, IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy, - IUpdateableEntityWithVersion + IUpdateableEntityWithLastModifiedBy where T : Cloneable { [JsonProperty] diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 6dd770faf..0c2dba475 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -19,7 +19,6 @@ namespace Squidex.Infrastructure.EventSourcing { public class MongoEventStore : MongoRepositoryBase, IEventStore { - private const long AnyVersion = long.MinValue; private const int MaxAttempts = 20; private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); private static readonly FieldDefinition TimestampField = Fields.Build(x => x.Timestamp); @@ -130,7 +129,7 @@ namespace Squidex.Infrastructure.EventSourcing public Task AppendEventsAsync(Guid commitId, string streamName, ICollection events) { - return AppendEventsInternalAsync(commitId, streamName, AnyVersion, events); + return AppendEventsInternalAsync(commitId, streamName, ExpectedVersion.Any, events); } public Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) @@ -152,7 +151,7 @@ namespace Squidex.Infrastructure.EventSourcing var currentVersion = await GetEventStreamOffset(streamName); - if (expectedVersion != AnyVersion && expectedVersion != currentVersion) + if (expectedVersion != ExpectedVersion.Any && expectedVersion != currentVersion) { throw new WrongEventVersionException(currentVersion, expectedVersion); } @@ -175,7 +174,7 @@ namespace Squidex.Infrastructure.EventSourcing { currentVersion = await GetEventStreamOffset(streamName); - if (expectedVersion != AnyVersion) + if (expectedVersion != ExpectedVersion.Any) { throw new WrongEventVersionException(currentVersion, expectedVersion); } diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index 96c96fa00..32135d0fb 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -78,24 +78,19 @@ namespace Squidex.Infrastructure.Commands public async Task WriteAsync(ISemanticLog log) { - var newVersion = Version + uncomittedEvents.Count; + await persistence.WriteSnapshotAsync(state); - if (newVersion != Version) + try { - await persistence.WriteSnapshotAsync(state, newVersion); - - try - { - await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); - } - catch (Exception ex) - { - log.LogFatal(ex, w => w.WriteProperty("action", "writeEvents")); - } - finally - { - uncomittedEvents.Clear(); - } + await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); + } + catch (Exception ex) + { + log.LogFatal(ex, w => w.WriteProperty("action", "writeEvents")); + } + finally + { + uncomittedEvents.Clear(); } } } diff --git a/src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs b/src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs new file mode 100644 index 000000000..3908d3cd2 --- /dev/null +++ b/src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ExpectedVersion.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + public static class ExpectedVersion + { + public const int Any = -2; + + public const int Empty = -1; + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs index a40f15a66..eff3b2f72 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs @@ -68,17 +68,17 @@ namespace Squidex.Infrastructure.EventSourcing.Grains public virtual void Stop() { - dispatcher.DispatchAsync(() => HandleStopAsync()).Forget(); + dispatcher.DispatchAsync(HandleStopAsync).Forget(); } public virtual void Start() { - dispatcher.DispatchAsync(() => HandleStartAsync()).Forget(); + dispatcher.DispatchAsync(HandleStartAsync).Forget(); } public virtual void Reset() { - dispatcher.DispatchAsync(() => HandleResetAsync()).Forget(); + dispatcher.DispatchAsync(HandleResetAsync).Forget(); } public virtual void Activate(IEventConsumer eventConsumer) diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/src/Squidex.Infrastructure/States/IPersistence.cs index 25787a561..3c9528178 100644 --- a/src/Squidex.Infrastructure/States/IPersistence.cs +++ b/src/Squidex.Infrastructure/States/IPersistence.cs @@ -18,8 +18,8 @@ namespace Squidex.Infrastructure.States Task WriteEventsAsync(IEnumerable> @events); - Task WriteSnapshotAsync(TState state, long newVersion = -1); + Task WriteSnapshotAsync(TState state); - Task ReadAsync(long? expectedVersion = null); + Task ReadAsync(long expectedVersion = ExpectedVersion.Any); } } diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index 5e91a3905..542320f75 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -21,15 +21,17 @@ namespace Squidex.Infrastructure.States private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; private readonly IEventDataFormatter eventDataFormatter; + private readonly PersistenceMode persistenceMode; private readonly Action invalidate; private readonly Func applyState; private readonly Func, Task> applyEvent; - private long positionSnapshot = -1; - private long positionEvent = -1; + private long versionSnapshot = -1; + private long versionEvents = -1; + private long version; public long Version { - get { return Math.Max(positionEvent, positionSnapshot); } + get { return version; } } public Persistence(string ownerKey, @@ -38,6 +40,7 @@ namespace Squidex.Infrastructure.States IEventDataFormatter eventDataFormatter, ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, + PersistenceMode persistenceMode, Func applyState, Func, Task> applyEvent) { @@ -47,37 +50,61 @@ namespace Squidex.Infrastructure.States this.invalidate = invalidate; this.eventStore = eventStore; this.eventDataFormatter = eventDataFormatter; + this.persistenceMode = persistenceMode; this.snapshotStore = snapshotStore; this.streamNameResolver = streamNameResolver; } - public async Task ReadAsync(long? expectedVersion) + public async Task ReadAsync(long expectedVersion = ExpectedVersion.Any) { - positionSnapshot = -1; - positionEvent = -1; + versionSnapshot = -1; + versionEvents = -1; - if (applyState != null) + await ReadSnapshotAsync(); + await ReadEventsAsync(); + + UpdateVersion(); + + if (expectedVersion != ExpectedVersion.Any && expectedVersion != version) + { + if (version == ExpectedVersion.Empty) + { + throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner)); + } + else + { + throw new DomainObjectVersionException(ownerKey, typeof(TOwner), version, expectedVersion); + } + } + } + + private async Task ReadSnapshotAsync() + { + if (UseSnapshots()) { var (state, position) = await snapshotStore.ReadAsync(ownerKey); - positionSnapshot = position; - positionEvent = position; + versionSnapshot = position; + versionEvents = position; if (applyState != null && position >= 0) { await applyState(state); } } + } - if (applyEvent != null && streamNameResolver != null) + private async Task ReadEventsAsync() + { + if (UseEventSourcing()) { - var events = await eventStore.GetEventsAsync(GetStreamName(), positionEvent + 1); + var events = await eventStore.GetEventsAsync(GetStreamName(), versionEvents + 1); foreach (var @event in events) { - positionEvent++; + versionEvents++; - if (@event.EventStreamNumber != positionEvent) + if (@event.EventStreamNumber != versionEvents) { throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); } @@ -90,46 +117,28 @@ namespace Squidex.Infrastructure.States } } } - - var newVersion = Version; - - if (expectedVersion.HasValue && expectedVersion.Value != newVersion) - { - if (newVersion == -1) - { - throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner)); - } - else - { - throw new DomainObjectVersionException(ownerKey, typeof(TOwner), newVersion, expectedVersion.Value); - } - } } - public async Task WriteSnapshotAsync(TState state, long newVersion = -1) + public async Task WriteSnapshotAsync(TState state) { - if (newVersion < 0) - { - newVersion = - applyEvent != null ? - positionEvent : - positionSnapshot + 1; - } + var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; - if (newVersion != positionSnapshot) + if (newVersion != versionSnapshot) { try { - await snapshotStore.WriteAsync(ownerKey, state, positionSnapshot, newVersion); + await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); } catch (InconsistentStateException ex) { throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); } - positionSnapshot = newVersion; + versionSnapshot = newVersion; } + UpdateVersion(); + invalidate?.Invoke(); } @@ -141,6 +150,8 @@ namespace Squidex.Infrastructure.States if (eventArray.Length > 0) { + var expectedVersion = UseEventSourcing() ? version : ExpectedVersion.Any; + var commitId = Guid.NewGuid(); var eventStream = GetStreamName(); @@ -155,9 +166,11 @@ namespace Squidex.Infrastructure.States throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); } - positionEvent += eventArray.Length; + versionEvents += eventArray.Length; } + UpdateVersion(); + invalidate?.Invoke(); } @@ -171,6 +184,16 @@ namespace Squidex.Infrastructure.States return streamNameResolver.GetStreamName(typeof(TOwner), ownerKey); } + private bool UseSnapshots() + { + return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + + private bool UseEventSourcing() + { + return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + private Envelope ParseKnownEvent(StoredEvent storedEvent) { try @@ -182,5 +205,21 @@ namespace Squidex.Infrastructure.States return null; } } + + private void UpdateVersion() + { + if (persistenceMode == PersistenceMode.Snapshots) + { + version = versionSnapshot; + } + else if (persistenceMode == PersistenceMode.EventSourcing) + { + version = versionEvents; + } + else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) + { + version = Math.Max(versionEvents, versionSnapshot); + } + } } } diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs b/src/Squidex.Infrastructure/States/PersistenceMode.cs similarity index 66% rename from src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs rename to src/Squidex.Infrastructure/States/PersistenceMode.cs index 229f7ea2d..b1c09fa62 100644 --- a/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs +++ b/src/Squidex.Infrastructure/States/PersistenceMode.cs @@ -1,15 +1,17 @@ // ========================================================================== -// IUpdateableEntityWithVersion.cs +// PersistenceMode.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Infrastructure.States { - public interface IUpdateableEntityWithVersion + public enum PersistenceMode { - long Version { get; set; } + EventSourcing, + Snapshots, + SnapshotsAndEventSourcing } } diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs index b217dac38..578762dc5 100644 --- a/src/Squidex.Infrastructure/States/Store.cs +++ b/src/Squidex.Infrastructure/States/Store.cs @@ -36,26 +36,26 @@ namespace Squidex.Infrastructure.States public IPersistence WithEventSourcing(string key, Func, Task> applyEvent) { - return CreatePersistence(key, null, applyEvent); + return CreatePersistence(key, PersistenceMode.EventSourcing, null, applyEvent); } public IPersistence WithSnapshots(string key, Func applySnapshot) { - return CreatePersistence(key, applySnapshot, null); + return CreatePersistence(key, PersistenceMode.Snapshots, applySnapshot, null); } public IPersistence WithSnapshotsAndEventSourcing(string key, Func applySnapshot, Func, Task> applyEvent) { - return CreatePersistence(key, applySnapshot, applyEvent); + return CreatePersistence(key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); } - private IPersistence CreatePersistence(string key, Func applySnapshot, Func, Task> applyEvent) + private IPersistence CreatePersistence(string key, PersistenceMode mode, Func applySnapshot, Func, Task> applyEvent) { Guard.NotNullOrEmpty(key, nameof(key)); var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); - return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applySnapshot, applyEvent); + return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); } } } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs index 661d2f8e8..aea30eb48 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs @@ -77,7 +77,7 @@ namespace Squidex.Infrastructure.Commands await sut.WriteAsync(A.Fake()); - A.CallTo(() => persistence.WriteSnapshotAsync(newState, 102)) + A.CallTo(() => persistence.WriteSnapshotAsync(newState)) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 2))) .MustHaveHappened(); @@ -110,7 +110,7 @@ namespace Squidex.Infrastructure.Commands await sut.WriteAsync(A.Fake()); - A.CallTo(() => persistence.WriteSnapshotAsync(newState, 102)) + A.CallTo(() => persistence.WriteSnapshotAsync(newState)) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 2))) .MustHaveHappened(); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs index 4a081616d..85f873321 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -67,11 +67,11 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.Name) .Returns(consumerName); - A.CallTo(() => persistence.ReadAsync(null)) - .Invokes(new Action(s => apply(state))); + A.CallTo(() => persistence.ReadAsync(ExpectedVersion.Any)) + .Invokes(new Action(s => apply(state))); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) - .Invokes(new Action((s, v) => state = s)); + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .Invokes(new Action(s => state = s)); A.CallTo(() => formatter.Parse(eventData, true)).Returns(envelope); @@ -132,7 +132,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -150,7 +150,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = null, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Twice); A.CallTo(() => eventConsumer.ClearAsync()) @@ -180,7 +180,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventConsumer.On(envelope)) @@ -204,7 +204,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventConsumer.On(envelope)) @@ -243,7 +243,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -264,7 +264,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustNotHaveHappened(); } @@ -284,7 +284,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -313,7 +313,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .MustHaveHappened(); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -344,7 +344,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .MustNotHaveHappened(); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => eventSubscription.StopAsync()) @@ -375,7 +375,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .MustHaveHappened(); - A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored, -1)) + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(Repeated.Exactly.Twice); A.CallTo(() => eventSubscription.StopAsync()) diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs b/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs index 90f86cf37..ba19dd2c7 100644 --- a/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs @@ -65,6 +65,7 @@ namespace Squidex.Infrastructure.Reflection Assert.Equal(value.Value1, copy.Value1); Assert.Equal(value.Value2, copy.Value2); + Assert.Equal(0, copy.ValueReadOnly); Assert.Equal(value.Cloneable.Value, copy.Cloneable.Value); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs index cb1d48e71..c7ae719ce 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs @@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.States private readonly List appliedEvents = new List(); private IPersistence persistence; - public long? ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } public List AppliedEvents { @@ -54,7 +54,7 @@ namespace Squidex.Infrastructure.States { private IPersistence persistence; - public long? ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } public Task ActivateAsync(string key, IStore store) { @@ -115,7 +115,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_read_events_from_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = null; + statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -131,7 +131,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_events_are_older_than_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = null; + statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -144,7 +144,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_events_have_gaps_to_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = null; + statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -177,7 +177,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_throw_exception_if_noting_expected() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; SetupEventStore(0); @@ -187,7 +187,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_provide_state_from_services_and_add_to_cache() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; SetupEventStore(0); @@ -200,7 +200,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_serve_next_request_from_cache() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; SetupEventStore(0); @@ -218,7 +218,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_to_store_with_previous_position() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; InvalidateMessage message = null; @@ -248,7 +248,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_position() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; SetupEventStore(3); @@ -263,7 +263,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_remove_from_cache_when_invalidation_message_received() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -275,7 +275,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_return_same_instance_for_parallel_requests() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => ((object)1, 1L))); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index cbca0905b..b7d91bb0c 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -15,6 +15,8 @@ using Microsoft.Extensions.Options; using Squidex.Infrastructure.EventSourcing; using Xunit; +#pragma warning disable RECS0002 // Convert anonymous method to method group + namespace Squidex.Infrastructure.States { public class StateSnapshotTests : IDisposable @@ -24,7 +26,7 @@ namespace Squidex.Infrastructure.States private IPersistence persistence; private int state; - public long? ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } public int State { @@ -43,9 +45,9 @@ namespace Squidex.Infrastructure.States state = value; } - public Task WriteStateAsync(long newVersion = -1) + public Task WriteStateAsync() { - return persistence.WriteSnapshotAsync(state, newVersion); + return persistence.WriteSnapshotAsync(state); } } @@ -117,7 +119,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_throw_exception_if_noting_expected() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, -1)); @@ -128,7 +130,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_provide_state_from_services_and_add_to_cache() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -139,7 +141,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_serve_next_request_from_cache() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; var actualObject1 = await sut.GetSingleAsync(key); @@ -155,7 +157,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_serve_next_request_from_cache_when_detached() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; var actualObject1 = await sut.CreateAsync(key); @@ -171,7 +173,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_to_store_with_previous_version() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; InvalidateMessage message = null; @@ -199,31 +201,10 @@ namespace Squidex.Infrastructure.States Assert.Equal(key, message.Key); } - [Fact] - public async Task Should_write_to_store_with_explicit_version() - { - statefulObject.ExpectedVersion = null; - - A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((123, 1)); - - var actualObject = await sut.GetSingleAsync(key); - - Assert.Same(statefulObject, actualObject); - Assert.Equal(123, statefulObject.State); - - statefulObject.SetState(456); - - await statefulObject.WriteStateAsync(100); - - A.CallTo(() => snapshotStore.WriteAsync(key, 456, 1, 100)) - .MustHaveHappened(); - } - [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_version() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 13)); @@ -239,7 +220,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_remove_from_cache_when_invalidation_message_received() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -251,7 +232,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_return_same_instance_for_parallel_requests() { - statefulObject.ExpectedVersion = null; + statefulObject.ExpectedVersion = ExpectedVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L)));