diff --git a/source/Khala.EventSourcing.Azure/EventSourcing/Azure/AggregateEntity.cs b/source/Khala.EventSourcing.Azure/EventSourcing/Azure/AggregateEntity.cs index 7c22466..9b92ae8 100644 --- a/source/Khala.EventSourcing.Azure/EventSourcing/Azure/AggregateEntity.cs +++ b/source/Khala.EventSourcing.Azure/EventSourcing/Azure/AggregateEntity.cs @@ -5,19 +5,19 @@ public abstract class AggregateEntity : TableEntity { - public static string GetPartitionKey(Type aggregateType, Guid aggregateId) + public static string GetPartitionKey(Type sourceType, Guid sourceId) { - if (aggregateType == null) + if (sourceType == null) { - throw new ArgumentNullException(nameof(aggregateType)); + throw new ArgumentNullException(nameof(sourceType)); } - if (aggregateId == Guid.Empty) + if (sourceId == Guid.Empty) { - throw new ArgumentException("Value cannot be empty.", nameof(aggregateId)); + throw new ArgumentException("Value cannot be empty.", nameof(sourceId)); } - return $"{aggregateType.Name}-{aggregateId:n}"; + return $"{sourceType.Name}-{sourceId:n}"; } } } diff --git a/source/Khala.EventSourcing.Azure/EventSourcing/Azure/AzureEventStore.cs b/source/Khala.EventSourcing.Azure/EventSourcing/Azure/AzureEventStore.cs index 0cefdb0..3520694 100644 --- a/source/Khala.EventSourcing.Azure/EventSourcing/Azure/AzureEventStore.cs +++ b/source/Khala.EventSourcing.Azure/EventSourcing/Azure/AzureEventStore.cs @@ -9,17 +9,12 @@ using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Table; - using static Microsoft.WindowsAzure.Storage.Table.QueryComparisons; - using static Microsoft.WindowsAzure.Storage.Table.TableOperators; - using static Microsoft.WindowsAzure.Storage.Table.TableQuery; - public class AzureEventStore : IAzureEventStore { - private CloudTable _eventTable; - private IMessageSerializer _serializer; + private readonly CloudTable _eventTable; + private readonly IMessageSerializer _serializer; - public AzureEventStore( - CloudTable eventTable, IMessageSerializer serializer) + public AzureEventStore(CloudTable eventTable, IMessageSerializer serializer) { _eventTable = eventTable ?? throw new ArgumentNullException(nameof(eventTable)); _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); @@ -40,6 +35,13 @@ public Task SaveEvents( var domainEvents = events.ToList(); + if (domainEvents.Count == 0) + { + return Task.FromResult(true); + } + + IDomainEvent firstEvent = domainEvents.First(); + for (int i = 0; i < domainEvents.Count; i++) { if (domainEvents[i] == null) @@ -48,6 +50,20 @@ public Task SaveEvents( $"{nameof(events)} cannot contain null.", nameof(events)); } + + if (domainEvents[i].Version != firstEvent.Version + i) + { + throw new ArgumentException( + $"Versions of {nameof(events)} must be sequential.", + nameof(events)); + } + + if (domainEvents[i].SourceId != firstEvent.SourceId) + { + throw new ArgumentException( + $"All events must have the same source id.", + nameof(events)); + } } return Save(domainEvents, operationId, correlationId, contributor, cancellationToken); @@ -61,53 +77,24 @@ private async Task Save( CancellationToken cancellationToken) where T : class, IEventSourced { - if (domainEvents.Any() == false) - { - return; - } - - var envelopes = new List( + var envelopes = new List>( from domainEvent in domainEvents - select new Envelope(Guid.NewGuid(), domainEvent, operationId, correlationId, contributor)); - - await InsertPendingEvents(envelopes, cancellationToken).ConfigureAwait(false); - await InsertEventsAndCorrelation(envelopes, correlationId, cancellationToken).ConfigureAwait(false); - } + let messageId = Guid.NewGuid() + select new Envelope(messageId, domainEvent, operationId, correlationId, contributor)); - private Task InsertPendingEvents( - List envelopes, - CancellationToken cancellationToken) - where T : class, IEventSourced - { var batch = new TableBatchOperation(); - foreach (Envelope envelope in envelopes) + foreach (Envelope envelope in envelopes) { - batch.Insert(PendingEventTableEntity.FromEnvelope(envelope, _serializer)); + batch.Insert(PersistentEvent.Create(typeof(T), envelope, _serializer)); + batch.Insert(PendingEvent.Create(typeof(T), envelope, _serializer)); } - return _eventTable.ExecuteBatch(batch, cancellationToken); - } - - private async Task InsertEventsAndCorrelation( - List envelopes, - Guid? correlationId, - CancellationToken cancellationToken) - where T : class, IEventSourced - { - var batch = new TableBatchOperation(); - - var firstEvent = (IDomainEvent)envelopes.First().Message; - Guid sourceId = firstEvent.SourceId; - - foreach (Envelope envelope in envelopes) - { - batch.Insert(EventTableEntity.FromEnvelope(envelope, _serializer)); - } + Guid sourceId = domainEvents.First().SourceId; if (correlationId.HasValue) { - batch.Insert(CorrelationTableEntity.Create(typeof(T), sourceId, correlationId.Value)); + batch.Insert(Correlation.Create(typeof(T), sourceId, correlationId.Value)); } try @@ -116,9 +103,9 @@ private async Task InsertEventsAndCorrelation( } catch (StorageException exception) when (correlationId.HasValue) { - string filter = CorrelationTableEntity.GetFilter(typeof(T), sourceId, correlationId.Value); - TableQuery query = new TableQuery().Where(filter); - if (await _eventTable.Any(query, cancellationToken)) + string filter = Correlation.GetFilter(typeof(T), sourceId, correlationId.Value); + var query = new TableQuery { FilterString = filter }; + if (await _eventTable.Any(query, cancellationToken).ConfigureAwait(false)) { throw new DuplicateCorrelationException( typeof(T), @@ -152,18 +139,8 @@ private async Task> Load( CancellationToken cancellationToken) where T : class, IEventSourced { - string filter = CombineFilters( - GenerateFilterCondition( - nameof(ITableEntity.PartitionKey), - Equal, - EventTableEntity.GetPartitionKey(typeof(T), sourceId)), - And, - GenerateFilterCondition( - nameof(ITableEntity.RowKey), - GreaterThan, - EventTableEntity.GetRowKey(afterVersion))); - - TableQuery query = new TableQuery().Where(filter); + string filter = PersistentEvent.GetFilter(typeof(T), sourceId, afterVersion); + var query = new TableQuery { FilterString = filter }; IEnumerable events = await _eventTable .ExecuteQuery(query, cancellationToken) diff --git a/source/Khala.EventSourcing.Azure/EventSourcing/Azure/Correlation.cs b/source/Khala.EventSourcing.Azure/EventSourcing/Azure/Correlation.cs new file mode 100644 index 0000000..2ca44b0 --- /dev/null +++ b/source/Khala.EventSourcing.Azure/EventSourcing/Azure/Correlation.cs @@ -0,0 +1,81 @@ +namespace Khala.EventSourcing.Azure +{ + using System; + using Microsoft.WindowsAzure.Storage.Table; + + public class Correlation : AggregateEntity + { + public Guid CorrelationId { get; set; } + + public static string GetRowKey(Guid correlationId) + { + if (correlationId == Guid.Empty) + { + throw new ArgumentException("Value cannot be empty.", nameof(correlationId)); + } + + return $"Correlation-{correlationId:n}"; + } + + public static Correlation Create( + Type sourceType, + Guid sourceId, + Guid correlationId) + { + if (sourceType == null) + { + throw new ArgumentNullException(nameof(sourceType)); + } + + if (sourceId == Guid.Empty) + { + throw new ArgumentException("Value cannot be empty.", nameof(sourceId)); + } + + if (correlationId == Guid.Empty) + { + throw new ArgumentException("Value cannot be empty.", nameof(correlationId)); + } + + return new Correlation + { + PartitionKey = GetPartitionKey(sourceType, sourceId), + RowKey = GetRowKey(correlationId), + CorrelationId = correlationId, + }; + } + + public static string GetFilter( + Type sourceType, + Guid sourceId, + Guid correlationId) + { + if (sourceType == null) + { + throw new ArgumentNullException(nameof(sourceType)); + } + + if (sourceId == Guid.Empty) + { + throw new ArgumentException("Value cannot be empty.", nameof(sourceId)); + } + + if (correlationId == Guid.Empty) + { + throw new ArgumentException("Value cannot be empty.", nameof(correlationId)); + } + + string partitionCondition = TableQuery.GenerateFilterCondition( + nameof(PartitionKey), + QueryComparisons.Equal, + GetPartitionKey(sourceType, sourceId)); + + string rowCondition = TableQuery.GenerateFilterCondition( + nameof(RowKey), + QueryComparisons.Equal, + GetRowKey(correlationId)); + + return TableQuery.CombineFilters(partitionCondition, TableOperators.And, rowCondition); + } + } +} diff --git a/source/Khala.EventSourcing.Azure/EventSourcing/Azure/CorrelationEntity.cs b/source/Khala.EventSourcing.Azure/EventSourcing/Azure/CorrelationEntity.cs deleted file mode 100644 index 83eb6f2..0000000 --- a/source/Khala.EventSourcing.Azure/EventSourcing/Azure/CorrelationEntity.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace Khala.EventSourcing.Azure -{ - using System; - - public class CorrelationEntity : AggregateEntity - { - public Guid CorrelationId { get; set; } - - public static string GetRowKey(Guid correlationId) - { - if (correlationId == Guid.Empty) - { - throw new ArgumentException("Value cannot be empty.", nameof(correlationId)); - } - - return $"Correlation-{correlationId:n}"; - } - - public static CorrelationEntity Create( - Type aggregateType, - Guid aggregateId, - Guid correlationId) - { - if (aggregateType == null) - { - throw new ArgumentNullException(nameof(aggregateType)); - } - - if (aggregateId == Guid.Empty) - { - throw new ArgumentException("Value cannot be empty.", nameof(aggregateId)); - } - - if (correlationId == Guid.Empty) - { - throw new ArgumentException("Value cannot be empty.", nameof(correlationId)); - } - - return new CorrelationEntity - { - PartitionKey = GetPartitionKey(aggregateType, aggregateId), - RowKey = GetRowKey(correlationId), - CorrelationId = correlationId, - }; - } - } -} diff --git a/source/Khala.EventSourcing.Azure/EventSourcing/Azure/PendingEvent.cs b/source/Khala.EventSourcing.Azure/EventSourcing/Azure/PendingEvent.cs index 0f4c431..f72c317 100644 --- a/source/Khala.EventSourcing.Azure/EventSourcing/Azure/PendingEvent.cs +++ b/source/Khala.EventSourcing.Azure/EventSourcing/Azure/PendingEvent.cs @@ -2,19 +2,20 @@ { using System; using Khala.Messaging; + using Microsoft.WindowsAzure.Storage.Table; public class PendingEvent : EventEntity { public static string GetRowKey(int version) => $"Pending-{version:D10}"; public static PendingEvent Create( - Type aggregateType, + Type sourceType, Envelope envelope, IMessageSerializer serializer) { - if (aggregateType == null) + if (sourceType == null) { - throw new ArgumentNullException(nameof(aggregateType)); + throw new ArgumentNullException(nameof(sourceType)); } if (envelope == null) @@ -29,7 +30,7 @@ public static PendingEvent Create( return new PendingEvent { - PartitionKey = GetPartitionKey(aggregateType, envelope.Message.SourceId), + PartitionKey = GetPartitionKey(sourceType, envelope.Message.SourceId), RowKey = GetRowKey(envelope.Message.Version), MessageId = envelope.MessageId, EventJson = serializer.Serialize(envelope.Message), @@ -38,5 +39,36 @@ public static PendingEvent Create( Contributor = envelope.Contributor, }; } + + public static string GetFilter(Type sourceType, Guid sourceId, int afterVersion = default) + { + if (sourceType == null) + { + throw new ArgumentNullException(nameof(sourceType)); + } + + if (sourceId == Guid.Empty) + { + throw new ArgumentException("Value cannot be empty.", nameof(sourceId)); + } + + string partitionCondition = TableQuery.GenerateFilterCondition( + nameof(PartitionKey), + QueryComparisons.Equal, + GetPartitionKey(sourceType, sourceId)); + + string rowCondition = TableQuery.CombineFilters( + TableQuery.GenerateFilterCondition( + nameof(RowKey), + QueryComparisons.GreaterThan, + GetRowKey(afterVersion)), + TableOperators.And, + TableQuery.GenerateFilterCondition( + nameof(RowKey), + QueryComparisons.LessThanOrEqual, + GetRowKey(int.MaxValue))); + + return TableQuery.CombineFilters(partitionCondition, TableOperators.And, rowCondition); + } } } diff --git a/source/Khala.EventSourcing.Azure/EventSourcing/Azure/PersistentEvent.cs b/source/Khala.EventSourcing.Azure/EventSourcing/Azure/PersistentEvent.cs index 9b702a9..8e7d748 100644 --- a/source/Khala.EventSourcing.Azure/EventSourcing/Azure/PersistentEvent.cs +++ b/source/Khala.EventSourcing.Azure/EventSourcing/Azure/PersistentEvent.cs @@ -2,6 +2,7 @@ { using System; using Khala.Messaging; + using Microsoft.WindowsAzure.Storage.Table; public class PersistentEvent : EventEntity { @@ -14,13 +15,13 @@ public class PersistentEvent : EventEntity public static string GetRowKey(int version) => $"{version:D10}"; public static PersistentEvent Create( - Type aggregateType, + Type sourceType, Envelope envelope, IMessageSerializer serializer) { - if (aggregateType == null) + if (sourceType == null) { - throw new ArgumentNullException(nameof(aggregateType)); + throw new ArgumentNullException(nameof(sourceType)); } if (envelope == null) @@ -35,7 +36,7 @@ public static PersistentEvent Create( return new PersistentEvent { - PartitionKey = GetPartitionKey(aggregateType, envelope.Message.SourceId), + PartitionKey = GetPartitionKey(sourceType, envelope.Message.SourceId), RowKey = GetRowKey(envelope.Message.Version), Version = envelope.Message.Version, EventType = envelope.Message.GetType().FullName, @@ -47,5 +48,36 @@ public static PersistentEvent Create( Contributor = envelope.Contributor, }; } + + public static string GetFilter(Type sourceType, Guid sourceId, int afterVersion = default) + { + if (sourceType == null) + { + throw new ArgumentNullException(nameof(sourceType)); + } + + if (sourceId == Guid.Empty) + { + throw new ArgumentException("Value cannot be empty.", nameof(sourceId)); + } + + string partitionCondition = TableQuery.GenerateFilterCondition( + nameof(PartitionKey), + QueryComparisons.Equal, + GetPartitionKey(sourceType, sourceId)); + + string rowCondition = TableQuery.CombineFilters( + TableQuery.GenerateFilterCondition( + nameof(RowKey), + QueryComparisons.GreaterThan, + GetRowKey(afterVersion)), + TableOperators.And, + TableQuery.GenerateFilterCondition( + nameof(RowKey), + QueryComparisons.LessThanOrEqual, + GetRowKey(int.MaxValue))); + + return TableQuery.CombineFilters(partitionCondition, TableOperators.And, rowCondition); + } } } diff --git a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/AggregateEntity_specs.cs b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/AggregateEntity_specs.cs index 5f0575f..772246b 100644 --- a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/AggregateEntity_specs.cs +++ b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/AggregateEntity_specs.cs @@ -25,15 +25,15 @@ public void sut_inherits_TableEntity() } [TestMethod] - public void GetPartitionKey_returns_combination_of_aggregate_type_name_and_aggregate_id() + public void GetPartitionKey_returns_combination_of_source_type_name_and_source_id() { IFixture fixture = new Fixture().Customize(new AutoMoqCustomization()); - Type aggregateType = fixture.Create().GetType(); - Guid aggregateId = fixture.Create(); + Type sourceType = fixture.Create().GetType(); + Guid sourceId = fixture.Create(); - string actual = AggregateEntity.GetPartitionKey(aggregateType, aggregateId); + string actual = AggregateEntity.GetPartitionKey(sourceType, sourceId); - actual.Should().Be($"{aggregateType.Name}-{aggregateId:n}"); + actual.Should().Be($"{sourceType.Name}-{sourceId:n}"); } [TestMethod] diff --git a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/AzureEventStore_specs.cs b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/AzureEventStore_specs.cs new file mode 100644 index 0000000..121b806 --- /dev/null +++ b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/AzureEventStore_specs.cs @@ -0,0 +1,388 @@ +namespace Khala.EventSourcing.Azure +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Threading.Tasks; + using AutoFixture; + using AutoFixture.AutoMoq; + using FluentAssertions; + using Khala.FakeDomain; + using Khala.FakeDomain.Events; + using Khala.Messaging; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.WindowsAzure.Storage; + using Microsoft.WindowsAzure.Storage.RetryPolicies; + using Microsoft.WindowsAzure.Storage.Table; + + [TestClass] + public class AzureEventStore_specs + { + private static CloudStorageAccount s_storageAccount; + private static CloudTable s_eventTable; + private IMessageSerializer _serializer; + private AzureEventStore _sut; + + [ClassInitialize] + public static async Task ClassInitialize(TestContext context) + { + try + { + s_storageAccount = CloudStorageAccount.DevelopmentStorageAccount; + CloudTableClient tableClient = s_storageAccount.CreateCloudTableClient(); + s_eventTable = tableClient.GetTableReference("AzureEventStoreTestEventStore"); + await s_eventTable.DeleteIfExistsAsync( + new TableRequestOptions { RetryPolicy = new NoRetry() }, + operationContext: default); + await s_eventTable.CreateAsync(); + } + catch (StorageException exception) + when (exception.InnerException is WebException) + { + context.WriteLine($"{exception}"); + Assert.Inconclusive("Could not connect to Azure Storage Emulator. See the output for details. Refer to the following URL for more information: http://go.microsoft.com/fwlink/?LinkId=392237"); + } + } + + [TestInitialize] + public void TestInitialize() + { + _serializer = new JsonMessageSerializer(); + _sut = new AzureEventStore(s_eventTable, _serializer); + } + + public TestContext TestContext { get; set; } + + [TestMethod] + public void sut_implements_IAzureEventStore() + { + _sut.Should().BeAssignableTo(); + } + + [TestMethod] + public void SaveEvents_fails_if_events_contains_null() + { + IFixture fixture = new Fixture().Customize(new AutoMoqCustomization()); + IEnumerable events = Enumerable + .Repeat(fixture.Create(), 10) + .Concat(new[] { default(IDomainEvent) }) + .OrderBy(_ => fixture.Create()); + + Func action = () => _sut.SaveEvents(events); + + action.ShouldThrow().Where(x => x.ParamName == "events"); + } + + [TestMethod] + public async Task SaveEvents_inserts_persistent_event_entities_correctly() + { + // Arrange + var fixture = new Fixture(); + var user = new FakeUser(Guid.NewGuid(), fixture.Create()); + user.ChangeUsername(fixture.Create()); + IList domainEvents = user.FlushPendingEvents().ToList(); + var operationId = Guid.NewGuid(); + var correlationId = Guid.NewGuid(); + string contributor = fixture.Create(); + + // Act + await _sut.SaveEvents(domainEvents, operationId, correlationId, contributor); + + // Assert + string filter = PersistentEvent.GetFilter(typeof(FakeUser), user.Id); + var query = new TableQuery { FilterString = filter }; + IEnumerable actual = await s_eventTable.ExecuteQuerySegmentedAsync(query, default); + actual.ShouldAllBeEquivalentTo( + from domainEvent in domainEvents + let envelope = new Envelope( + Guid.NewGuid(), + domainEvent, + operationId, + correlationId, + contributor) + select PersistentEvent.Create(typeof(FakeUser), envelope, _serializer), + opts => opts + .Excluding(e => e.MessageId) + .Excluding(e => e.Timestamp) + .Excluding(e => e.ETag) + .WithStrictOrdering()); + } + + [TestMethod] + public async Task SaveEvents_inserts_pending_event_entities_correctly() + { + // Arrange + var fixture = new Fixture(); + var user = new FakeUser(Guid.NewGuid(), fixture.Create()); + user.ChangeUsername(fixture.Create()); + IList domainEvents = user.FlushPendingEvents().ToList(); + var operationId = Guid.NewGuid(); + var correlationId = Guid.NewGuid(); + string contributor = fixture.Create(); + + // Act + await _sut.SaveEvents(domainEvents, operationId, correlationId, contributor); + + // Assert + string filter = PendingEvent.GetFilter(typeof(FakeUser), user.Id); + var query = new TableQuery { FilterString = filter }; + IEnumerable actual = await s_eventTable.ExecuteQuerySegmentedAsync(query, default); + actual.ShouldAllBeEquivalentTo( + from domainEvent in domainEvents + let envelope = new Envelope( + Guid.NewGuid(), + domainEvent, + operationId, + correlationId, + contributor) + select PendingEvent.Create(typeof(FakeUser), envelope, _serializer), + opts => opts + .Excluding(e => e.MessageId) + .Excluding(e => e.Timestamp) + .Excluding(e => e.ETag) + .WithStrictOrdering()); + } + + [TestMethod] + public async Task SaveEvents_does_not_insert_persistent_event_entities_if_fails_to_insert_pending_event_entities() + { + // Arrange + var fixture = new Fixture(); + var user = new FakeUser(Guid.NewGuid(), fixture.Create()); + user.ChangeUsername(fixture.Create()); + IList domainEvents = user.FlushPendingEvents().ToList(); + + IDomainEvent conflicting = domainEvents[new Random().Next(domainEvents.Count)]; + var batch = new TableBatchOperation(); + batch.Insert(new TableEntity + { + PartitionKey = AggregateEntity.GetPartitionKey(typeof(FakeUser), user.Id), + RowKey = PendingEvent.GetRowKey(conflicting.Version), + }); + await s_eventTable.ExecuteBatchAsync(batch); + + // Act + Func action = () => _sut.SaveEvents(domainEvents); + + // Assert + action.ShouldThrow(); + string filter = PersistentEvent.GetFilter(typeof(FakeUser), user.Id); + var query = new TableQuery { FilterString = filter }; + IEnumerable actual = await s_eventTable.ExecuteQuerySegmentedAsync(query, default); + actual.Should().BeEmpty(); + } + + [TestMethod] + public async Task SaveEvents_does_not_insert_pending_event_entities_if_fails_to_insert_persistent_event_entities() + { + // Arrange + var fixture = new Fixture(); + var user = new FakeUser(Guid.NewGuid(), fixture.Create()); + user.ChangeUsername(fixture.Create()); + IList domainEvents = user.FlushPendingEvents().ToList(); + + IDomainEvent conflicting = domainEvents[new Random().Next(domainEvents.Count)]; + var batch = new TableBatchOperation(); + batch.Insert(new TableEntity + { + PartitionKey = AggregateEntity.GetPartitionKey(typeof(FakeUser), user.Id), + RowKey = PersistentEvent.GetRowKey(conflicting.Version), + }); + await s_eventTable.ExecuteBatchAsync(batch); + + // Act + Func action = () => _sut.SaveEvents(domainEvents); + + // Assert + action.ShouldThrow(); + string filter = PendingEvent.GetFilter(typeof(FakeUser), user.Id); + var query = new TableQuery { FilterString = filter }; + IEnumerable actual = await s_eventTable.ExecuteQuerySegmentedAsync(query, default); + actual.Should().BeEmpty(); + } + + [TestMethod] + public void SaveEvents_does_not_fail_even_if_events_empty() + { + DomainEvent[] events = new DomainEvent[] { }; + Func action = () => _sut.SaveEvents(events); + action.ShouldNotThrow(); + } + + [TestMethod] + public async Task SaveEvents_inserts_correlation_entity_correctly() + { + // Arrange + var fixture = new Fixture(); + var user = new FakeUser(Guid.NewGuid(), fixture.Create()); + IList domainEvents = user.FlushPendingEvents().ToList(); + var operationId = Guid.NewGuid(); + var correlationId = Guid.NewGuid(); + string contributor = fixture.Create(); + + // Act + await _sut.SaveEvents(domainEvents, operationId, correlationId, contributor); + + // Assert + string filter = Correlation.GetFilter(typeof(FakeUser), user.Id, correlationId); + var query = new TableQuery { FilterString = filter }; + IEnumerable actual = await s_eventTable.ExecuteQuerySegmentedAsync(query, default); + actual.Should().ContainSingle().Which.ShouldBeEquivalentTo( + new { CorrelationId = correlationId }, + opts => opts.ExcludingMissingMembers()); + } + + [TestMethod] + public async Task SaveEvents_throws_DuplicateCorrelationException_if_correlation_duplicate() + { + // Arrange + var fixture = new Fixture(); + var user = new FakeUser(Guid.NewGuid(), fixture.Create()); + user.ChangeUsername(fixture.Create()); + IList domainEvents = user.FlushPendingEvents().ToList(); + var correlationId = Guid.NewGuid(); + await _sut.SaveEvents(domainEvents.Take(1), correlationId: correlationId); + + // Act + Func action = () => _sut.SaveEvents(domainEvents.Skip(1), correlationId: correlationId); + + // Assert + action.ShouldThrow().Where( + x => + x.SourceType == typeof(FakeUser) && + x.SourceId == user.Id && + x.CorrelationId == correlationId && + x.InnerException is StorageException); + } + + [TestMethod] + public async Task SaveEvents_does_not_insert_persistent_event_entities_if_fails_to_insert_correlation_entity() + { + // Arrange + var fixture = new Fixture(); + var user = new FakeUser(Guid.NewGuid(), fixture.Create()); + user.ChangeUsername(fixture.Create()); + IList domainEvents = user.FlushPendingEvents().ToList(); + var correlationId = Guid.NewGuid(); + + var batch = new TableBatchOperation(); + batch.Insert(new TableEntity + { + PartitionKey = AggregateEntity.GetPartitionKey(typeof(FakeUser), user.Id), + RowKey = Correlation.GetRowKey(correlationId), + }); + await s_eventTable.ExecuteBatchAsync(batch); + + // Act + Func action = () => _sut.SaveEvents(domainEvents, correlationId: correlationId); + + // Assert + action.ShouldThrow(); + string filter = PersistentEvent.GetFilter(typeof(FakeUser), user.Id); + var query = new TableQuery { FilterString = filter }; + IEnumerable actual = await s_eventTable.ExecuteQuerySegmentedAsync(query, default); + actual.Should().BeEmpty(); + } + + [TestMethod] + public async Task SaveEvents_does_not_insert_pending_event_entities_if_fails_to_insert_correlation_entities() + { + // Arrange + var fixture = new Fixture(); + var user = new FakeUser(Guid.NewGuid(), fixture.Create()); + user.ChangeUsername(fixture.Create()); + IList domainEvents = user.FlushPendingEvents().ToList(); + var correlationId = Guid.NewGuid(); + + var batch = new TableBatchOperation(); + batch.Insert(new TableEntity + { + PartitionKey = AggregateEntity.GetPartitionKey(typeof(FakeUser), user.Id), + RowKey = Correlation.GetRowKey(correlationId), + }); + await s_eventTable.ExecuteBatchAsync(batch); + + // Act + Func action = () => _sut.SaveEvents(domainEvents, correlationId: correlationId); + + // Assert + action.ShouldThrow(); + string filter = PendingEvent.GetFilter(typeof(FakeUser), user.Id); + var query = new TableQuery { FilterString = filter }; + IEnumerable actual = await s_eventTable.ExecuteQuerySegmentedAsync(query, default); + actual.Should().BeEmpty(); + } + + [TestMethod] + public void SaveEvents_fails_if_versions_not_sequential() + { + // Arrange + DomainEvent[] events = new DomainEvent[] + { + new FakeUserCreated { Version = 1 }, + new FakeUsernameChanged { Version = 2 }, + new FakeUsernameChanged { Version = 4 }, + }; + + // Act + Func action = () => _sut.SaveEvents(events); + + // Assert + action.ShouldThrow().Where(x => x.ParamName == "events"); + } + + [TestMethod] + public void SaveEvents_fails_if_events_not_have_same_source_id() + { + // Arrange + DomainEvent[] events = new DomainEvent[] + { + new FakeUserCreated { Version = 1 }, + new FakeUsernameChanged { Version = 2 }, + }; + + // Act + Func action = () => _sut.SaveEvents(events); + + // Assert + action.ShouldThrow().Where(x => x.ParamName == "events"); + } + + [TestMethod] + public async Task LoadEvents_restores_domain_events_correctly() + { + // Arrange + var fixture = new Fixture(); + var user = new FakeUser(Guid.NewGuid(), fixture.Create()); + user.ChangeUsername(fixture.Create()); + IList domainEvents = user.FlushPendingEvents().ToList(); + await _sut.SaveEvents(domainEvents); + + // Act + IEnumerable actual = await _sut.LoadEvents(user.Id); + + // Assert + actual.Should().BeInAscendingOrder(e => e.Version); + actual.ShouldAllBeEquivalentTo(domainEvents); + } + + [TestMethod] + public async Task LoadEvents_correctly_restores_domain_events_after_specified_version() + { + // Arrange + var fixture = new Fixture(); + var user = new FakeUser(Guid.NewGuid(), fixture.Create()); + user.ChangeUsername(fixture.Create()); + IList domainEvents = user.FlushPendingEvents().ToList(); + await _sut.SaveEvents(domainEvents); + + // Act + IEnumerable actual = await _sut.LoadEvents(user.Id, 1); + + // Assert + actual.Should().BeInAscendingOrder(e => e.Version); + actual.ShouldAllBeEquivalentTo(domainEvents.Skip(1)); + } + } +} diff --git a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/CorrelationEntity_specs.cs b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/Correlation_specs.cs similarity index 53% rename from source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/CorrelationEntity_specs.cs rename to source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/Correlation_specs.cs index 591423e..aa17caa 100644 --- a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/CorrelationEntity_specs.cs +++ b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/Correlation_specs.cs @@ -8,37 +8,37 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] - public class CorrelationEntity_specs + public class Correlation_specs { [TestMethod] public void sut_inherits_AggregateEntity() { - typeof(CorrelationEntity).BaseType.Should().Be(typeof(AggregateEntity)); + typeof(Correlation).BaseType.Should().Be(typeof(AggregateEntity)); } [TestMethod] public void GetRowKey_returns_prefixed_correlation_id() { var correlationId = Guid.NewGuid(); - string actual = CorrelationEntity.GetRowKey(correlationId); + string actual = Correlation.GetRowKey(correlationId); actual.Should().Be($"Correlation-{correlationId:n}"); } [TestMethod] public void GetRowKey_has_guard_clause() { - MethodInfo mut = typeof(CorrelationEntity).GetMethod("GetRowKey"); + MethodInfo mut = typeof(Correlation).GetMethod("GetRowKey"); new GuardClauseAssertion(new Fixture()).Verify(mut); } [TestMethod] - public void Create_returns_CorrelationEntity_instance() + public void Create_returns_Correlation_instance() { - Type aggregateType = new Fixture().Create(); - var aggregateId = Guid.NewGuid(); + Type sourceType = new Fixture().Create(); + var sourceId = Guid.NewGuid(); var correlationId = Guid.NewGuid(); - var actual = CorrelationEntity.Create(aggregateType, aggregateId, correlationId); + var actual = Correlation.Create(sourceType, sourceId, correlationId); actual.Should().NotBeNull(); } @@ -46,42 +46,42 @@ public void Create_returns_CorrelationEntity_instance() [TestMethod] public void Create_has_guard_clauses() { - MethodInfo mut = typeof(CorrelationEntity).GetMethod("Create"); + MethodInfo mut = typeof(Correlation).GetMethod("Create"); new GuardClauseAssertion(new Fixture()).Verify(mut); } [TestMethod] public void Create_sets_PartitionKey_correctly() { - Type aggregateType = new Fixture().Create(); - var aggregateId = Guid.NewGuid(); + Type sourceType = new Fixture().Create(); + var sourceId = Guid.NewGuid(); var correlationId = Guid.NewGuid(); - var actual = CorrelationEntity.Create(aggregateType, aggregateId, correlationId); + var actual = Correlation.Create(sourceType, sourceId, correlationId); - actual.PartitionKey.Should().Be(AggregateEntity.GetPartitionKey(aggregateType, aggregateId)); + actual.PartitionKey.Should().Be(AggregateEntity.GetPartitionKey(sourceType, sourceId)); } [TestMethod] public void Create_sets_RowKey_correctly() { - Type aggregateType = new Fixture().Create(); - var aggregateId = Guid.NewGuid(); + Type sourceType = new Fixture().Create(); + var sourceId = Guid.NewGuid(); var correlationId = Guid.NewGuid(); - var actual = CorrelationEntity.Create(aggregateType, aggregateId, correlationId); + var actual = Correlation.Create(sourceType, sourceId, correlationId); - actual.RowKey.Should().Be(CorrelationEntity.GetRowKey(correlationId)); + actual.RowKey.Should().Be(Correlation.GetRowKey(correlationId)); } [TestMethod] public void Create_sets_CorrelationId_correctly() { - Type aggregateType = new Fixture().Create(); - var aggregateId = Guid.NewGuid(); + Type sourceType = new Fixture().Create(); + var sourceId = Guid.NewGuid(); var correlationId = Guid.NewGuid(); - var actual = CorrelationEntity.Create(aggregateType, aggregateId, correlationId); + var actual = Correlation.Create(sourceType, sourceId, correlationId); actual.CorrelationId.Should().Be(correlationId); } diff --git a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/PendingEvent_specs.cs b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/PendingEvent_specs.cs index 0b69d1c..64d8e62 100644 --- a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/PendingEvent_specs.cs +++ b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/PendingEvent_specs.cs @@ -61,17 +61,17 @@ public void Create_has_guard_clauses() public void Create_sets_PartitionKey_correctly() { IFixture fixture = new Fixture(); - Type aggregateType = fixture.Create(); + Type sourceType = fixture.Create(); SomeDomainEvent domainEvent = fixture.Create(); TestContext.WriteLine($"SourceId: {domainEvent.SourceId}"); fixture.Inject(domainEvent); var actual = PendingEvent.Create( - aggregateType, + sourceType, fixture.Create>(), new JsonMessageSerializer()); - actual.PartitionKey.Should().Be(AggregateEntity.GetPartitionKey(aggregateType, domainEvent.SourceId)); + actual.PartitionKey.Should().Be(AggregateEntity.GetPartitionKey(sourceType, domainEvent.SourceId)); } [TestMethod] diff --git a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/PersistentEvent_specs.cs b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/PersistentEvent_specs.cs index 8a6caa7..9b06a85 100644 --- a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/PersistentEvent_specs.cs +++ b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Azure/PersistentEvent_specs.cs @@ -61,17 +61,17 @@ public void Create_has_guard_clauses() public void Create_sets_PartitionKey_correctly() { IFixture fixture = new Fixture(); - Type aggregateType = fixture.Create(); + Type sourceType = fixture.Create(); SomeDomainEvent domainEvent = fixture.Create(); TestContext.WriteLine($"SourceId: {domainEvent.SourceId}"); fixture.Inject(domainEvent); var actual = PersistentEvent.Create( - aggregateType, + sourceType, fixture.Create>(), new JsonMessageSerializer()); - actual.PartitionKey.Should().Be(AggregateEntity.GetPartitionKey(aggregateType, domainEvent.SourceId)); + actual.PartitionKey.Should().Be(AggregateEntity.GetPartitionKey(sourceType, domainEvent.SourceId)); } [TestMethod] diff --git a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Sql/InternalExtensions.cs b/source/Khala.EventSourcing.Tests.Core/EventSourcing/InternalExtensions.cs similarity index 95% rename from source/Khala.EventSourcing.Tests.Core/EventSourcing/Sql/InternalExtensions.cs rename to source/Khala.EventSourcing.Tests.Core/EventSourcing/InternalExtensions.cs index da54102..12db820 100644 --- a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Sql/InternalExtensions.cs +++ b/source/Khala.EventSourcing.Tests.Core/EventSourcing/InternalExtensions.cs @@ -1,4 +1,4 @@ -namespace Khala.EventSourcing.Sql +namespace Khala.EventSourcing { using System; using System.Collections.Generic; diff --git a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Sql/SqlEventStore_specs.cs b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Sql/SqlEventStore_specs.cs index 504e588..34b4b3c 100644 --- a/source/Khala.EventSourcing.Tests.Core/EventSourcing/Sql/SqlEventStore_specs.cs +++ b/source/Khala.EventSourcing.Tests.Core/EventSourcing/Sql/SqlEventStore_specs.cs @@ -216,11 +216,7 @@ public void SaveEvents_fails_if_versions_not_sequential() new JsonMessageSerializer()); // Act - Func action = () => sut.SaveEvents( - events, - correlationId: default, - contributor: default, - cancellationToken: default); + Func action = () => sut.SaveEvents(events); // Assert action.ShouldThrow().Where(x => x.ParamName == "events"); diff --git a/source/Khala.EventSourcing.Tests/EventSourcing/Azure/AzureEventStore_specs.cs b/source/Khala.EventSourcing.Tests/EventSourcing/Azure/AzureEventStore_specs.cs deleted file mode 100644 index 20837b3..0000000 --- a/source/Khala.EventSourcing.Tests/EventSourcing/Azure/AzureEventStore_specs.cs +++ /dev/null @@ -1,307 +0,0 @@ -namespace Khala.EventSourcing.Azure -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using System.Threading.Tasks; - using FluentAssertions; - using Khala.FakeDomain; - using Khala.FakeDomain.Events; - using Khala.Messaging; - using Microsoft.VisualStudio.TestTools.UnitTesting; - using Microsoft.WindowsAzure.Storage; - using Microsoft.WindowsAzure.Storage.RetryPolicies; - using Microsoft.WindowsAzure.Storage.Table; - using Ploeh.AutoFixture; - using Ploeh.AutoFixture.AutoMoq; - - [TestClass] - public class AzureEventStore_specs - { - private static CloudStorageAccount s_storageAccount; - private static CloudTable s_eventTable; - private static bool s_storageEmulatorConnected; - private IFixture _fixture; - private IMessageSerializer _serializer; - private AzureEventStore _sut; - private Guid _userId; - - public TestContext TestContext { get; set; } - - [ClassInitialize] - public static void ClassInitialize(TestContext context) - { - try - { - s_storageAccount = CloudStorageAccount.DevelopmentStorageAccount; - CloudTableClient tableClient = s_storageAccount.CreateCloudTableClient(); - s_eventTable = tableClient.GetTableReference("AzureEventStoreTestEventStore"); - s_eventTable.DeleteIfExists(new TableRequestOptions { RetryPolicy = new NoRetry() }); - s_eventTable.Create(); - s_storageEmulatorConnected = true; - } - catch (StorageException exception) - when (exception.InnerException is WebException) - { - context.WriteLine("{0}", exception); - } - } - - [TestInitialize] - public void TestInitialize() - { - if (s_storageEmulatorConnected == false) - { - Assert.Inconclusive("Could not connect to Azure Storage Emulator. See the output for details. Refer to the following URL for more information: http://go.microsoft.com/fwlink/?LinkId=392237"); - } - - _fixture = new Fixture().Customize(new AutoMoqCustomization()); - _fixture.Inject(s_eventTable); - _serializer = new JsonMessageSerializer(); - _sut = new AzureEventStore(s_eventTable, _serializer); - _userId = Guid.NewGuid(); - } - - [TestMethod] - public void sut_implements_IAzureEventStore() - { - _sut.Should().BeAssignableTo(); - } - - [TestMethod] - public void SaveEvents_fails_if_events_contains_null() - { - IFixture fixture = new Fixture().Customize(new AutoMoqCustomization()); - var random = new Random(); - IEnumerable events = Enumerable - .Repeat(fixture.Create(), 10) - .Concat(new[] { default(IDomainEvent) }) - .OrderBy(_ => random.Next()); - - Func action = () => _sut.SaveEvents(events); - - action.ShouldThrow().Where(x => x.ParamName == "events"); - } - - [TestMethod] - public async Task SaveEvents_inserts_pending_event_entities_correctly() - { - // Arrange - FakeUserCreated created = _fixture.Create(); - FakeUsernameChanged usernameChanged = _fixture.Create(); - DomainEvent[] events = new DomainEvent[] { created, usernameChanged }; - var operationId = Guid.NewGuid(); - var correlationId = Guid.NewGuid(); - string contributor = Guid.NewGuid().ToString(); - RaiseEvents(_userId, events); - - // Act - await _sut.SaveEvents(events, operationId, correlationId, contributor); - - // Assert - string partitionKey = PendingEventTableEntity.GetPartitionKey(typeof(FakeUser), _userId); - TableQuery query = new TableQuery().Where($"PartitionKey eq '{partitionKey}'"); - IEnumerable pendingEvents = s_eventTable.ExecuteQuery(query); - pendingEvents.Should().HaveCount(events.Length); - foreach (var t in pendingEvents.Zip(events, (pending, source) => - new { Pending = pending, Source = source })) - { - var actual = new - { - t.Pending.RowKey, - t.Pending.PersistentPartition, - t.Pending.Version, - t.Pending.OperationId, - t.Pending.CorrelationId, - t.Pending.Contributor, - Message = _serializer.Deserialize(t.Pending.EventJson), - }; - actual.ShouldBeEquivalentTo(new - { - RowKey = PendingEventTableEntity.GetRowKey(t.Source.Version), - PersistentPartition = EventTableEntity.GetPartitionKey(typeof(FakeUser), _userId), - t.Source.Version, - OperationId = operationId, - CorrelationId = correlationId, - Contributor = contributor, - Message = t.Source, - }, - opts => opts.RespectingRuntimeTypes()); - } - } - - [TestMethod] - public async Task SaveEvents_inserts_event_entities_correctly() - { - // Arrange - FakeUserCreated created = _fixture.Create(); - FakeUsernameChanged usernameChanged = _fixture.Create(); - DomainEvent[] events = new DomainEvent[] { created, usernameChanged }; - var operationId = Guid.NewGuid(); - var correlationId = Guid.NewGuid(); - string contributor = Guid.NewGuid().ToString(); - RaiseEvents(_userId, events); - - // Act - await _sut.SaveEvents(events, operationId, correlationId, contributor); - - // Assert - string partitionKey = EventTableEntity.GetPartitionKey(typeof(FakeUser), _userId); - string filter = $"(PartitionKey eq '{partitionKey}') and (RowKey lt 'Correlation')"; - TableQuery query = new TableQuery().Where(filter); - IEnumerable persistentEvents = s_eventTable.ExecuteQuery(query); - persistentEvents.Should().HaveCount(events.Length); - foreach (var t in persistentEvents.Zip(events, (persistent, source) - => new { Persistent = persistent, Source = source })) - { - var actual = new - { - t.Persistent.RowKey, - t.Persistent.Version, - t.Persistent.EventType, - t.Persistent.OperationId, - t.Persistent.CorrelationId, - t.Persistent.Contributor, - Event = (DomainEvent)_serializer.Deserialize(t.Persistent.EventJson), - t.Persistent.RaisedAt, - }; - actual.ShouldBeEquivalentTo(new - { - RowKey = EventTableEntity.GetRowKey(t.Source.Version), - t.Source.Version, - EventType = t.Source.GetType().FullName, - OperationId = operationId, - CorrelationId = correlationId, - Contributor = contributor, - Event = t.Source, - t.Source.RaisedAt, - }); - } - } - - [TestMethod] - public async Task SaveEvents_does_not_insert_event_entities_if_fails_to_insert_pending_event_entities() - { - // Arrange - DomainEvent domainEvent = _fixture.Create(); - RaiseEvents(_userId, new[] { domainEvent }); - await s_eventTable.ExecuteAsync(TableOperation.Insert(new TableEntity - { - PartitionKey = PendingEventTableEntity.GetPartitionKey(typeof(FakeUser), _userId), - RowKey = PendingEventTableEntity.GetRowKey(domainEvent.Version), - })); - - // Act - Func action = () => _sut.SaveEvents(new[] { domainEvent }); - - // Assert - action.ShouldThrow(); - IEnumerable actual = await _sut.LoadEvents(_userId); - actual.Should().BeEmpty(); - } - - [TestMethod] - public void SaveEvents_does_not_fail_even_if_events_empty() - { - DomainEvent[] events = new DomainEvent[] { }; - Func action = () => _sut.SaveEvents(events); - action.ShouldNotThrow(); - } - - [TestMethod] - public async Task SaveEvents_inserts_CorrelationTableEntity_correctly() - { - // Arrange - FakeUserCreated created = _fixture.Create(); - DomainEvent[] events = new DomainEvent[] { created }; - var correlationId = Guid.NewGuid(); - RaiseEvents(_userId, events); - - // Act - DateTimeOffset now = DateTimeOffset.Now; - await _sut.SaveEvents(events, correlationId: correlationId); - - // Assert - string partitionKey = CorrelationTableEntity.GetPartitionKey(typeof(FakeUser), _userId); - string rowKey = CorrelationTableEntity.GetRowKey(correlationId); - TableQuery query = new TableQuery().Where($"PartitionKey eq '{partitionKey}' and RowKey eq '{rowKey}'"); - IEnumerable correlations = s_eventTable.ExecuteQuery(query); - correlations.Should() - .ContainSingle(e => e.CorrelationId == correlationId) - .Which.HandledAt.ToLocalTime().Should().BeCloseTo(now, 1000); - } - - [TestMethod] - public async Task SaveEvents_throws_DuplicateCorrelationException_if_correlation_duplicate() - { - FakeUserCreated created = _fixture.Create(); - FakeUsernameChanged usernameChanged = _fixture.Create(); - RaiseEvents(_userId, created, usernameChanged); - var correlationId = Guid.NewGuid(); - await _sut.SaveEvents(new[] { created }, correlationId: correlationId); - - Func action = () => - _sut.SaveEvents(new[] { usernameChanged }, correlationId: correlationId); - - action.ShouldThrow().Where( - x => - x.SourceType == typeof(FakeUser) && - x.SourceId == _userId && - x.CorrelationId == correlationId && - x.InnerException is StorageException); - } - - [TestMethod] - public async Task LoadEvents_restores_domain_events_correctly() - { - // Arrange - FakeUserCreated created = _fixture.Create(); - FakeUsernameChanged usernameChanged = _fixture.Create(); - DomainEvent[] events = new DomainEvent[] { created, usernameChanged }; - RaiseEvents(_userId, events); - await _sut.SaveEvents(events); - - // Act - IEnumerable actual = await _sut.LoadEvents(_userId); - - // Assert - actual.Should().BeInAscendingOrder(e => e.Version); - actual.ShouldAllBeEquivalentTo(events); - } - - [TestMethod] - public async Task LoadEvents_correctly_restores_domain_events_after_specified_version() - { - // Arrange - FakeUserCreated created = _fixture.Create(); - FakeUsernameChanged usernameChanged = _fixture.Create(); - DomainEvent[] events = new DomainEvent[] { created, usernameChanged }; - RaiseEvents(_userId, events); - await _sut.SaveEvents(events); - - // Act - IEnumerable actual = await _sut.LoadEvents(_userId, 1); - - // Assert - actual.Should().BeInAscendingOrder(e => e.Version); - actual.ShouldAllBeEquivalentTo(events.Skip(1)); - } - - private static void RaiseEvents(Guid sourceId, params DomainEvent[] events) - { - RaiseEvents(sourceId, 0, events); - } - - private static void RaiseEvents( - Guid sourceId, int versionOffset, params DomainEvent[] events) - { - for (int i = 0; i < events.Length; i++) - { - events[i].SourceId = sourceId; - events[i].Version = versionOffset + i + 1; - events[i].RaisedAt = DateTimeOffset.Now; - } - } - } -} diff --git a/source/Khala.EventSourcing.Tests/Khala.EventSourcing.Tests.csproj b/source/Khala.EventSourcing.Tests/Khala.EventSourcing.Tests.csproj index 64077c2..46e4d3e 100644 --- a/source/Khala.EventSourcing.Tests/Khala.EventSourcing.Tests.csproj +++ b/source/Khala.EventSourcing.Tests/Khala.EventSourcing.Tests.csproj @@ -149,7 +149,6 @@ -