From 6af830838b66184bdb29b0f71e9d53029eaf675b Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 23 Aug 2016 21:11:00 +0200 Subject: [PATCH 01/17] Version is now 0.35 --- RELEASE_NOTES.md | 6 +++++- appveyor.yml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 566ecd10f..376a1fae6 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,8 @@ -### New in 0.34 (not released yet) +### New in 0.35 (not released yet) + +* _Nothing yet_ + +### New in 0.34.2221 (released 2016-08-23) * **New core feature:** EventFlow now support sagas, also known as process managers. The use of sagas is opt-in. Currently EventFlow only supports sagas diff --git a/appveyor.yml b/appveyor.yml index e98fcdcec..c049c9875 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ init: - git config --global core.autocrlf input -version: 0.34.{build} +version: 0.35.{build} skip_tags: true From a73711905ed09ee3386b29658f842effaecce032 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 27 Aug 2016 23:45:51 +0200 Subject: [PATCH 02/17] Created a complete example --- Source/EventFlow.Tests/EventFlow.Tests.csproj | 1 + .../IntegrationTests/CompleteExampleTests.cs | 147 ++++++++++++++++++ .../EventFlow/Queries/ReadModelByIdQuery.cs | 6 + 3 files changed, 154 insertions(+) create mode 100644 Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index 31e515065..19c8f5dbd 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -58,6 +58,7 @@ + diff --git a/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs b/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs new file mode 100644 index 000000000..979b64edf --- /dev/null +++ b/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs @@ -0,0 +1,147 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2016 Rasmus Mikkelsen +// Copyright (c) 2015-2016 eBay Software Foundation +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Commands; +using EventFlow.Core; +using EventFlow.Exceptions; +using EventFlow.Extensions; +using EventFlow.Queries; +using EventFlow.ReadStores; +using FluentAssertions; +using NUnit.Framework; + +namespace EventFlow.Tests.IntegrationTests +{ + public class CompleteExampleTests + { + [Test] + public async Task Example() + { + using (var resolver = EventFlowOptions.New + .AddEvents(typeof(SimpleEvent)) + .AddCommands(typeof(SimpleCommand)) + .AddCommandHandlers(typeof(SimpleCommandHandler)) + .UseInMemoryReadStoreFor() + .CreateResolver()) + { + var simpleId = SimpleId.New; + var commandBus = resolver.Resolve(); + var queryProcessor = resolver.Resolve(); + + await commandBus.PublishAsync( + new SimpleCommand(simpleId, 42), CancellationToken.None) + .ConfigureAwait(false); + + var simpleReadModel = await queryProcessor.ProcessAsync( + new ReadModelByIdQuery(simpleId), CancellationToken.None) + .ConfigureAwait(false); + + simpleReadModel.MagicNumber.Should().Be(42); + } + } + + // Represents the aggregate ID + public class SimpleId : Identity + { + public SimpleId(string value) : base(value) { } + } + + // The aggregate root + public class SimpleAggrenate : AggregateRoot, + IEmit + { + private int? _magicNumber; + + public SimpleAggrenate(SimpleId id) : base(id) { } + + public void SetMagicNumer(int magicNumber) + { + if (_magicNumber.HasValue) + throw DomainError.With("Magic number already set"); + + Emit(new SimpleEvent(magicNumber)); + } + + public void Apply(SimpleEvent aggregateEvent) + { + _magicNumber = aggregateEvent.MagicNumber; + } + } + + // A basic event containing some information + public class SimpleEvent : AggregateEvent + { + public SimpleEvent(int magicNumber) + { + MagicNumber = magicNumber; + } + + public int MagicNumber { get; } + } + + // Command for update magic number + public class SimpleCommand : Command + { + public SimpleCommand( + SimpleId aggregateId, + int magicNumber) + : base(aggregateId) + { + MagicNumber = magicNumber; + } + + public int MagicNumber { get; } + } + + // Command handler for our command + public class SimpleCommandHandler : CommandHandler + { + public override Task ExecuteAsync( + SimpleAggrenate aggregate, + SimpleCommand command, + CancellationToken cancellationToken) + { + aggregate.SetMagicNumer(command.MagicNumber); + return Task.FromResult(0); + } + } + + // Read model for our aggregate + public class SimpleReadModel : IReadModel, + IAmReadModelFor + { + public int MagicNumber { get; private set; } + + public void Apply( + IReadModelContext context, + IDomainEvent domainEvent) + { + MagicNumber = domainEvent.AggregateEvent.MagicNumber; + } + } + } +} \ No newline at end of file diff --git a/Source/EventFlow/Queries/ReadModelByIdQuery.cs b/Source/EventFlow/Queries/ReadModelByIdQuery.cs index 7bf262da4..82012c16d 100644 --- a/Source/EventFlow/Queries/ReadModelByIdQuery.cs +++ b/Source/EventFlow/Queries/ReadModelByIdQuery.cs @@ -24,6 +24,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using EventFlow.Core; using EventFlow.ReadStores; namespace EventFlow.Queries @@ -33,6 +34,11 @@ public class ReadModelByIdQuery : IQuery { public string Id { get; } + public ReadModelByIdQuery(IIdentity identity) + : this(identity.Value) + { + } + public ReadModelByIdQuery(string id) { if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); From fa155435c32cf1e6daf5bc0c70136a87469d7d79 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 27 Aug 2016 23:59:17 +0200 Subject: [PATCH 03/17] Update the README with the complete example --- README.md | 144 +++++++++++++++--- .../IntegrationTests/CompleteExampleTests.cs | 19 ++- 2 files changed, 136 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 6934816a0..f0fc6e369 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,9 @@ the [dos and don'ts](./Documentation/DoesAndDonts.md) and the ### Examples -* **[Simple](#simple-example):** Shows the key concepts of EventFlow in a few - lines of code +* **[Complete](#complete-example):** Shows a complete example on how to use + EventFlow with in-memory event store and read models in a relatively few lines + of code * **Shipping:** To get a more complete example of how EventFlow _could_ be used, have a look at the shipping example found here in the code base. The example is based on the shipping example from the book "Domain-Driven Design - @@ -113,37 +114,132 @@ to the documentation. EventFlow can be swapped with a custom implementation through the embedded IoC container. -## Simple example -Here's an example on how to use the in-memory event store (default) -and a in-memory read model store. +## Complete example +Here's a complete example on how to use the default in-memory event store +along with an in-memory read model. ```csharp -using (var resolver = EventFlowOptions.New - .AddEvents(typeof (TestAggregate).Assembly) - .AddCommandHandlers(typeof (TestAggregate).Assembly) - .UseInMemoryReadStoreFor() +[Test] +public async Task Example() +{ + // We wire up EventFlow with all of our classes. Instead of adding events, + // commands, etc. explicitly, we could have used the the simpler + // AddDefaults(Assembly) instead. See each of the referenced classes below + using (var resolver = EventFlowOptions.New + .AddEvents(typeof(SimpleEvent)) + .AddCommands(typeof(SimpleCommand)) + .AddCommandHandlers(typeof(SimpleCommandHandler)) + .UseInMemoryReadStoreFor() .CreateResolver()) + { + // Create a new identity for our aggregate root + var simpleId = SimpleId.New; + + // Resolve the command bus and use it to publish a command + var commandBus = resolver.Resolve(); + await commandBus.PublishAsync( + new SimpleCommand(simpleId, 42), CancellationToken.None) + .ConfigureAwait(false); + + // Resolve the query handler and use the built-in query for fetching + // read models by identity to get our read model representing the + // state of our aggregate root + var queryProcessor = resolver.Resolve(); + var simpleReadModel = await queryProcessor.ProcessAsync( + new ReadModelByIdQuery(simpleId), CancellationToken.None) + .ConfigureAwait(false); + + // Verify that the read model has the expected magic number + simpleReadModel.MagicNumber.Should().Be(42); + } +} + +// Represents the aggregate identity (ID) +public class SimpleId : Identity { - var commandBus = resolver.Resolve(); - var eventStore = resolver.Resolve(); - var readModelStore = resolver.Resolve>(); - var id = TestId.New; + public SimpleId(string value) : base(value) { } +} - // Publish a command - await commandBus.PublishAsync(new PingCommand(id)); +// The aggregate root +public class SimpleAggrenate : AggregateRoot, + IEmit +{ + private int? _magicNumber; + + public SimpleAggrenate(SimpleId id) : base(id) { } + + // Method invoked by our command + public void SetMagicNumer(int magicNumber) + { + if (_magicNumber.HasValue) + throw DomainError.With("Magic number already set"); + + Emit(new SimpleEvent(magicNumber)); + } + + // We apply the event as part of the event sourcing system. EventFlow + // provides several different methods for doing this, e.g. state objects, + // the Apply method is merely the simplest + public void Apply(SimpleEvent aggregateEvent) + { + _magicNumber = aggregateEvent.MagicNumber; + } +} - // Load aggregate - var testAggregate = await eventStore.LoadAggregateAsync(id); +// A basic event containing some information +public class SimpleEvent : AggregateEvent +{ + public SimpleEvent(int magicNumber) + { + MagicNumber = magicNumber; + } - // Get read model from in-memory read store - var testReadModel = await readModelStore.GetAsync(id); + public int MagicNumber { get; } } -``` -Note: `.ConfigureAwait(false)` and use of `CancellationToken` is omitted in -the above example to ease reading. +// Command for update magic number +public class SimpleCommand : Command +{ + public SimpleCommand( + SimpleId aggregateId, + int magicNumber) + : base(aggregateId) + { + MagicNumber = magicNumber; + } + + public int MagicNumber { get; } +} + +// Command handler for our command +public class SimpleCommandHandler : CommandHandler +{ + public override Task ExecuteAsync( + SimpleAggrenate aggregate, + SimpleCommand command, + CancellationToken cancellationToken) + { + aggregate.SetMagicNumer(command.MagicNumber); + return Task.FromResult(0); + } +} + +// Read model for our aggregate +public class SimpleReadModel : IReadModel, + IAmReadModelFor +{ + public int MagicNumber { get; private set; } + + public void Apply( + IReadModelContext context, + IDomainEvent domainEvent) + { + MagicNumber = domainEvent.AggregateEvent.MagicNumber; + } +}``` + +**Note:** The above example is part of the EventFlow test suite, so checkout +the code and give it a go. ## State of EventFlow diff --git a/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs b/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs index 979b64edf..7b253918c 100644 --- a/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs @@ -41,6 +41,9 @@ public class CompleteExampleTests [Test] public async Task Example() { + // We wire up EventFlow with all of our classes. Instead of adding events, + // commands, etc. explicitly, we could have used the the simpler + // AddDefaults(Assembly) instead. See each of the referenced classes below using (var resolver = EventFlowOptions.New .AddEvents(typeof(SimpleEvent)) .AddCommands(typeof(SimpleCommand)) @@ -48,23 +51,29 @@ public async Task Example() .UseInMemoryReadStoreFor() .CreateResolver()) { + // Create a new identity for our aggregate root var simpleId = SimpleId.New; - var commandBus = resolver.Resolve(); - var queryProcessor = resolver.Resolve(); + // Resolve the command bus and use it to publish a command + var commandBus = resolver.Resolve(); await commandBus.PublishAsync( new SimpleCommand(simpleId, 42), CancellationToken.None) .ConfigureAwait(false); + // Resolve the query handler and use the built-in query for fetching + // read models by identity to get our read model representing the + // state of our aggregate root + var queryProcessor = resolver.Resolve(); var simpleReadModel = await queryProcessor.ProcessAsync( new ReadModelByIdQuery(simpleId), CancellationToken.None) .ConfigureAwait(false); + // Verify that the read model has the expected magic number simpleReadModel.MagicNumber.Should().Be(42); } } - // Represents the aggregate ID + // Represents the aggregate identity (ID) public class SimpleId : Identity { public SimpleId(string value) : base(value) { } @@ -78,6 +87,7 @@ public class SimpleAggrenate : AggregateRoot, public SimpleAggrenate(SimpleId id) : base(id) { } + // Method invoked by our command public void SetMagicNumer(int magicNumber) { if (_magicNumber.HasValue) @@ -86,6 +96,9 @@ public void SetMagicNumer(int magicNumber) Emit(new SimpleEvent(magicNumber)); } + // We apply the event as part of the event sourcing system. EventFlow + // provides several different methods for doing this, e.g. state objects, + // the Apply method is merely the simplest public void Apply(SimpleEvent aggregateEvent) { _magicNumber = aggregateEvent.MagicNumber; From dc2ff11ec2b6f6bbd2d7327a444cfa7653f585d1 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sun, 28 Aug 2016 00:00:48 +0200 Subject: [PATCH 04/17] Fix broken markdown --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f0fc6e369..4abfb030e 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,8 @@ public class SimpleReadModel : IReadModel, { MagicNumber = domainEvent.AggregateEvent.MagicNumber; } -}``` +} +``` **Note:** The above example is part of the EventFlow test suite, so checkout the code and give it a go. From 230ceb152dc49bd6fac1f2a6484f4cdcabe3ae0a Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sun, 28 Aug 2016 00:02:40 +0200 Subject: [PATCH 05/17] Make the markdown code a bit easier to read --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 4abfb030e..e75de80ec 100644 --- a/README.md +++ b/README.md @@ -153,13 +153,17 @@ public async Task Example() simpleReadModel.MagicNumber.Should().Be(42); } } +``` +```csharp // Represents the aggregate identity (ID) public class SimpleId : Identity { public SimpleId(string value) : base(value) { } } +``` +```csharp // The aggregate root public class SimpleAggrenate : AggregateRoot, IEmit @@ -185,7 +189,9 @@ public class SimpleAggrenate : AggregateRoot, _magicNumber = aggregateEvent.MagicNumber; } } +``` +```csharp // A basic event containing some information public class SimpleEvent : AggregateEvent { @@ -196,7 +202,9 @@ public class SimpleEvent : AggregateEvent public int MagicNumber { get; } } +``` +```csharp // Command for update magic number public class SimpleCommand : Command { @@ -210,7 +218,9 @@ public class SimpleCommand : Command public int MagicNumber { get; } } +``` +```csharp // Command handler for our command public class SimpleCommandHandler : CommandHandler { @@ -223,7 +233,9 @@ public class SimpleCommandHandler : CommandHandler From 28f14c8a7e8dcbdd6342dc8315864b4dd41b2394 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sun, 28 Aug 2016 00:16:45 +0200 Subject: [PATCH 06/17] Cleanup --- README.md | 81 +++++++++++-------- .../IntegrationTests/CompleteExampleTests.cs | 52 ++++++------ 2 files changed, 73 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index e75de80ec..da43cb233 100644 --- a/README.md +++ b/README.md @@ -118,59 +118,63 @@ to the documentation. Here's a complete example on how to use the default in-memory event store along with an in-memory read model. +The example consists of the following classes, each shown below + +- `ExampleAggregate`: The aggregate root +- `ExampleId`: Value object representing the identity of the aggregate root +- `ExampleEvent`: Event emitted by the aggregate root +- `ExampleCommand`: Value object defining a command that can be published to the + aggregate root +- `ExampleCommandHandler`: Command handler which EventFlow resolves using its IoC + container and defines how the command specific is applied to the aggregate root +- `ExampleReadModel`: In-memory read model providing easy access to the current + state + ```csharp [Test] public async Task Example() { // We wire up EventFlow with all of our classes. Instead of adding events, // commands, etc. explicitly, we could have used the the simpler - // AddDefaults(Assembly) instead. See each of the referenced classes below + // AddDefaults(Assembly) instead. using (var resolver = EventFlowOptions.New - .AddEvents(typeof(SimpleEvent)) - .AddCommands(typeof(SimpleCommand)) - .AddCommandHandlers(typeof(SimpleCommandHandler)) - .UseInMemoryReadStoreFor() + .AddEvents(typeof(ExampleEvent)) + .AddCommands(typeof(ExampleCommand)) + .AddCommandHandlers(typeof(ExampleCommandHandler)) + .UseInMemoryReadStoreFor() .CreateResolver()) { // Create a new identity for our aggregate root - var simpleId = SimpleId.New; + var exampleId = ExampleId.New; // Resolve the command bus and use it to publish a command var commandBus = resolver.Resolve(); await commandBus.PublishAsync( - new SimpleCommand(simpleId, 42), CancellationToken.None) + new ExampleCommand(exampleId, 42), CancellationToken.None) .ConfigureAwait(false); // Resolve the query handler and use the built-in query for fetching // read models by identity to get our read model representing the // state of our aggregate root var queryProcessor = resolver.Resolve(); - var simpleReadModel = await queryProcessor.ProcessAsync( - new ReadModelByIdQuery(simpleId), CancellationToken.None) + var exampleReadModel = await queryProcessor.ProcessAsync( + new ReadModelByIdQuery(exampleId), CancellationToken.None) .ConfigureAwait(false); // Verify that the read model has the expected magic number - simpleReadModel.MagicNumber.Should().Be(42); + exampleReadModel.MagicNumber.Should().Be(42); } } ``` -```csharp -// Represents the aggregate identity (ID) -public class SimpleId : Identity -{ - public SimpleId(string value) : base(value) { } -} -``` - ```csharp // The aggregate root -public class SimpleAggrenate : AggregateRoot, - IEmit +public class ExampleAggrenate : AggregateRoot, + IEmit { private int? _magicNumber; - public SimpleAggrenate(SimpleId id) : base(id) { } + public ExampleAggrenate(ExampleId id) : base(id) { } // Method invoked by our command public void SetMagicNumer(int magicNumber) @@ -178,24 +182,32 @@ public class SimpleAggrenate : AggregateRoot, if (_magicNumber.HasValue) throw DomainError.With("Magic number already set"); - Emit(new SimpleEvent(magicNumber)); + Emit(new ExampleEvent(magicNumber)); } // We apply the event as part of the event sourcing system. EventFlow // provides several different methods for doing this, e.g. state objects, // the Apply method is merely the simplest - public void Apply(SimpleEvent aggregateEvent) + public void Apply(ExampleEvent aggregateEvent) { _magicNumber = aggregateEvent.MagicNumber; } } ``` +```csharp +// Represents the aggregate identity (ID) +public class ExampleId : Identity +{ + public ExampleId(string value) : base(value) { } +} +``` + ```csharp // A basic event containing some information -public class SimpleEvent : AggregateEvent +public class ExampleEvent : AggregateEvent { - public SimpleEvent(int magicNumber) + public ExampleEvent(int magicNumber) { MagicNumber = magicNumber; } @@ -206,10 +218,10 @@ public class SimpleEvent : AggregateEvent ```csharp // Command for update magic number -public class SimpleCommand : Command +public class ExampleCommand : Command { - public SimpleCommand( - SimpleId aggregateId, + public ExampleCommand( + ExampleId aggregateId, int magicNumber) : base(aggregateId) { @@ -222,11 +234,12 @@ public class SimpleCommand : Command ```csharp // Command handler for our command -public class SimpleCommandHandler : CommandHandler +public class ExampleCommandHandler + : CommandHandler { public override Task ExecuteAsync( - SimpleAggrenate aggregate, - SimpleCommand command, + ExampleAggrenate aggregate, + ExampleCommand command, CancellationToken cancellationToken) { aggregate.SetMagicNumer(command.MagicNumber); @@ -237,14 +250,14 @@ public class SimpleCommandHandler : CommandHandler +public class ExampleReadModel : IReadModel, + IAmReadModelFor { public int MagicNumber { get; private set; } public void Apply( IReadModelContext context, - IDomainEvent domainEvent) + IDomainEvent domainEvent) { MagicNumber = domainEvent.AggregateEvent.MagicNumber; } diff --git a/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs b/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs index 7b253918c..31b067e89 100644 --- a/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs @@ -43,21 +43,21 @@ public async Task Example() { // We wire up EventFlow with all of our classes. Instead of adding events, // commands, etc. explicitly, we could have used the the simpler - // AddDefaults(Assembly) instead. See each of the referenced classes below + // AddDefaults(Assembly) instead. using (var resolver = EventFlowOptions.New - .AddEvents(typeof(SimpleEvent)) - .AddCommands(typeof(SimpleCommand)) - .AddCommandHandlers(typeof(SimpleCommandHandler)) - .UseInMemoryReadStoreFor() + .AddEvents(typeof(ExampleEvent)) + .AddCommands(typeof(ExampleCommand)) + .AddCommandHandlers(typeof(ExampleCommandHandler)) + .UseInMemoryReadStoreFor() .CreateResolver()) { // Create a new identity for our aggregate root - var simpleId = SimpleId.New; + var simpleId = ExampleId.New; // Resolve the command bus and use it to publish a command var commandBus = resolver.Resolve(); await commandBus.PublishAsync( - new SimpleCommand(simpleId, 42), CancellationToken.None) + new ExampleCommand(simpleId, 42), CancellationToken.None) .ConfigureAwait(false); // Resolve the query handler and use the built-in query for fetching @@ -65,7 +65,7 @@ await commandBus.PublishAsync( // state of our aggregate root var queryProcessor = resolver.Resolve(); var simpleReadModel = await queryProcessor.ProcessAsync( - new ReadModelByIdQuery(simpleId), CancellationToken.None) + new ReadModelByIdQuery(simpleId), CancellationToken.None) .ConfigureAwait(false); // Verify that the read model has the expected magic number @@ -74,18 +74,18 @@ await commandBus.PublishAsync( } // Represents the aggregate identity (ID) - public class SimpleId : Identity + public class ExampleId : Identity { - public SimpleId(string value) : base(value) { } + public ExampleId(string value) : base(value) { } } // The aggregate root - public class SimpleAggrenate : AggregateRoot, - IEmit + public class ExampleAggrenate : AggregateRoot, + IEmit { private int? _magicNumber; - public SimpleAggrenate(SimpleId id) : base(id) { } + public ExampleAggrenate(ExampleId id) : base(id) { } // Method invoked by our command public void SetMagicNumer(int magicNumber) @@ -93,22 +93,22 @@ public void SetMagicNumer(int magicNumber) if (_magicNumber.HasValue) throw DomainError.With("Magic number already set"); - Emit(new SimpleEvent(magicNumber)); + Emit(new ExampleEvent(magicNumber)); } // We apply the event as part of the event sourcing system. EventFlow // provides several different methods for doing this, e.g. state objects, // the Apply method is merely the simplest - public void Apply(SimpleEvent aggregateEvent) + public void Apply(ExampleEvent aggregateEvent) { _magicNumber = aggregateEvent.MagicNumber; } } // A basic event containing some information - public class SimpleEvent : AggregateEvent + public class ExampleEvent : AggregateEvent { - public SimpleEvent(int magicNumber) + public ExampleEvent(int magicNumber) { MagicNumber = magicNumber; } @@ -117,10 +117,10 @@ public SimpleEvent(int magicNumber) } // Command for update magic number - public class SimpleCommand : Command + public class ExampleCommand : Command { - public SimpleCommand( - SimpleId aggregateId, + public ExampleCommand( + ExampleId aggregateId, int magicNumber) : base(aggregateId) { @@ -131,11 +131,11 @@ public SimpleCommand( } // Command handler for our command - public class SimpleCommandHandler : CommandHandler + public class ExampleCommandHandler : CommandHandler { public override Task ExecuteAsync( - SimpleAggrenate aggregate, - SimpleCommand command, + ExampleAggrenate aggregate, + ExampleCommand command, CancellationToken cancellationToken) { aggregate.SetMagicNumer(command.MagicNumber); @@ -144,14 +144,14 @@ public override Task ExecuteAsync( } // Read model for our aggregate - public class SimpleReadModel : IReadModel, - IAmReadModelFor + public class ExampleReadModel : IReadModel, + IAmReadModelFor { public int MagicNumber { get; private set; } public void Apply( IReadModelContext context, - IDomainEvent domainEvent) + IDomainEvent domainEvent) { MagicNumber = domainEvent.AggregateEvent.MagicNumber; } From e8af05ea942f450054c4e7faadac8cc27f2b7170 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sun, 28 Aug 2016 00:18:04 +0200 Subject: [PATCH 07/17] Cleanup --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index da43cb233..4c1a861c0 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,9 @@ The example consists of the following classes, each shown below - `ExampleReadModel`: In-memory read model providing easy access to the current state +**Note:** This example is part of the EventFlow test suite, so checkout the +code and give it a go. + ```csharp [Test] public async Task Example() @@ -264,8 +267,6 @@ public class ExampleReadModel : IReadModel, } ``` -**Note:** The above example is part of the EventFlow test suite, so checkout -the code and give it a go. ## State of EventFlow From 8593ecf496cadefe888b5016795d26343a8c411e Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sun, 28 Aug 2016 00:19:49 +0200 Subject: [PATCH 08/17] Add missing category --- Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs b/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs index 31b067e89..f8296d62a 100644 --- a/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/CompleteExampleTests.cs @@ -31,11 +31,13 @@ using EventFlow.Extensions; using EventFlow.Queries; using EventFlow.ReadStores; +using EventFlow.TestHelpers; using FluentAssertions; using NUnit.Framework; namespace EventFlow.Tests.IntegrationTests { + [Category(Categories.Integration)] public class CompleteExampleTests { [Test] From 0b5bad8c30c77f869b81cf1a374db6d518726886 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 5 Sep 2016 21:03:51 +0200 Subject: [PATCH 09/17] Aggregate saga events should be published --- .../Sagas/AggregateSagaTests.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Source/EventFlow.Tests/IntegrationTests/Sagas/AggregateSagaTests.cs b/Source/EventFlow.Tests/IntegrationTests/Sagas/AggregateSagaTests.cs index 6cf52f75d..3a96401a1 100644 --- a/Source/EventFlow.Tests/IntegrationTests/Sagas/AggregateSagaTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/Sagas/AggregateSagaTests.cs @@ -25,14 +25,18 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using EventFlow.Aggregates; using EventFlow.Configuration; using EventFlow.Sagas; +using EventFlow.Subscribers; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Commands; using EventFlow.TestHelpers.Aggregates.Sagas; +using EventFlow.TestHelpers.Aggregates.Sagas.Events; using EventFlow.TestHelpers.Aggregates.ValueObjects; using FluentAssertions; +using Moq; using NUnit.Framework; namespace EventFlow.Tests.IntegrationTests.Sagas @@ -40,6 +44,8 @@ namespace EventFlow.Tests.IntegrationTests.Sagas [Category(Categories.Integration)] public class AggregateSagaTests : IntegrationTest { + private Mock> _thingySagaStartedSubscriber; + [Test] public async Task InitialSagaStateIsNew() { @@ -121,6 +127,21 @@ public async Task PublishingStartAndCompleteTiggerEventsCompletesSaga() thingySaga.State.Should().Be(SagaState.Completed); } + [Test] + public async Task AggregateSagaEventsArePublishedToSubscribers() + { + // Arrange + var thingyId = A(); + + // Act + await CommandBus.PublishAsync(new ThingyRequestSagaStartCommand(thingyId), CancellationToken.None).ConfigureAwait(false); + + // Assert + _thingySagaStartedSubscriber.Verify( + s => s.HandleAsync(It.IsAny>(), It.IsAny()), + Times.Once()); + } + [Test] public async Task PublishingStartAndCompleteWithPingsResultInCorrectMessages() { @@ -162,7 +183,14 @@ private Task LoadSagaAsync(ThingyId thingyId) protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) { - return eventFlowOptions.CreateResolver(); + _thingySagaStartedSubscriber = new Mock>(); + _thingySagaStartedSubscriber + .Setup(s => s.HandleAsync(It.IsAny>(), It.IsAny())) + .Returns(Task.FromResult(0)); + + return eventFlowOptions + .RegisterServices(sr => sr.Register(_ => _thingySagaStartedSubscriber.Object)) + .CreateResolver(); } } } \ No newline at end of file From 5f3f699a7828a65f50653f7b16feb28c43571fd9 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 5 Sep 2016 21:07:16 +0200 Subject: [PATCH 10/17] Aggregate saga events are now published --- Source/EventFlow/Aggregates/AggregateStore.cs | 27 +++++++++++++++++-- Source/EventFlow/CommandBus.cs | 6 ----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Source/EventFlow/Aggregates/AggregateStore.cs b/Source/EventFlow/Aggregates/AggregateStore.cs index 817e83960..621a11908 100644 --- a/Source/EventFlow/Aggregates/AggregateStore.cs +++ b/Source/EventFlow/Aggregates/AggregateStore.cs @@ -24,30 +24,36 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using EventFlow.Configuration; using EventFlow.Core; using EventFlow.Core.RetryStrategies; using EventFlow.EventStores; using EventFlow.Exceptions; using EventFlow.Extensions; using EventFlow.Snapshots; +using EventFlow.Subscribers; namespace EventFlow.Aggregates { public class AggregateStore : IAggregateStore { + private readonly IResolver _resolver; private readonly IAggregateFactory _aggregateFactory; private readonly IEventStore _eventStore; private readonly ISnapshotStore _snapshotStore; private readonly ITransientFaultHandler _transientFaultHandler; public AggregateStore( + IResolver resolver, IAggregateFactory aggregateFactory, IEventStore eventStore, ISnapshotStore snapshotStore, ITransientFaultHandler transientFaultHandler) { + _resolver = resolver; _aggregateFactory = aggregateFactory; _eventStore = eventStore; _snapshotStore = snapshotStore; @@ -93,14 +99,31 @@ public Task> UpdateAsync> StoreAsync( + public async Task> StoreAsync( TAggregate aggregate, ISourceId sourceId, CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity { - return aggregate.CommitAsync(_eventStore, _snapshotStore, sourceId, cancellationToken); + var domainEvents = await aggregate.CommitAsync( + _eventStore, + _snapshotStore, + sourceId, + cancellationToken) + .ConfigureAwait(false); + + if (domainEvents.Any()) + { + var domainEventPublisher = _resolver.Resolve(); + await domainEventPublisher.PublishAsync( + aggregate.Id, + domainEvents, + cancellationToken) + .ConfigureAwait(false); + } + + return domainEvents; } } } \ No newline at end of file diff --git a/Source/EventFlow/CommandBus.cs b/Source/EventFlow/CommandBus.cs index 1a99162f6..c6c169aa6 100644 --- a/Source/EventFlow/CommandBus.cs +++ b/Source/EventFlow/CommandBus.cs @@ -107,12 +107,6 @@ public async Task PublishAsync d.EventType.PrettyPrint())))); - await _domainEventPublisher.PublishAsync( - command.AggregateId, - domainEvents, - cancellationToken) - .ConfigureAwait(false); - return command.SourceId; } From f350d9bcba55b4ba128f06a0bac64e1c0d640e2e Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 5 Sep 2016 21:10:56 +0200 Subject: [PATCH 11/17] Cleanup --- Source/EventFlow/CommandBus.cs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/Source/EventFlow/CommandBus.cs b/Source/EventFlow/CommandBus.cs index c6c169aa6..9847f1add 100644 --- a/Source/EventFlow/CommandBus.cs +++ b/Source/EventFlow/CommandBus.cs @@ -35,7 +35,6 @@ using EventFlow.Exceptions; using EventFlow.Extensions; using EventFlow.Logs; -using EventFlow.Subscribers; namespace EventFlow { @@ -44,20 +43,17 @@ public class CommandBus : ICommandBus private readonly ILog _log; private readonly IResolver _resolver; private readonly IAggregateStore _aggregateStore; - private readonly IDomainEventPublisher _domainEventPublisher; private readonly IMemoryCache _memoryCache; public CommandBus( ILog log, IResolver resolver, IAggregateStore aggregateStore, - IDomainEventPublisher domainEventPublisher, IMemoryCache memoryCache) { _log = log; _resolver = resolver; _aggregateStore = aggregateStore; - _domainEventPublisher = domainEventPublisher; _memoryCache = memoryCache; } @@ -90,22 +86,18 @@ public async Task PublishAsync string.Format( + _log.Verbose(() => domainEvents.Any() + ? string.Format( "Execution command '{0}' with ID '{1}' on aggregate '{2}' did NOT result in any domain events", command.GetType().PrettyPrint(), command.SourceId, - typeof(TAggregate).PrettyPrint())); - return command.SourceId; - } - - _log.Verbose(() => string.Format( - "Execution command '{0}' with ID '{1}' on aggregate '{2}' resulted in these events: {3}", - command.GetType().PrettyPrint(), - command.SourceId, - typeof(TAggregate), - string.Join(", ", domainEvents.Select(d => d.EventType.PrettyPrint())))); + typeof(TAggregate).PrettyPrint()) + : string.Format( + "Execution command '{0}' with ID '{1}' on aggregate '{2}' resulted in these events: {3}", + command.GetType().PrettyPrint(), + command.SourceId, + typeof(TAggregate), + string.Join(", ", domainEvents.Select(d => d.EventType.PrettyPrint())))); return command.SourceId; } From f3207e044c3c3c1d22f97949bf47e3e2119f409b Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 5 Sep 2016 21:13:38 +0200 Subject: [PATCH 12/17] Only resolve the command bus once --- Source/EventFlow/Sagas/DispatchToSagas.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Source/EventFlow/Sagas/DispatchToSagas.cs b/Source/EventFlow/Sagas/DispatchToSagas.cs index eb4dbc13c..699719247 100644 --- a/Source/EventFlow/Sagas/DispatchToSagas.cs +++ b/Source/EventFlow/Sagas/DispatchToSagas.cs @@ -60,18 +60,23 @@ public async Task ProcessAsync( IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { + var commandBus = _resolver.Resolve(); foreach (var domainEvent in domainEvents) { - await ProcessAsync(domainEvent, cancellationToken).ConfigureAwait(false); + await ProcessAsync( + commandBus, + domainEvent, + cancellationToken) + .ConfigureAwait(false); } } private async Task ProcessAsync( + ICommandBus commandBus, IDomainEvent domainEvent, CancellationToken cancellationToken) { var sagaTypeDetails = _sagaDefinitionService.GetSagaDetails(domainEvent.EventType); - var commandBus = _resolver.Resolve(); _log.Verbose(() => $"Saga types to process for domain event '{domainEvent.EventType.PrettyPrint()}': {string.Join(", ", sagaTypeDetails.Select(d => d.SagaType.PrettyPrint()))}"); From 6a6e026fc635206df5d6e4c9d80cb8fdf4ef65bc Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 5 Sep 2016 21:37:09 +0200 Subject: [PATCH 13/17] Test events are stored and published --- .../Aggregates/AggregateStoreTests.cs | 92 +++++++++++++++++-- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs index 9edad1b5b..1f3039a12 100644 --- a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs @@ -33,9 +33,11 @@ using EventFlow.EventStores; using EventFlow.Exceptions; using EventFlow.Logs; +using EventFlow.Subscribers; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Events; +using EventFlow.TestHelpers.Aggregates.ValueObjects; using Moq; using NUnit.Framework; using Ploeh.AutoFixture; @@ -47,6 +49,8 @@ public class AggregateStoreTests : TestsFor { private Mock _eventStoreMock; private Mock _aggregateFactoryMock; + private Mock _resolverMock; + private Mock _domainEventPublisherMock; [SetUp] public void SetUp() @@ -58,6 +62,12 @@ public void SetUp() _eventStoreMock = InjectMock(); _aggregateFactoryMock = InjectMock(); + _resolverMock = InjectMock(); + + _domainEventPublisherMock = new Mock(); + _resolverMock + .Setup(r => r.Resolve()) + .Returns(_domainEventPublisherMock.Object); _aggregateFactoryMock .Setup(f => f.CreateNewAggregateAsync(It.IsAny())) @@ -68,15 +78,24 @@ public void SetUp() public void UpdateAsync_RetryForOptimisticConcurrencyExceptionsAreDone() { // Arrange - Arrange_EventStore_LoadEventsAsync_ReturnsEvents(); + Arrange_EventStore_LoadEventsAsync(); Arrange_EventStore_StoreAsync_ThrowsOptimisticConcurrencyException(); // Act - Assert.ThrowsAsync(async () => await Sut.UpdateAsync(A(), A(), NoOperationAsync, CancellationToken.None).ConfigureAwait(false)); + Assert.ThrowsAsync(async () => await Sut.UpdateAsync( + A(), + A(), + NoOperationAsync, + CancellationToken.None) + .ConfigureAwait(false)); // Assert _eventStoreMock.Verify( - s => s.StoreAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), + s => s.StoreAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()), Times.Exactly(6)); } @@ -86,23 +105,80 @@ public void UpdateAsync_DuplicateOperationExceptionIsThrowsIfSourceAlreadyApplie // Arrange var domainEvents = ManyDomainEvents(1).ToArray(); var sourceId = domainEvents[0].Metadata.SourceId; - Arrange_EventStore_LoadEventsAsync_ReturnsEvents(domainEvents); + Arrange_EventStore_LoadEventsAsync(domainEvents); // Act - Assert.ThrowsAsync(async () => await Sut.UpdateAsync(A(), sourceId, NoOperationAsync, CancellationToken.None).ConfigureAwait(false)); + Assert.ThrowsAsync(async () => await Sut.UpdateAsync( + A(), + sourceId, + NoOperationAsync, + CancellationToken.None) + .ConfigureAwait(false)); + } + + [Test] + public async Task UpdateAsync_EventsCommittedAndPublished() + { + // Arrange + Arrange_EventStore_LoadEventsAsync(); + Arrange_EventStore_StoreAsync(ManyDomainEvents(1).ToArray()); + + // Sut + await Sut.UpdateAsync( + A(), + A(), + (a, c) => + { + a.Ping(A()); + return Task.FromResult(0); + }, + CancellationToken.None) + .ConfigureAwait(false); + + // Assert + _eventStoreMock.Verify( + m => m.StoreAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Once); + _domainEventPublisherMock.Verify( + m => m.PublishAsync( + It.IsAny(), + It.Is>(e => e.Count == 1), + It.IsAny()), + Times.Once); + } + + private void Arrange_EventStore_StoreAsync(params IDomainEvent[] domainEvents) + { + _eventStoreMock + .Setup(s => s.StoreAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult>>(domainEvents)); } private void Arrange_EventStore_StoreAsync_ThrowsOptimisticConcurrencyException() { _eventStoreMock - .Setup(s => s.StoreAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())) + .Setup(s => s.StoreAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny())) .ThrowsAsync(new OptimisticConcurrencyException(string.Empty, null)); } - private void Arrange_EventStore_LoadEventsAsync_ReturnsEvents(params IDomainEvent[] domainEvents) + private void Arrange_EventStore_LoadEventsAsync(params IDomainEvent[] domainEvents) { _eventStoreMock - .Setup(s => s.LoadEventsAsync(It.IsAny(), It.IsAny())) + .Setup(s => s.LoadEventsAsync( + It.IsAny(), + It.IsAny())) .Returns(Task.FromResult>>(domainEvents)); } From deb72bfed142ada74342fc557953a22d0d88f35c Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 5 Sep 2016 21:41:53 +0200 Subject: [PATCH 14/17] Updated release notes --- RELEASE_NOTES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 376a1fae6..4b357d43a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,9 @@ ### New in 0.35 (not released yet) -* _Nothing yet_ +* Breaking: Domain event publishing has been moved from `CommandBus` to + `AggregateStore`. If you do not use `IAggregateStore` directly in your + code base (which is unlikely), there's no change in behavior +* Fixed: Domain events emitted from aggregate sagas are now published ### New in 0.34.2221 (released 2016-08-23) From bce60c0edea792f681741628d8e56b34f0ffeb69 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 6 Sep 2016 19:27:50 +0200 Subject: [PATCH 15/17] Show that the aggregate store doesn't handle optimistic concurrency exceptions correctly --- .../Aggregates/AggregateStoreTests.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs index 1f3039a12..b334a16fd 100644 --- a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs @@ -116,6 +116,35 @@ public void UpdateAsync_DuplicateOperationExceptionIsThrowsIfSourceAlreadyApplie .ConfigureAwait(false)); } + [Test] + public void UpdateAsync_EventsArePublishedOnce_IfPublisherThrowsOptimisticConcurrencyException() + { + // Arrange + Arrange_EventStore_LoadEventsAsync(); + Arrange_EventStore_StoreAsync(ManyDomainEvents(1).ToArray()); + Arrange_DomainEventPublisher_PublishAsync_ThrowsOptimisticConcurrencyException(); + + // Sut + Assert.ThrowsAsync(async () => await Sut.UpdateAsync( + A(), + A(), + (a, c) => + { + a.Ping(A()); + return Task.FromResult(0); + }, + CancellationToken.None) + .ConfigureAwait(false)); + + // Assert + _domainEventPublisherMock.Verify( + m => m.PublishAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny()), + Times.Once); + } + [Test] public async Task UpdateAsync_EventsCommittedAndPublished() { @@ -173,6 +202,16 @@ private void Arrange_EventStore_StoreAsync_ThrowsOptimisticConcurrencyException( .ThrowsAsync(new OptimisticConcurrencyException(string.Empty, null)); } + private void Arrange_DomainEventPublisher_PublishAsync_ThrowsOptimisticConcurrencyException() + { + _domainEventPublisherMock + .Setup(m => m.PublishAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Throws(new OptimisticConcurrencyException(string.Empty, null)); + } + private void Arrange_EventStore_LoadEventsAsync(params IDomainEvent[] domainEvents) { _eventStoreMock From f1203b8722c909a6b1e7b57ed559485acaedcb8d Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 6 Sep 2016 19:29:59 +0200 Subject: [PATCH 16/17] Correct too many invocations if there's an optimistic concurrency exception --- Source/EventFlow/Aggregates/AggregateStore.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/Source/EventFlow/Aggregates/AggregateStore.cs b/Source/EventFlow/Aggregates/AggregateStore.cs index 621a11908..327204a34 100644 --- a/Source/EventFlow/Aggregates/AggregateStore.cs +++ b/Source/EventFlow/Aggregates/AggregateStore.cs @@ -71,7 +71,7 @@ public async Task LoadAsync( return aggregate; } - public Task> UpdateAsync( + public async Task> UpdateAsync( TIdentity id, ISourceId sourceId, Func updateAggregate, @@ -79,7 +79,7 @@ public Task> UpdateAsync where TIdentity : IIdentity { - return _transientFaultHandler.TryAsync( + var domainEvents = await _transientFaultHandler.TryAsync( async c => { var aggregate = await LoadAsync(id, c).ConfigureAwait(false); @@ -93,10 +93,28 @@ public Task> UpdateAsync(aggregate, sourceId, c).ConfigureAwait(false); + return await aggregate.CommitAsync( + _eventStore, + _snapshotStore, + sourceId, + cancellationToken) + .ConfigureAwait(false); }, Label.Named("aggregate-update"), - cancellationToken); + cancellationToken) + .ConfigureAwait(false); + + if (domainEvents.Any()) + { + var domainEventPublisher = _resolver.Resolve(); + await domainEventPublisher.PublishAsync( + id, + domainEvents, + cancellationToken) + .ConfigureAwait(false); + } + + return domainEvents; } public async Task> StoreAsync( From d0ee378d18dc0297ca13f036d15553a2750b96d5 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 6 Sep 2016 19:34:05 +0200 Subject: [PATCH 17/17] Updated release notes --- RELEASE_NOTES.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4b357d43a..0d26fff15 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,8 +1,9 @@ ### New in 0.35 (not released yet) -* Breaking: Domain event publishing has been moved from `CommandBus` to - `AggregateStore`. If you do not use `IAggregateStore` directly in your - code base (which is unlikely), there's no change in behavior +* Fixed: `IAggregateStore.UpdateAsync` and `StoreAsync` now publishes committed + events as expected. This basically means that its now possible to circumvent the + command and command handler pattern and use the `IAggregateStore.UpdateAsync` + directly to modify an aggregate root * Fixed: Domain events emitted from aggregate sagas are now published ### New in 0.34.2221 (released 2016-08-23)