From 531b151733d07317c0078ee5a0fc24e3135bd319 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sun, 16 Aug 2015 09:45:08 +0200 Subject: [PATCH 01/54] Started on a does and don'ts guide to help developers minimize the problems when developing a CQRS+ES application --- Documentation/DoesAndDonts.md | 34 ++++++++++++++++++++++++++++++++++ README.md | 3 ++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 Documentation/DoesAndDonts.md diff --git a/Documentation/DoesAndDonts.md b/Documentation/DoesAndDonts.md new file mode 100644 index 000000000..9657bdf47 --- /dev/null +++ b/Documentation/DoesAndDonts.md @@ -0,0 +1,34 @@ +# Does and Don'ts +Whenever creating an application that uses CQRS+ES there are several things +you need to keep in mind to make it easier and minimize the potential bugs. +This guide will give you some details on typical problems and how EventFlow +can help you minimize the risk. + +## Events + +#### Produce clean JSON +Make sure that when your aggregate events are JSON serialized, they produce +clean JSON as it makes it easier to work with and enable you to easier +deserialize the events in the future. + +- No type information +- No hints of value objects (see [value objects](ValueObjects.md)) + +Here's an example of good clean event JSON produced from a create user event. + +```JSON +{ + "Username": "root", + "PasswordHash": "1234567890ABCDEF", + "EMail": "root@example.org", +} +``` + +#### Keep old event types +Keep in mind, that you need to keep the event types in your code for as long as +these events are in the event source, which in most cases are _forever_ as +storage is cheap and information, i.e., your domain events, is expensive. + +However, you should still clear your code, have a look at how you can +[upgrade and version your events](./EventUpgrade.md) for details on how +EventFlow supports you in this. diff --git a/README.md b/README.md index 8b3578dd2..5b3fd7c07 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ EventFlow is a basic CQRS+ES framework designed to be easy to use. -Have a look at our [Getting started guide](./Documentation/GettingStarted.md). +Have a look at our [Getting started guide](./Documentation/GettingStarted.md) +and the [dos and don'ts](./Documentation/DoesAndDonts.md). ### Features From 5a3bcbc105f8511f0c795c923fd9e551b5b750fa Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sun, 16 Aug 2015 10:17:58 +0200 Subject: [PATCH 02/54] Upgrade FAKE to v4.1.3 --- build.cmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.cmd b/build.cmd index 639c826f8..051978694 100644 --- a/build.cmd +++ b/build.cmd @@ -3,7 +3,7 @@ rem @echo off mkdir Tools powershell -Command "if ((Test-Path '.\Tools\NuGet.exe') -eq $false) {(New-Object System.Net.WebClient).DownloadFile('http://nuget.org/nuget.exe', '.\Tools\NuGet.exe')}" -".\Tools\NuGet.exe" "install" "FAKE.Core" "-OutputDirectory" "Tools" "-ExcludeVersion" "-version" "3.30.3" +".\Tools\NuGet.exe" "install" "FAKE.Core" "-OutputDirectory" "Tools" "-ExcludeVersion" "-version" "4.1.3" ".\Tools\NuGet.exe" "install" "NUnit.Runners" "-OutputDirectory" "Tools" "-ExcludeVersion" "-version" "2.6.4" ".\Tools\NuGet.exe" "install" "ilmerge" "-OutputDirectory" "Tools" "-ExcludeVersion" "-version" "2.14.1208" From 2fb4027bd829085b76bfd2120f4fdfacbb868cb6 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sun, 16 Aug 2015 13:20:58 +0200 Subject: [PATCH 03/54] Add information about the provided Identity<> class --- Documentation/Aggregates.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Documentation/Aggregates.md b/Documentation/Aggregates.md index 14d5d8b62..a53b7a55d 100644 --- a/Documentation/Aggregates.md +++ b/Documentation/Aggregates.md @@ -2,8 +2,8 @@ Initially before you can create a aggregate, you need to create its identity. You can create your own implementation by implementing -the `IIdentity` interface or you can use a base class that EventFlow provides -like this. +the `IIdentity` interface or you can use a base class `Identity<>` that +EventFlow provides, like this. ```csharp public class TestId : Identity @@ -14,7 +14,17 @@ public class TestId : Identity } ``` -Note that its important to call the constructor argument for `value` as +The `Identity<>` value object provides generic functionality to create and +validate aggregate root IDs. + +- IDs follow the form `{class with "Id"}-{guid}` e.g. + `test-c93fdb8c-5c9a-4134-bbcd-87c0644ca34f` +- IDs can be generated using the static `New` property +- IDs can be validated using the static `bool IsValid(string)` method +- ID validation errors (if any) can be gathered using the static + `IEnumerable Validate(string)` method + +Note that its important to _name_ the constructor argument `value` as its significant if you serialize the ID. Next, to create a new aggregate, simply inherit from `AggregateRoot<,>` like From a3329bf275266ebef868268022dc6a5a0aa74ebf Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sun, 16 Aug 2015 13:21:35 +0200 Subject: [PATCH 04/54] Fixed wording --- Documentation/Aggregates.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/Aggregates.md b/Documentation/Aggregates.md index a53b7a55d..767d0c553 100644 --- a/Documentation/Aggregates.md +++ b/Documentation/Aggregates.md @@ -17,8 +17,8 @@ public class TestId : Identity The `Identity<>` value object provides generic functionality to create and validate aggregate root IDs. -- IDs follow the form `{class with "Id"}-{guid}` e.g. - `test-c93fdb8c-5c9a-4134-bbcd-87c0644ca34f` +- IDs follow the form `{class without "Id"}-{guid}` e.g. + `test-c93fdb8c-5c9a-4134-bbcd-87c0644ca34f` for the above `TestId` - IDs can be generated using the static `New` property - IDs can be validated using the static `bool IsValid(string)` method - ID validation errors (if any) can be gathered using the static From 970e620935947949586429dbd625265f425cacee Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 17 Aug 2015 07:08:43 +0200 Subject: [PATCH 05/54] Version is now 0.11 --- 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 8703b4c8e..a8bd05d6d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,8 @@ -### New in 0.10 (not released yet) +### New in 0.11 (not released yet) + + * _Nothing yet_ + +### New in 0.10.642 (released 2015-08-17) * Breaking: Updated NuGet reference `Newtonsoft.Json` to v7.0.1 (up from v6.0.8) diff --git a/appveyor.yml b/appveyor.yml index 913c61873..6f8b3f8a6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ init: - git config --global core.autocrlf input -version: 0.10.{build} +version: 0.11.{build} skip_tags: true From f9cdfdbadd675d0b41817ad42f0886ac6536531e Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 17 Aug 2015 19:55:40 +0200 Subject: [PATCH 06/54] Started on RabbitMQ integration --- EventFlow.sln | 13 ++- .../EventFlow.RabbitMQ.csproj | 82 ++++++++++++++++++ .../IRabbitMqConfiguration.cs | 31 +++++++ .../IRabbitMqConnectionFactory.cs | 34 ++++++++ .../IRabbitMqMessageFactory.cs | 35 ++++++++ .../IRabbitMqModelFactory.cs | 29 +++++++ .../EventFlow.RabbitMQ/IRabbitMqPublisher.cs | 28 +++++++ .../IRabbitMqRetryStrategy.cs | 30 +++++++ .../Properties/AssemblyInfo.cs | 23 +++++ .../RabbitMqConfiguration.cs | 41 +++++++++ .../RabbitMqConnectionFactory.cs | 71 ++++++++++++++++ Source/EventFlow.RabbitMQ/RabbitMqMessage.cs | 40 +++++++++ .../RabbitMqMessageFactory.cs | 60 ++++++++++++++ .../RabbitMqModelFactory.cs | 54 ++++++++++++ .../EventFlow.RabbitMQ/RabbitMqPublisher.cs | 83 +++++++++++++++++++ .../RabbitMqRetryStrategy.cs | 47 +++++++++++ Source/EventFlow.RabbitMQ/packages.config | 4 + 17 files changed, 703 insertions(+), 2 deletions(-) create mode 100644 Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj create mode 100644 Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs create mode 100644 Source/EventFlow.RabbitMQ/IRabbitMqConnectionFactory.cs create mode 100644 Source/EventFlow.RabbitMQ/IRabbitMqMessageFactory.cs create mode 100644 Source/EventFlow.RabbitMQ/IRabbitMqModelFactory.cs create mode 100644 Source/EventFlow.RabbitMQ/IRabbitMqPublisher.cs create mode 100644 Source/EventFlow.RabbitMQ/IRabbitMqRetryStrategy.cs create mode 100644 Source/EventFlow.RabbitMQ/Properties/AssemblyInfo.cs create mode 100644 Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs create mode 100644 Source/EventFlow.RabbitMQ/RabbitMqConnectionFactory.cs create mode 100644 Source/EventFlow.RabbitMQ/RabbitMqMessage.cs create mode 100644 Source/EventFlow.RabbitMQ/RabbitMqMessageFactory.cs create mode 100644 Source/EventFlow.RabbitMQ/RabbitMqModelFactory.cs create mode 100644 Source/EventFlow.RabbitMQ/RabbitMqPublisher.cs create mode 100644 Source/EventFlow.RabbitMQ/RabbitMqRetryStrategy.cs create mode 100644 Source/EventFlow.RabbitMQ/packages.config diff --git a/EventFlow.sln b/EventFlow.sln index 9edc4ae89..1adf4b815 100644 --- a/EventFlow.sln +++ b/EventFlow.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.31101.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow", "Source\EventFlow\EventFlow.csproj", "{11131251-778D-4D2E-BDD1-4844A789BCA9}" EndProject @@ -33,6 +33,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.EventStores.Event EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.EventStores.EventStore.Tests", "Source\EventFlow.EventStores.EventStore.Tests\EventFlow.EventStores.EventStore.Tests.csproj", "{BC4F0E41-6659-4D6D-9D25-1558CBA1649B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RabbitMQ", "RabbitMQ", "{7951DC73-5DAF-4322-9AF0-099BF5C90837}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.RabbitMQ", "Source\EventFlow.RabbitMQ\EventFlow.RabbitMQ.csproj", "{4B06F01F-ACE6-489D-A92A-012F533EFA3C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,6 +91,10 @@ Global {BC4F0E41-6659-4D6D-9D25-1558CBA1649B}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC4F0E41-6659-4D6D-9D25-1558CBA1649B}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC4F0E41-6659-4D6D-9D25-1558CBA1649B}.Release|Any CPU.Build.0 = Release|Any CPU + {4B06F01F-ACE6-489D-A92A-012F533EFA3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B06F01F-ACE6-489D-A92A-012F533EFA3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B06F01F-ACE6-489D-A92A-012F533EFA3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B06F01F-ACE6-489D-A92A-012F533EFA3C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -100,5 +108,6 @@ Global {2F3A5BCA-5336-4BB1-BA3D-0FEEA78C0415} = {9876C758-0A72-400E-A1B1-685E1C22ACB2} {E42A253D-2011-4799-B55D-1D0C61E171C2} = {F6D62A27-50EA-4846-8F36-F3D36F52DCA6} {BC4F0E41-6659-4D6D-9D25-1558CBA1649B} = {F6D62A27-50EA-4846-8F36-F3D36F52DCA6} + {4B06F01F-ACE6-489D-A92A-012F533EFA3C} = {7951DC73-5DAF-4322-9AF0-099BF5C90837} EndGlobalSection EndGlobal diff --git a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj new file mode 100644 index 000000000..9f983181c --- /dev/null +++ b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj @@ -0,0 +1,82 @@ + + + + + Debug + AnyCPU + {4B06F01F-ACE6-489D-A92A-012F533EFA3C} + Library + Properties + EventFlow.RabbitMQ + EventFlow.RabbitMQ + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\RabbitMQ.Client.3.5.4\lib\net40\RabbitMQ.Client.dll + True + + + + + + + + + + + + + Properties\SolutionInfo.cs + + + + + + + + + + + + + + + + + + + {11131251-778d-4d2e-bdd1-4844a789bca9} + EventFlow + + + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs b/Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs new file mode 100644 index 000000000..6cfe308fe --- /dev/null +++ b/Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs @@ -0,0 +1,31 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; + +namespace EventFlow.RabbitMQ +{ + public interface IRabbitMqConfiguration + { + Uri Uri { get; } + } +} \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqConnectionFactory.cs b/Source/EventFlow.RabbitMQ/IRabbitMqConnectionFactory.cs new file mode 100644 index 000000000..57ce70216 --- /dev/null +++ b/Source/EventFlow.RabbitMQ/IRabbitMqConnectionFactory.cs @@ -0,0 +1,34 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; + +namespace EventFlow.RabbitMQ +{ + public interface IRabbitMqConnectionFactory + { + Task CreateConnectionAsync(Uri uri, CancellationToken cancellationToken); + } +} diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/IRabbitMqMessageFactory.cs new file mode 100644 index 000000000..a66beb37b --- /dev/null +++ b/Source/EventFlow.RabbitMQ/IRabbitMqMessageFactory.cs @@ -0,0 +1,35 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; + +namespace EventFlow.RabbitMQ +{ + public interface IRabbitMqMessageFactory + { + Task CreateMessageAsync( + IDomainEvent domainEvent, + CancellationToken cancellationToken); + } +} diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqModelFactory.cs b/Source/EventFlow.RabbitMQ/IRabbitMqModelFactory.cs new file mode 100644 index 000000000..493340e31 --- /dev/null +++ b/Source/EventFlow.RabbitMQ/IRabbitMqModelFactory.cs @@ -0,0 +1,29 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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. + +namespace EventFlow.RabbitMQ +{ + public interface IRabbitMqModelFactory + { + + } +} \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/IRabbitMqPublisher.cs new file mode 100644 index 000000000..b250b967b --- /dev/null +++ b/Source/EventFlow.RabbitMQ/IRabbitMqPublisher.cs @@ -0,0 +1,28 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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. + +namespace EventFlow.RabbitMQ +{ + public interface IRabbitMqPublisher + { + } +} diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqRetryStrategy.cs b/Source/EventFlow.RabbitMQ/IRabbitMqRetryStrategy.cs new file mode 100644 index 000000000..11957006b --- /dev/null +++ b/Source/EventFlow.RabbitMQ/IRabbitMqRetryStrategy.cs @@ -0,0 +1,30 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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 EventFlow.Core; + +namespace EventFlow.RabbitMQ +{ + public interface IRabbitMqRetryStrategy : IRetryStrategy + { + } +} \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ/Properties/AssemblyInfo.cs b/Source/EventFlow.RabbitMQ/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..4bea0ad63 --- /dev/null +++ b/Source/EventFlow.RabbitMQ/Properties/AssemblyInfo.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("EventFlow.RabbitMQ")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("EventFlow.RabbitMQ")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4b06f01f-ace6-489d-a92a-012f533efa3c")] diff --git a/Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs b/Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs new file mode 100644 index 000000000..abd54b8ab --- /dev/null +++ b/Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs @@ -0,0 +1,41 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; + +namespace EventFlow.RabbitMQ +{ + public class RabbitMqConfiguration : IRabbitMqConfiguration + { + public Uri Uri { get; } + + public static IRabbitMqConfiguration With(Uri uri) + { + return new RabbitMqConfiguration(uri); + } + + private RabbitMqConfiguration(Uri uri) + { + Uri = uri; + } + } +} diff --git a/Source/EventFlow.RabbitMQ/RabbitMqConnectionFactory.cs b/Source/EventFlow.RabbitMQ/RabbitMqConnectionFactory.cs new file mode 100644 index 000000000..bfeff246e --- /dev/null +++ b/Source/EventFlow.RabbitMQ/RabbitMqConnectionFactory.cs @@ -0,0 +1,71 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Core; +using RabbitMQ.Client; + +namespace EventFlow.RabbitMQ +{ + public class RabbitMqConnectionFactory : IRabbitMqConnectionFactory + { + private readonly AsyncLock _asyncLock = new AsyncLock(); + private readonly Dictionary _connectionFactories = new Dictionary(); + + public async Task CreateConnectionAsync(Uri uri, CancellationToken cancellationToken) + { + var connectionFactory = await CreateConnectionFactoryAsync(uri, cancellationToken).ConfigureAwait(false); + return connectionFactory.CreateConnection(); + } + + private async Task CreateConnectionFactoryAsync(Uri uri, CancellationToken cancellationToken) + { + using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) + { + ConnectionFactory connectionFactory; + if (_connectionFactories.TryGetValue(uri, out connectionFactory)) + { + return connectionFactory; + } + + connectionFactory = new ConnectionFactory + { + Uri = uri.ToString(), + UseBackgroundThreadsForIO = true, + TopologyRecoveryEnabled = true, + AutomaticRecoveryEnabled = true, + ClientProperties = new Dictionary + { + { "eventflow-version", typeof(RabbitMqConnectionFactory).Assembly.GetName().Version.ToString() }, + { "machine-name", Environment.MachineName } + }, + }; + + _connectionFactories.Add(uri, connectionFactory); + return connectionFactory; + } + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ/RabbitMqMessage.cs b/Source/EventFlow.RabbitMQ/RabbitMqMessage.cs new file mode 100644 index 000000000..7e2632a0b --- /dev/null +++ b/Source/EventFlow.RabbitMQ/RabbitMqMessage.cs @@ -0,0 +1,40 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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.Collections.Generic; + +namespace EventFlow.RabbitMQ +{ + public class RabbitMqMessage + { + public byte[] Message { get; private set; } + public IReadOnlyDictionary Headers { get; private set; } + + public RabbitMqMessage( + byte[] message, + IReadOnlyDictionary headers) + { + Message = message; + Headers = headers; + } + } +} diff --git a/Source/EventFlow.RabbitMQ/RabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/RabbitMqMessageFactory.cs new file mode 100644 index 000000000..57e342d7e --- /dev/null +++ b/Source/EventFlow.RabbitMQ/RabbitMqMessageFactory.cs @@ -0,0 +1,60 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.EventStores; + +namespace EventFlow.RabbitMQ +{ + public class RabbitMqMessageFactory : IRabbitMqMessageFactory + { + private readonly IEventJsonSerializer _eventJsonSerializer; + + public RabbitMqMessageFactory( + IEventJsonSerializer eventJsonSerializer) + { + _eventJsonSerializer = eventJsonSerializer; + } + + public Task CreateMessageAsync( + IDomainEvent domainEvent, + CancellationToken cancellationToken) + { + var headers = domainEvent.Metadata + .ToDictionary(kv => string.Format("eventflow-metadata-{0}", kv.Key), kv => kv.Value); + headers.Add("eventflow-encoding", "utf8"); + + var serializedEvent = _eventJsonSerializer.Serialize( + domainEvent.GetAggregateEvent(), + Enumerable.Empty>()); + + var message = Encoding.UTF8.GetBytes(serializedEvent.SerializedData); + + return Task.FromResult(new RabbitMqMessage(message, headers)); + } + } +} diff --git a/Source/EventFlow.RabbitMQ/RabbitMqModelFactory.cs b/Source/EventFlow.RabbitMQ/RabbitMqModelFactory.cs new file mode 100644 index 000000000..7af244377 --- /dev/null +++ b/Source/EventFlow.RabbitMQ/RabbitMqModelFactory.cs @@ -0,0 +1,54 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Core; +using RabbitMQ.Client; + +namespace EventFlow.RabbitMQ +{ + public class RabbitMqModelFactory : IRabbitMqModelFactory + { + private readonly IRabbitMqConnectionFactory _rabbitMqConnectionFactory; + + public RabbitMqModelFactory( + IRabbitMqConnectionFactory rabbitMqConnectionFactory) + { + _rabbitMqConnectionFactory = rabbitMqConnectionFactory; + } + + public async Task WithModelAsync( + Uri uri, + Label label, + Func> action, + CancellationToken cancellationToken) + { + using (var connection = await _rabbitMqConnectionFactory.CreateConnectionAsync(uri, cancellationToken).ConfigureAwait(false)) + using (var model = connection.CreateModel()) + { + return await action(model).ConfigureAwait(false); + } + } + } +} diff --git a/Source/EventFlow.RabbitMQ/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/RabbitMqPublisher.cs new file mode 100644 index 000000000..6b1bde25a --- /dev/null +++ b/Source/EventFlow.RabbitMQ/RabbitMqPublisher.cs @@ -0,0 +1,83 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Core; +using EventFlow.Extensions; +using RabbitMQ.Client; + +namespace EventFlow.RabbitMQ +{ + public class RabbitMqPublisher : IRabbitMqPublisher + { + private readonly IRabbitMqConnectionFactory _connectionFactory; + private readonly IRabbitMqMessageFactory _messageFactory; + private readonly IRabbitMqConfiguration _configuration; + private readonly ITransientFaultHandler _transientFaultHandler; + + public RabbitMqPublisher( + IRabbitMqConnectionFactory connectionFactory, + IRabbitMqMessageFactory messageFactory, + IRabbitMqConfiguration configuration, + ITransientFaultHandler transientFaultHandler) + { + _connectionFactory = connectionFactory; + _messageFactory = messageFactory; + _configuration = configuration; + _transientFaultHandler = transientFaultHandler; + } + + public async Task PublishAsync(IDomainEvent domainEvent, CancellationToken cancellationToken) + { + var message = await _messageFactory.CreateMessageAsync(domainEvent, cancellationToken).ConfigureAwait(false); + + await _transientFaultHandler.TryAsync( + c => PublishAsync(message, c), + Label.Named("rabbitmq-publish"), + cancellationToken) + .ConfigureAwait(false); + } + + private async Task PublishAsync(RabbitMqMessage message, CancellationToken cancellationToken) + { + // TODO: Cache connection/model + + using (var connection = await _connectionFactory.CreateConnectionAsync(_configuration.Uri, cancellationToken).ConfigureAwait(false)) + using (var model = connection.CreateModel()) + { + var basicProperties = model.CreateBasicProperties(); + basicProperties.Headers = message.Headers.ToDictionary(kv => kv.Value, kv => (object)kv.Value); + basicProperties.Persistent = true; // TODO: get from config + basicProperties.Timestamp = new AmqpTimestamp(DateTimeOffset.Now.ToUnixTime()); + + model.BasicPublish("get from config", "get from config", false, basicProperties, message.Message); + } + + return 0; + } + } +} + diff --git a/Source/EventFlow.RabbitMQ/RabbitMqRetryStrategy.cs b/Source/EventFlow.RabbitMQ/RabbitMqRetryStrategy.cs new file mode 100644 index 000000000..0b8c18e0c --- /dev/null +++ b/Source/EventFlow.RabbitMQ/RabbitMqRetryStrategy.cs @@ -0,0 +1,47 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; +using System.Collections.Generic; +using System.IO; +using EventFlow.Core; +using RabbitMQ.Client.Exceptions; + +namespace EventFlow.RabbitMQ +{ + public class RabbitMqRetryStrategy : IRabbitMqRetryStrategy + { + private static readonly ISet TransientExceptions = new HashSet + { + typeof(EndOfStreamException), + typeof(BrokerUnreachableException), + typeof(OperationInterruptedException) + }; + + public Retry ShouldThisBeRetried(Exception exception, TimeSpan totalExecutionTime, int currentRetryCount) + { + return currentRetryCount <= 3 && TransientExceptions.Contains(exception.GetType()) + ? Retry.YesAfter(TimeSpan.FromMilliseconds(25)) + : Retry.No; + } + } +} diff --git a/Source/EventFlow.RabbitMQ/packages.config b/Source/EventFlow.RabbitMQ/packages.config new file mode 100644 index 000000000..143cb339f --- /dev/null +++ b/Source/EventFlow.RabbitMQ/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 5eec07cb4e298cae991dc597815e1aa6890bf157 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 17 Aug 2015 20:02:02 +0200 Subject: [PATCH 07/54] Don't clutter root namespace with classes only used internally --- .../EventFlow.RabbitMQ.csproj | 22 +++++++++---------- .../IRabbitMqConnectionFactory.cs | 2 +- .../IRabbitMqMessageFactory.cs | 2 +- .../IRabbitMqModelFactory.cs | 2 +- .../{ => Integrations}/IRabbitMqPublisher.cs | 2 +- .../IRabbitMqRetryStrategy.cs | 2 +- .../RabbitMqConnectionFactory.cs | 2 +- .../{ => Integrations}/RabbitMqMessage.cs | 2 +- .../RabbitMqMessageFactory.cs | 2 +- .../RabbitMqModelFactory.cs | 2 +- .../{ => Integrations}/RabbitMqPublisher.cs | 2 +- .../RabbitMqRetryStrategy.cs | 2 +- 12 files changed, 22 insertions(+), 22 deletions(-) rename Source/EventFlow.RabbitMQ/{ => Integrations}/IRabbitMqConnectionFactory.cs (97%) rename Source/EventFlow.RabbitMQ/{ => Integrations}/IRabbitMqMessageFactory.cs (97%) rename Source/EventFlow.RabbitMQ/{ => Integrations}/IRabbitMqModelFactory.cs (96%) rename Source/EventFlow.RabbitMQ/{ => Integrations}/IRabbitMqPublisher.cs (96%) rename Source/EventFlow.RabbitMQ/{ => Integrations}/IRabbitMqRetryStrategy.cs (96%) rename Source/EventFlow.RabbitMQ/{ => Integrations}/RabbitMqConnectionFactory.cs (98%) rename Source/EventFlow.RabbitMQ/{ => Integrations}/RabbitMqMessage.cs (97%) rename Source/EventFlow.RabbitMQ/{ => Integrations}/RabbitMqMessageFactory.cs (98%) rename Source/EventFlow.RabbitMQ/{ => Integrations}/RabbitMqModelFactory.cs (98%) rename Source/EventFlow.RabbitMQ/{ => Integrations}/RabbitMqPublisher.cs (98%) rename Source/EventFlow.RabbitMQ/{ => Integrations}/RabbitMqRetryStrategy.cs (97%) diff --git a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj index 9f983181c..5ea979a81 100644 --- a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj +++ b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj @@ -48,19 +48,19 @@ Properties\SolutionInfo.cs - - - - - + + + + + - - - - - - + + + + + + diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqConnectionFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqConnectionFactory.cs similarity index 97% rename from Source/EventFlow.RabbitMQ/IRabbitMqConnectionFactory.cs rename to Source/EventFlow.RabbitMQ/Integrations/IRabbitMqConnectionFactory.cs index 57ce70216..5028353d9 100644 --- a/Source/EventFlow.RabbitMQ/IRabbitMqConnectionFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqConnectionFactory.cs @@ -25,7 +25,7 @@ using System.Threading.Tasks; using RabbitMQ.Client; -namespace EventFlow.RabbitMQ +namespace EventFlow.RabbitMQ.Integrations { public interface IRabbitMqConnectionFactory { diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqMessageFactory.cs similarity index 97% rename from Source/EventFlow.RabbitMQ/IRabbitMqMessageFactory.cs rename to Source/EventFlow.RabbitMQ/Integrations/IRabbitMqMessageFactory.cs index a66beb37b..e781c82f4 100644 --- a/Source/EventFlow.RabbitMQ/IRabbitMqMessageFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqMessageFactory.cs @@ -24,7 +24,7 @@ using System.Threading.Tasks; using EventFlow.Aggregates; -namespace EventFlow.RabbitMQ +namespace EventFlow.RabbitMQ.Integrations { public interface IRabbitMqMessageFactory { diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqModelFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqModelFactory.cs similarity index 96% rename from Source/EventFlow.RabbitMQ/IRabbitMqModelFactory.cs rename to Source/EventFlow.RabbitMQ/Integrations/IRabbitMqModelFactory.cs index 493340e31..5ea7662b7 100644 --- a/Source/EventFlow.RabbitMQ/IRabbitMqModelFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqModelFactory.cs @@ -20,7 +20,7 @@ // 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. -namespace EventFlow.RabbitMQ +namespace EventFlow.RabbitMQ.Integrations { public interface IRabbitMqModelFactory { diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqPublisher.cs similarity index 96% rename from Source/EventFlow.RabbitMQ/IRabbitMqPublisher.cs rename to Source/EventFlow.RabbitMQ/Integrations/IRabbitMqPublisher.cs index b250b967b..ca2252ce6 100644 --- a/Source/EventFlow.RabbitMQ/IRabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqPublisher.cs @@ -20,7 +20,7 @@ // 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. -namespace EventFlow.RabbitMQ +namespace EventFlow.RabbitMQ.Integrations { public interface IRabbitMqPublisher { diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqRetryStrategy.cs b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqRetryStrategy.cs similarity index 96% rename from Source/EventFlow.RabbitMQ/IRabbitMqRetryStrategy.cs rename to Source/EventFlow.RabbitMQ/Integrations/IRabbitMqRetryStrategy.cs index 11957006b..2f49f287c 100644 --- a/Source/EventFlow.RabbitMQ/IRabbitMqRetryStrategy.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqRetryStrategy.cs @@ -22,7 +22,7 @@ using EventFlow.Core; -namespace EventFlow.RabbitMQ +namespace EventFlow.RabbitMQ.Integrations { public interface IRabbitMqRetryStrategy : IRetryStrategy { diff --git a/Source/EventFlow.RabbitMQ/RabbitMqConnectionFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs similarity index 98% rename from Source/EventFlow.RabbitMQ/RabbitMqConnectionFactory.cs rename to Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs index bfeff246e..7481c4b3c 100644 --- a/Source/EventFlow.RabbitMQ/RabbitMqConnectionFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs @@ -27,7 +27,7 @@ using EventFlow.Core; using RabbitMQ.Client; -namespace EventFlow.RabbitMQ +namespace EventFlow.RabbitMQ.Integrations { public class RabbitMqConnectionFactory : IRabbitMqConnectionFactory { diff --git a/Source/EventFlow.RabbitMQ/RabbitMqMessage.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs similarity index 97% rename from Source/EventFlow.RabbitMQ/RabbitMqMessage.cs rename to Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs index 7e2632a0b..4b26eb359 100644 --- a/Source/EventFlow.RabbitMQ/RabbitMqMessage.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs @@ -22,7 +22,7 @@ using System.Collections.Generic; -namespace EventFlow.RabbitMQ +namespace EventFlow.RabbitMQ.Integrations { public class RabbitMqMessage { diff --git a/Source/EventFlow.RabbitMQ/RabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs similarity index 98% rename from Source/EventFlow.RabbitMQ/RabbitMqMessageFactory.cs rename to Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs index 57e342d7e..b0fc1f4db 100644 --- a/Source/EventFlow.RabbitMQ/RabbitMqMessageFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs @@ -28,7 +28,7 @@ using EventFlow.Aggregates; using EventFlow.EventStores; -namespace EventFlow.RabbitMQ +namespace EventFlow.RabbitMQ.Integrations { public class RabbitMqMessageFactory : IRabbitMqMessageFactory { diff --git a/Source/EventFlow.RabbitMQ/RabbitMqModelFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqModelFactory.cs similarity index 98% rename from Source/EventFlow.RabbitMQ/RabbitMqModelFactory.cs rename to Source/EventFlow.RabbitMQ/Integrations/RabbitMqModelFactory.cs index 7af244377..3ca2027f4 100644 --- a/Source/EventFlow.RabbitMQ/RabbitMqModelFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqModelFactory.cs @@ -26,7 +26,7 @@ using EventFlow.Core; using RabbitMQ.Client; -namespace EventFlow.RabbitMQ +namespace EventFlow.RabbitMQ.Integrations { public class RabbitMqModelFactory : IRabbitMqModelFactory { diff --git a/Source/EventFlow.RabbitMQ/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs similarity index 98% rename from Source/EventFlow.RabbitMQ/RabbitMqPublisher.cs rename to Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index 6b1bde25a..ffa957ab8 100644 --- a/Source/EventFlow.RabbitMQ/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -29,7 +29,7 @@ using EventFlow.Extensions; using RabbitMQ.Client; -namespace EventFlow.RabbitMQ +namespace EventFlow.RabbitMQ.Integrations { public class RabbitMqPublisher : IRabbitMqPublisher { diff --git a/Source/EventFlow.RabbitMQ/RabbitMqRetryStrategy.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqRetryStrategy.cs similarity index 97% rename from Source/EventFlow.RabbitMQ/RabbitMqRetryStrategy.cs rename to Source/EventFlow.RabbitMQ/Integrations/RabbitMqRetryStrategy.cs index 0b8c18e0c..d84cb9bfc 100644 --- a/Source/EventFlow.RabbitMQ/RabbitMqRetryStrategy.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqRetryStrategy.cs @@ -26,7 +26,7 @@ using EventFlow.Core; using RabbitMQ.Client.Exceptions; -namespace EventFlow.RabbitMQ +namespace EventFlow.RabbitMQ.Integrations { public class RabbitMqRetryStrategy : IRabbitMqRetryStrategy { From 726e726f3bb2ceeb9f4cf51c8ae591ac0e1a06bc Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 17 Aug 2015 20:19:09 +0200 Subject: [PATCH 08/54] Basic RabbitMQ integration --- .../EventFlow.RabbitMQ.csproj | 4 +-- .../EventFlowOptionsRabbitMqExtensions.cs} | 29 ++++++++++++++--- .../Integrations/IRabbitMqMessageFactory.cs | 4 +-- .../Integrations/IRabbitMqPublisher.cs | 6 ++++ .../Integrations/RabbitMqMessage.cs | 5 ++- .../Integrations/RabbitMqMessageFactory.cs | 13 ++++---- .../Integrations/RabbitMqPublisher.cs | 23 ++++++++------ ...ory.cs => RabbitMqDomainEventPublisher.cs} | 31 ++++++++----------- 8 files changed, 72 insertions(+), 43 deletions(-) rename Source/EventFlow.RabbitMQ/{Integrations/IRabbitMqModelFactory.cs => Extensions/EventFlowOptionsRabbitMqExtensions.cs} (54%) rename Source/EventFlow.RabbitMQ/{Integrations/RabbitMqModelFactory.cs => RabbitMqDomainEventPublisher.cs} (61%) diff --git a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj index 5ea979a81..7b41d1c2c 100644 --- a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj +++ b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj @@ -47,10 +47,11 @@ Properties\SolutionInfo.cs + + - @@ -58,7 +59,6 @@ - diff --git a/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqModelFactory.cs b/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs similarity index 54% rename from Source/EventFlow.RabbitMQ/Integrations/IRabbitMqModelFactory.cs rename to Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs index 5ea7662b7..1b97ae44f 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqModelFactory.cs +++ b/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs @@ -20,10 +20,31 @@ // 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. -namespace EventFlow.RabbitMQ.Integrations +using EventFlow.Configuration.Registrations; +using EventFlow.RabbitMQ.Integrations; +using EventFlow.ReadStores; + +namespace EventFlow.RabbitMQ.Extensions { - public interface IRabbitMqModelFactory + public static class EventFlowOptionsRabbitMqExtensions { - + public static EventFlowOptions PublishToRabbitMq( + this EventFlowOptions eventFlowOptions, + IRabbitMqConfiguration configuration) + { + eventFlowOptions.RegisterServices(sr => + { + sr.Register(); + sr.Register(); + sr.Register(); + sr.Register(); + + sr.Register(rc => configuration, Lifetime.Singleton); + + sr.Register(); + }); + + return eventFlowOptions; + } } -} \ No newline at end of file +} diff --git a/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqMessageFactory.cs index e781c82f4..c0807fc30 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqMessageFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqMessageFactory.cs @@ -28,8 +28,6 @@ namespace EventFlow.RabbitMQ.Integrations { public interface IRabbitMqMessageFactory { - Task CreateMessageAsync( - IDomainEvent domainEvent, - CancellationToken cancellationToken); + RabbitMqMessage CreateMessage(IDomainEvent domainEvent); } } diff --git a/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqPublisher.cs index ca2252ce6..2d0f3af99 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqPublisher.cs @@ -20,9 +20,15 @@ // 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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; + namespace EventFlow.RabbitMQ.Integrations { public interface IRabbitMqPublisher { + Task PublishAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken); } } diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs index 4b26eb359..72308c5a8 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs @@ -28,13 +28,16 @@ public class RabbitMqMessage { public byte[] Message { get; private set; } public IReadOnlyDictionary Headers { get; private set; } + public string RoutingKey { get; private set; } public RabbitMqMessage( byte[] message, - IReadOnlyDictionary headers) + IReadOnlyDictionary headers, + string routingKey) { Message = message; Headers = headers; + RoutingKey = routingKey; } } } diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs index b0fc1f4db..296b8a1e7 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs @@ -23,8 +23,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using System.Threading; -using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.EventStores; @@ -40,9 +38,7 @@ public RabbitMqMessageFactory( _eventJsonSerializer = eventJsonSerializer; } - public Task CreateMessageAsync( - IDomainEvent domainEvent, - CancellationToken cancellationToken) + public RabbitMqMessage CreateMessage(IDomainEvent domainEvent) { var headers = domainEvent.Metadata .ToDictionary(kv => string.Format("eventflow-metadata-{0}", kv.Key), kv => kv.Value); @@ -54,7 +50,12 @@ public Task CreateMessageAsync( var message = Encoding.UTF8.GetBytes(serializedEvent.SerializedData); - return Task.FromResult(new RabbitMqMessage(message, headers)); + var routingKey = string.Format( + "eventflow.domainevent.{0}.{1}", + domainEvent.Metadata.EventName, + domainEvent.Metadata.EventVersion); + + return new RabbitMqMessage(message, headers, routingKey); } } } diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index ffa957ab8..a6395764c 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -21,6 +21,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -50,9 +51,11 @@ public RabbitMqPublisher( _transientFaultHandler = transientFaultHandler; } - public async Task PublishAsync(IDomainEvent domainEvent, CancellationToken cancellationToken) + public async Task PublishAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { - var message = await _messageFactory.CreateMessageAsync(domainEvent, cancellationToken).ConfigureAwait(false); + var message = domainEvents + .Select(e => _messageFactory.CreateMessage(e)) + .ToList(); await _transientFaultHandler.TryAsync( c => PublishAsync(message, c), @@ -61,23 +64,25 @@ await _transientFaultHandler.TryAsync( .ConfigureAwait(false); } - private async Task PublishAsync(RabbitMqMessage message, CancellationToken cancellationToken) + private async Task PublishAsync(IEnumerable messages, CancellationToken cancellationToken) { // TODO: Cache connection/model using (var connection = await _connectionFactory.CreateConnectionAsync(_configuration.Uri, cancellationToken).ConfigureAwait(false)) using (var model = connection.CreateModel()) { - var basicProperties = model.CreateBasicProperties(); - basicProperties.Headers = message.Headers.ToDictionary(kv => kv.Value, kv => (object)kv.Value); - basicProperties.Persistent = true; // TODO: get from config - basicProperties.Timestamp = new AmqpTimestamp(DateTimeOffset.Now.ToUnixTime()); + foreach (var message in messages) + { + var basicProperties = model.CreateBasicProperties(); + basicProperties.Headers = message.Headers.ToDictionary(kv => kv.Value, kv => (object)kv.Value); + basicProperties.Persistent = true; // TODO: get from config + basicProperties.Timestamp = new AmqpTimestamp(DateTimeOffset.Now.ToUnixTime()); - model.BasicPublish("get from config", "get from config", false, basicProperties, message.Message); + model.BasicPublish("get from config", "get from config", false, false, basicProperties, message.Message); + } } return 0; } } } - diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqModelFactory.cs b/Source/EventFlow.RabbitMQ/RabbitMqDomainEventPublisher.cs similarity index 61% rename from Source/EventFlow.RabbitMQ/Integrations/RabbitMqModelFactory.cs rename to Source/EventFlow.RabbitMQ/RabbitMqDomainEventPublisher.cs index 3ca2027f4..ed0b337a2 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqModelFactory.cs +++ b/Source/EventFlow.RabbitMQ/RabbitMqDomainEventPublisher.cs @@ -20,35 +20,30 @@ // 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; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using EventFlow.Core; -using RabbitMQ.Client; +using EventFlow.Aggregates; +using EventFlow.RabbitMQ.Integrations; +using EventFlow.ReadStores; -namespace EventFlow.RabbitMQ.Integrations +namespace EventFlow.RabbitMQ { - public class RabbitMqModelFactory : IRabbitMqModelFactory + public class RabbitMqDomainEventPublisher : IReadStoreManager { - private readonly IRabbitMqConnectionFactory _rabbitMqConnectionFactory; + private readonly IRabbitMqPublisher _rabbitMqPublisher; - public RabbitMqModelFactory( - IRabbitMqConnectionFactory rabbitMqConnectionFactory) + public RabbitMqDomainEventPublisher( + IRabbitMqPublisher rabbitMqPublisher) { - _rabbitMqConnectionFactory = rabbitMqConnectionFactory; + _rabbitMqPublisher = rabbitMqPublisher; } - public async Task WithModelAsync( - Uri uri, - Label label, - Func> action, + public Task UpdateReadStoresAsync( + IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { - using (var connection = await _rabbitMqConnectionFactory.CreateConnectionAsync(uri, cancellationToken).ConfigureAwait(false)) - using (var model = connection.CreateModel()) - { - return await action(model).ConfigureAwait(false); - } + return _rabbitMqPublisher.PublishAsync(domainEvents, cancellationToken); } } } From 9e0125fc42596a82da446e5e94ea3a5626c01ddc Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 17 Aug 2015 20:35:26 +0200 Subject: [PATCH 09/54] Started on the FAQ --- Documentation/FAQ.md | 17 +++++++++++++++++ README.md | 5 +++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 Documentation/FAQ.md diff --git a/Documentation/FAQ.md b/Documentation/FAQ.md new file mode 100644 index 000000000..7ee637114 --- /dev/null +++ b/Documentation/FAQ.md @@ -0,0 +1,17 @@ +# FAQ - frequently asked questions + +#### **Why doesn't EventFlow have a unit of work concept?** + +Short answer, you shouldn't need it. But Mike has a way better answer: + +> In the Domain, everything flows in one direction: forward. When something bad +> happens, a correction is applied. The Domain doesn't care about the database +> and UoW is very coupled to the db. In my opinion, it's a pattern which is +> usable only with data access objects, and in probably 99% of the cases you +> won't be needing it. As with the Singleton, there are better ways but +> everything depends on proper domain design. +>> [Mike Mogosanu](http://blog.sapiensworks.com/post/2014/06/04/Unit-Of-Work-is-the-new-Singleton.aspx/) + +If your case falls within the 1% case, write an decorator for the `ICommandBus` +that starts a transaction, use MSSQL as event store and make sure your read +models are stored in MSSQL as well. diff --git a/README.md b/README.md index 60aa1c27d..50b9af994 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ EventFlow is a basic CQRS+ES framework designed to be easy to use. -Have a look at our [Getting started guide](./Documentation/GettingStarted.md) -and the [dos and don'ts](./Documentation/DoesAndDonts.md). +Have a look at our [getting started guide](./Documentation/GettingStarted.md), +the [dos and don'ts](./Documentation/DoesAndDonts.md) and the +[FAQ](./Documentation/FAQ.md). ### Features From 342e22a1dafcc9a342afd9bbdf3b8a6a62bde61a Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 17 Aug 2015 20:39:19 +0200 Subject: [PATCH 10/54] Added more to the FAQ --- Documentation/FAQ.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Documentation/FAQ.md b/Documentation/FAQ.md index 7ee637114..51242c888 100644 --- a/Documentation/FAQ.md +++ b/Documentation/FAQ.md @@ -1,6 +1,18 @@ # FAQ - frequently asked questions -#### **Why doesn't EventFlow have a unit of work concept?** +#### Why isn't there a "global sequence number" on domain events? + +While this is easy to support in some event stores like MSSQL, it doesn't +really make sense from a domain perspective. Greg Young also has this to say +on the subject: + +> Order is only assured per a handler within an aggregate root +> boundary. There is no assurance of order between handlers or +> between aggregates. Trying to provide those things leads to +> the dark side. +>> [Greg Young](https://groups.yahoo.com/neo/groups/domaindrivendesign/conversations/topics/18453) + +#### Why doesn't EventFlow have a unit of work concept? Short answer, you shouldn't need it. But Mike has a way better answer: From 3e12e23aba7f46dfa9289656da2f01243fa86c4c Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 18 Aug 2015 06:46:06 +0200 Subject: [PATCH 11/54] C#6 --- Source/EventFlow/Aggregates/AggregateEvent.cs | 2 +- Source/EventFlow/Aggregates/AggregateRoot.cs | 28 +++++------------ Source/EventFlow/Aggregates/AggregateState.cs | 6 ++-- Source/EventFlow/Aggregates/DomainEvent.cs | 31 ++++++++----------- Source/EventFlow/Aggregates/Metadata.cs | 4 +-- Source/EventFlow/Commands/Command.cs | 2 +- Source/EventFlow/EventFlowOptions.cs | 7 ++--- .../ValueObjects/SingleValueObject.cs | 9 ++---- Source/EventFlow/ValueObjects/ValueObject.cs | 2 +- 9 files changed, 33 insertions(+), 58 deletions(-) diff --git a/Source/EventFlow/Aggregates/AggregateEvent.cs b/Source/EventFlow/Aggregates/AggregateEvent.cs index deff84070..fb67a6e0c 100644 --- a/Source/EventFlow/Aggregates/AggregateEvent.cs +++ b/Source/EventFlow/Aggregates/AggregateEvent.cs @@ -28,7 +28,7 @@ public abstract class AggregateEvent : IAggregateEvent : IAggregateRoot _uncommittedEvents = new List(); - public TIdentity Id { get; private set; } + public TIdentity Id { get; } public int Version { get; private set; } - public bool IsNew { get { return Version <= 0; } } + public bool IsNew => Version <= 0; public IEnumerable UncommittedEvents { get { return _uncommittedEvents.Select(e => e.AggregateEvent); } } protected AggregateRoot(TIdentity id) { - if (id == null) throw new ArgumentNullException("id"); + if (id == null) throw new ArgumentNullException(nameof(id)); if ((this as TAggregate) == null) { throw WrongImplementationException.With( @@ -64,7 +64,7 @@ protected virtual void Emit(TEvent aggregateEvent, IMetadata metadata = { if (aggregateEvent == null) { - throw new ArgumentNullException("aggregateEvent"); + throw new ArgumentNullException(nameof(aggregateEvent)); } var now = DateTimeOffset.Now; @@ -114,10 +114,7 @@ public void ApplyEvents(IEnumerable aggregateEvents) { if (Version > 0) { - throw new InvalidOperationException(string.Format( - "Aggregate '{0}' with ID '{1}' already has events", - GetType().Name, - Id)); + throw new InvalidOperationException($"Aggregate '{GetType().Name}' with ID '{Id}' already has events"); } foreach (var aggregateEvent in aggregateEvents) @@ -125,10 +122,7 @@ public void ApplyEvents(IEnumerable aggregateEvents) var e = aggregateEvent as IAggregateEvent; if (e == null) { - throw new ArgumentException(string.Format( - "Aggregate event of type '{0}' does not belong with aggregate '{1}',", - aggregateEvent.GetType(), - this)); + throw new ArgumentException($"Aggregate event of type '{aggregateEvent.GetType()}' does not belong with aggregate '{this}',"); } ApplyEvent(e); @@ -159,9 +153,7 @@ protected void Register(Action handler) var eventType = typeof (TAggregateEvent); if (_eventHandlers.ContainsKey(eventType)) { - throw new ArgumentException(string.Format( - "There's already a event handler registered for the aggregate event '{0}'", - eventType.Name)); + throw new ArgumentException($"There's already a event handler registered for the aggregate event '{eventType.Name}'"); } _eventHandlers[eventType] = e => handler((TAggregateEvent)e); } @@ -207,11 +199,7 @@ protected Action GetApplyMethod(Type aggregateEvent public override string ToString() { - return string.Format( - "{0} v{1}(-{2})", - GetType().Name, - Version, - _uncommittedEvents.Count); + return $"{GetType().Name} v{Version}(-{_uncommittedEvents.Count})"; } } } diff --git a/Source/EventFlow/Aggregates/AggregateState.cs b/Source/EventFlow/Aggregates/AggregateState.cs index 878142647..30fc0967e 100644 --- a/Source/EventFlow/Aggregates/AggregateState.cs +++ b/Source/EventFlow/Aggregates/AggregateState.cs @@ -59,10 +59,8 @@ protected AggregateState() var me = this as TEventApplier; if (me == null) { - throw new InvalidOperationException(string.Format( - "Event applier of type '{0}' has a wrong generic argument '{1}'", - GetType().PrettyPrint(), - typeof(TEventApplier).PrettyPrint())); + throw new InvalidOperationException( + $"Event applier of type '{GetType().PrettyPrint()}' has a wrong generic argument '{typeof (TEventApplier).PrettyPrint()}'"); } } diff --git a/Source/EventFlow/Aggregates/DomainEvent.cs b/Source/EventFlow/Aggregates/DomainEvent.cs index abe9d5aa5..692bf690b 100644 --- a/Source/EventFlow/Aggregates/DomainEvent.cs +++ b/Source/EventFlow/Aggregates/DomainEvent.cs @@ -29,14 +29,14 @@ public class DomainEvent : IDomainEvent< where TIdentity : IIdentity where TAggregateEvent : IAggregateEvent { - public Type AggregateType { get { return typeof (TAggregate); } } - public Type EventType { get { return typeof (TAggregateEvent); } } + public Type AggregateType => typeof (TAggregate); + public Type EventType => typeof (TAggregateEvent); - public int AggregateSequenceNumber { get; private set; } - public TAggregateEvent AggregateEvent { get; private set; } - public TIdentity AggregateIdentity { get; private set; } - public IMetadata Metadata { get; private set; } - public DateTimeOffset Timestamp { get; private set; } + public int AggregateSequenceNumber { get; } + public TAggregateEvent AggregateEvent { get; } + public TIdentity AggregateIdentity { get; } + public IMetadata Metadata { get; } + public DateTimeOffset Timestamp { get; } public DomainEvent( TAggregateEvent aggregateEvent, @@ -45,11 +45,11 @@ public DomainEvent( TIdentity aggregateIdentity, int aggregateSequenceNumber) { - if (aggregateEvent == null) throw new ArgumentNullException("aggregateEvent"); - if (metadata == null) throw new ArgumentNullException("metadata"); - if (timestamp == default(DateTimeOffset)) throw new ArgumentNullException("timestamp"); - if (aggregateIdentity == null || string.IsNullOrEmpty(aggregateIdentity.Value)) throw new ArgumentNullException("aggregateIdentity"); - if (aggregateSequenceNumber <= 0) throw new ArgumentOutOfRangeException("aggregateSequenceNumber"); + if (aggregateEvent == null) throw new ArgumentNullException(nameof(aggregateEvent)); + if (metadata == null) throw new ArgumentNullException(nameof(metadata)); + if (timestamp == default(DateTimeOffset)) throw new ArgumentNullException(nameof(timestamp)); + if (aggregateIdentity == null || string.IsNullOrEmpty(aggregateIdentity.Value)) throw new ArgumentNullException(nameof(aggregateIdentity)); + if (aggregateSequenceNumber <= 0) throw new ArgumentOutOfRangeException(nameof(aggregateSequenceNumber)); AggregateEvent = aggregateEvent; Metadata = metadata; @@ -70,12 +70,7 @@ public IAggregateEvent GetAggregateEvent() public override string ToString() { - return string.Format( - "{0} v{1}/{2}:{3}", - AggregateType.Name, - AggregateSequenceNumber, - EventType.Name, - AggregateIdentity); + return $"{AggregateType.Name} v{AggregateSequenceNumber}/{EventType.Name}:{AggregateIdentity}"; } } } diff --git a/Source/EventFlow/Aggregates/Metadata.cs b/Source/EventFlow/Aggregates/Metadata.cs index fa93c81cc..e46d9bc1f 100644 --- a/Source/EventFlow/Aggregates/Metadata.cs +++ b/Source/EventFlow/Aggregates/Metadata.cs @@ -103,7 +103,7 @@ public IMetadata CloneWith(IEnumerable> keyValuePai { if (metadata.ContainsKey(kv.Key)) { - throw new ArgumentException(string.Format("Key '{0}' is already present!", kv.Key)); + throw new ArgumentException($"Key '{kv.Key}' is already present!"); } metadata[kv.Key] = kv.Value; } @@ -112,7 +112,7 @@ public IMetadata CloneWith(IEnumerable> keyValuePai public override string ToString() { - return string.Join(Environment.NewLine, this.Select(kv => string.Format("{0}: {1}", kv.Key, kv.Value))); + return string.Join(Environment.NewLine, this.Select(kv => $"{kv.Key}: {kv.Value}")); } } } diff --git a/Source/EventFlow/Commands/Command.cs b/Source/EventFlow/Commands/Command.cs index 1e4aac143..8eaf2be6a 100644 --- a/Source/EventFlow/Commands/Command.cs +++ b/Source/EventFlow/Commands/Command.cs @@ -28,7 +28,7 @@ public abstract class Command : ICommand where TIdentity : IIdentity { - public TIdentity Id { get; private set; } + public TIdentity Id { get; } protected Command(TIdentity id) { diff --git a/Source/EventFlow/EventFlowOptions.cs b/Source/EventFlow/EventFlowOptions.cs index bfeed1496..3f32b045f 100644 --- a/Source/EventFlow/EventFlowOptions.cs +++ b/Source/EventFlow/EventFlowOptions.cs @@ -39,7 +39,7 @@ namespace EventFlow { public class EventFlowOptions { - public static EventFlowOptions New { get { return new EventFlowOptions(); } } + public static EventFlowOptions New => new EventFlowOptions(); private readonly ConcurrentBag _aggregateEventTypes = new ConcurrentBag(); private readonly EventFlowConfiguration _eventFlowConfiguration = new EventFlowConfiguration(); @@ -66,10 +66,7 @@ public EventFlowOptions AddEvents(IEnumerable aggregateEventTypes) { if (!typeof(IAggregateEvent).IsAssignableFrom(aggregateEventType)) { - throw new ArgumentException(string.Format( - "Type {0} is not a {1}", - aggregateEventType.Name, - typeof(IAggregateEvent).Name)); + throw new ArgumentException($"Type {aggregateEventType.Name} is not a {typeof (IAggregateEvent).Name}"); } _aggregateEventTypes.Add(aggregateEventType); } diff --git a/Source/EventFlow/ValueObjects/SingleValueObject.cs b/Source/EventFlow/ValueObjects/SingleValueObject.cs index 58cb05ab7..cc23763bf 100644 --- a/Source/EventFlow/ValueObjects/SingleValueObject.cs +++ b/Source/EventFlow/ValueObjects/SingleValueObject.cs @@ -28,7 +28,7 @@ namespace EventFlow.ValueObjects public abstract class SingleValueObject : ValueObject, IComparable, ISingleValueObject where T : IComparable, IComparable { - public T Value { get; private set; } + public T Value { get; } protected SingleValueObject(T value) { @@ -39,16 +39,13 @@ public int CompareTo(object obj) { if (ReferenceEquals(null, obj)) { - throw new ArgumentNullException("obj"); + throw new ArgumentNullException(nameof(obj)); } var other = obj as SingleValueObject; if (other == null) { - throw new ArgumentException(string.Format( - "Cannot compare '{0}' and '{1}'", - GetType().Name, - obj.GetType().Name)); + throw new ArgumentException($"Cannot compare '{GetType().Name}' and '{obj.GetType().Name}'"); } return Value.CompareTo(other.Value); diff --git a/Source/EventFlow/ValueObjects/ValueObject.cs b/Source/EventFlow/ValueObjects/ValueObject.cs index 840beee6a..24d4f1e8b 100644 --- a/Source/EventFlow/ValueObjects/ValueObject.cs +++ b/Source/EventFlow/ValueObjects/ValueObject.cs @@ -64,7 +64,7 @@ public override string ToString() return string.Format( "{{{0}{1}}}", Environment.NewLine, - string.Join(", ", GetProperties().Select(f => string.Format(" {0}: '{1}'{2}", f.Name, f.GetValue(this), Environment.NewLine)))); + string.Join(", ", GetProperties().Select(f => $" {f.Name}: '{f.GetValue(this)}'{Environment.NewLine}"))); } protected virtual IEnumerable GetEqualityComponents() From 1b1abd0cf996234dde6fa9878f83cce88c188375 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 18 Aug 2015 20:37:33 +0200 Subject: [PATCH 12/54] Got basic RabbitMQ publish working, still needs a lot of work though --- EventFlow.sln | 7 ++ .../EventFlow.RabbitMQ.Tests.csproj | 87 +++++++++++++ .../Integration/RabbitMqTests.cs | 62 ++++++++++ .../Properties/AssemblyInfo.cs | 36 ++++++ .../RabbitMqConsumer.cs | 114 ++++++++++++++++++ .../EventFlow.RabbitMQ.Tests/packages.config | 6 + .../EventFlow.RabbitMQ.csproj | 9 +- .../Integrations/RabbitMqMessageFactory.cs | 4 +- .../Integrations/RabbitMqPublisher.cs | 4 +- .../EventStores/EventJsonSerializer.cs | 12 +- .../EventFlowOptionsDefaultExtensions.cs | 1 + 11 files changed, 333 insertions(+), 9 deletions(-) create mode 100644 Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj create mode 100644 Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs create mode 100644 Source/EventFlow.RabbitMQ.Tests/Properties/AssemblyInfo.cs create mode 100644 Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs create mode 100644 Source/EventFlow.RabbitMQ.Tests/packages.config diff --git a/EventFlow.sln b/EventFlow.sln index 1adf4b815..ba7bc05fd 100644 --- a/EventFlow.sln +++ b/EventFlow.sln @@ -37,6 +37,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RabbitMQ", "RabbitMQ", "{79 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.RabbitMQ", "Source\EventFlow.RabbitMQ\EventFlow.RabbitMQ.csproj", "{4B06F01F-ACE6-489D-A92A-012F533EFA3C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.RabbitMQ.Tests", "Source\EventFlow.RabbitMQ.Tests\EventFlow.RabbitMQ.Tests.csproj", "{BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +97,10 @@ Global {4B06F01F-ACE6-489D-A92A-012F533EFA3C}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B06F01F-ACE6-489D-A92A-012F533EFA3C}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B06F01F-ACE6-489D-A92A-012F533EFA3C}.Release|Any CPU.Build.0 = Release|Any CPU + {BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -109,5 +115,6 @@ Global {E42A253D-2011-4799-B55D-1D0C61E171C2} = {F6D62A27-50EA-4846-8F36-F3D36F52DCA6} {BC4F0E41-6659-4D6D-9D25-1558CBA1649B} = {F6D62A27-50EA-4846-8F36-F3D36F52DCA6} {4B06F01F-ACE6-489D-A92A-012F533EFA3C} = {7951DC73-5DAF-4322-9AF0-099BF5C90837} + {BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A} = {7951DC73-5DAF-4322-9AF0-099BF5C90837} EndGlobalSection EndGlobal diff --git a/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj b/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj new file mode 100644 index 000000000..4fb5fe658 --- /dev/null +++ b/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj @@ -0,0 +1,87 @@ + + + + + Debug + AnyCPU + {BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A} + Library + Properties + EventFlow.RabbitMQ.Tests + EventFlow.RabbitMQ.Tests + v4.5.1 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + + ..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\NUnit.2.6.4\lib\nunit.framework.dll + True + + + ..\..\packages\RabbitMQ.Client.3.5.4\lib\net40\RabbitMQ.Client.dll + True + + + + + + + + + + + + + + + + + + + + + {4b06f01f-ace6-489d-a92a-012f533efa3c} + EventFlow.RabbitMQ + + + {571D291C-5E4C-43AF-855F-7C4E2F318F4C} + EventFlow.TestHelpers + + + {11131251-778d-4d2e-bdd1-4844a789bca9} + EventFlow + + + + + \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs new file mode 100644 index 000000000..52b6df8be --- /dev/null +++ b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs @@ -0,0 +1,62 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; +using System.Linq; +using System.Text; +using System.Threading; +using EventFlow.Extensions; +using EventFlow.RabbitMQ.Extensions; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.Aggregates.Test; +using EventFlow.TestHelpers.Aggregates.Test.Commands; +using EventFlow.TestHelpers.Aggregates.Test.ValueObjects; +using NUnit.Framework; + +namespace EventFlow.RabbitMQ.Tests.Integration +{ + public class RabbitMqTests + { + [Test, Explicit("Needs RabbitMQ running localhost (https://github.com/rasmus/Vagrant.Boxes)")] + public void Test() + { + var uri = new Uri("amqp://localhost"); + using (var consumer = new RabbitMqConsumer(uri, "eventflow", new[] {"#"})) + { + var resolver = EventFlowOptions.New + .PublishToRabbitMq(RabbitMqConfiguration.With(uri)) + .AddDefaults(EventFlowTestHelpers.Assembly) + .CreateResolver(false); + + var commandBus = resolver.Resolve(); + + commandBus.Publish(new PingCommand(TestId.New, PingId.New), CancellationToken.None); + + var rabbitMqMessage = consumer.GetMessages().Single(); + var json = Encoding.UTF8.GetString(rabbitMqMessage.Message); + Console.WriteLine(json); + + // TODO: Real testing + } + } + } +} diff --git a/Source/EventFlow.RabbitMQ.Tests/Properties/AssemblyInfo.cs b/Source/EventFlow.RabbitMQ.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..87cbbc1c9 --- /dev/null +++ b/Source/EventFlow.RabbitMQ.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("EventFlow.RabbitMQ.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("EventFlow.RabbitMQ.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("bc96beae-e84e-4c51-b66d-da1f43ead54a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs b/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs new file mode 100644 index 000000000..cb2866a51 --- /dev/null +++ b/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs @@ -0,0 +1,114 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using EventFlow.RabbitMQ.Integrations; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace EventFlow.RabbitMQ.Tests +{ + public class RabbitMqConsumer : IDisposable + { + private readonly IConnection _connection; + private readonly IModel _model; + private readonly EventingBasicConsumer _eventingBasicConsumer; + private readonly List _receivedMessages = new List(); + private readonly AutoResetEvent _autoResetEvent = new AutoResetEvent(false); + + public RabbitMqConsumer(Uri uri, string exchange, IEnumerable routingKeys) + { + var connectionFactory = new ConnectionFactory + { + Uri = uri.ToString(), + }; + _connection = connectionFactory.CreateConnection(); + _model = _connection.CreateModel(); + + var queueName = string.Format("test-{0}", Guid.NewGuid()); + _model.QueueDeclare( + queueName, + false, + false, + true, + null); + + foreach (var routingKey in routingKeys) + { + _model.QueueBind( + queueName, + exchange, + routingKey, + null); + } + + _eventingBasicConsumer = new EventingBasicConsumer(_model); + _eventingBasicConsumer.Received += OnReceived; + + _model.BasicConsume(queueName, false, _eventingBasicConsumer); + } + + private void OnReceived(object sender, BasicDeliverEventArgs basicDeliverEventArgs) + { + Console.WriteLine("Received message: {0}", basicDeliverEventArgs.RoutingKey); + + lock (_receivedMessages) + { + _receivedMessages.Add(basicDeliverEventArgs); + _autoResetEvent.Set(); + } + } + + public IReadOnlyCollection GetMessages(int count = 1) + { + while (true) + { + _autoResetEvent.WaitOne(); + lock (_receivedMessages) + { + if (_receivedMessages.Count >= count) + { + var basicDeliverEventArgses =_receivedMessages.GetRange(0, count); + _receivedMessages.RemoveRange(0, count); + return basicDeliverEventArgses.Select(CreateRabbitMqMessage).ToList(); + } + } + } + } + + private static RabbitMqMessage CreateRabbitMqMessage(BasicDeliverEventArgs basicDeliverEventArgs) + { + var headers = new Dictionary(); // TODO: Get real headers + return new RabbitMqMessage(basicDeliverEventArgs.Body, headers, basicDeliverEventArgs.RoutingKey); + } + + public void Dispose() + { + _eventingBasicConsumer.Received -= OnReceived; + _model.Dispose(); + _connection.Dispose(); + } + } +} diff --git a/Source/EventFlow.RabbitMQ.Tests/packages.config b/Source/EventFlow.RabbitMQ.Tests/packages.config new file mode 100644 index 000000000..4b1637d76 --- /dev/null +++ b/Source/EventFlow.RabbitMQ.Tests/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj index 7b41d1c2c..a42f75d12 100644 --- a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj +++ b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj @@ -9,8 +9,9 @@ Properties EventFlow.RabbitMQ EventFlow.RabbitMQ - v4.5.2 + v4.5.1 512 + true @@ -20,6 +21,7 @@ DEBUG;TRACE prompt 4 + true pdbonly @@ -28,6 +30,7 @@ TRACE prompt 4 + true @@ -63,6 +66,10 @@ + + {571d291c-5e4c-43af-855f-7c4e2f318f4c} + EventFlow.TestHelpers + {11131251-778d-4d2e-bdd1-4844a789bca9} EventFlow diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs index 296b8a1e7..9c66afd8c 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs @@ -46,10 +46,12 @@ public RabbitMqMessage CreateMessage(IDomainEvent domainEvent) var serializedEvent = _eventJsonSerializer.Serialize( domainEvent.GetAggregateEvent(), - Enumerable.Empty>()); + domainEvent.Metadata); var message = Encoding.UTF8.GetBytes(serializedEvent.SerializedData); + // TODO: Add aggregate name to routing key + var routingKey = string.Format( "eventflow.domainevent.{0}.{1}", domainEvent.Metadata.EventName, diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index a6395764c..1a00346fc 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -74,11 +74,11 @@ private async Task PublishAsync(IEnumerable messages, Canc foreach (var message in messages) { var basicProperties = model.CreateBasicProperties(); - basicProperties.Headers = message.Headers.ToDictionary(kv => kv.Value, kv => (object)kv.Value); + basicProperties.Headers = message.Headers.ToDictionary(kv => kv.Key, kv => (object)kv.Value); basicProperties.Persistent = true; // TODO: get from config basicProperties.Timestamp = new AmqpTimestamp(DateTimeOffset.Now.ToUnixTime()); - model.BasicPublish("get from config", "get from config", false, false, basicProperties, message.Message); + model.BasicPublish("eventflow", message.RoutingKey, false, false, basicProperties, message.Message); } } diff --git a/Source/EventFlow/EventStores/EventJsonSerializer.cs b/Source/EventFlow/EventStores/EventJsonSerializer.cs index 1b898c38f..d7de4e03f 100644 --- a/Source/EventFlow/EventStores/EventJsonSerializer.cs +++ b/Source/EventFlow/EventStores/EventJsonSerializer.cs @@ -48,11 +48,13 @@ public SerializedEvent Serialize(IAggregateEvent aggregateEvent, IEnumerable(MetadataKeys.EventName, eventDefinition.Name), - new KeyValuePair(MetadataKeys.EventVersion, eventDefinition.Version.ToString(CultureInfo.InvariantCulture)), - })); + var metadata = new Metadata(metadatas + .Where(kv => kv.Key != MetadataKeys.EventName && kv.Key != MetadataKeys.EventVersion) // TODO: Fix this + .Concat(new[] + { + new KeyValuePair(MetadataKeys.EventName, eventDefinition.Name), + new KeyValuePair(MetadataKeys.EventVersion, eventDefinition.Version.ToString(CultureInfo.InvariantCulture)), + })); var dataJson = _jsonSerializer.Serialize(aggregateEvent); var metaJson = _jsonSerializer.Serialize(metadata); diff --git a/Source/EventFlow/Extensions/EventFlowOptionsDefaultExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsDefaultExtensions.cs index 3fa0923dc..27e2a2ad7 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsDefaultExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsDefaultExtensions.cs @@ -31,6 +31,7 @@ public static EventFlowOptions AddDefaults( Assembly fromAssembly) { return eventFlowOptions + .AddEvents(fromAssembly) .AddCommandHandlers(fromAssembly) .AddMetadataProviders(fromAssembly) .AddSubscribers(fromAssembly) From 34b46f153fd556b5f9be46529a897bd6a8d49ef3 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 18 Aug 2015 20:39:28 +0200 Subject: [PATCH 13/54] How did I miss this... --- RELEASE_NOTES.md | 3 ++- .../EventFlow/Extensions/EventFlowOptionsDefaultExtensions.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a8bd05d6d..748145f18 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,7 @@ ### New in 0.11 (not released yet) - * _Nothing yet_ + * Breaking: `EventFlowOptions AddDefaults(...)` now also adds event + definitions ### New in 0.10.642 (released 2015-08-17) diff --git a/Source/EventFlow/Extensions/EventFlowOptionsDefaultExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsDefaultExtensions.cs index 3fa0923dc..27e2a2ad7 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsDefaultExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsDefaultExtensions.cs @@ -31,6 +31,7 @@ public static EventFlowOptions AddDefaults( Assembly fromAssembly) { return eventFlowOptions + .AddEvents(fromAssembly) .AddCommandHandlers(fromAssembly) .AddMetadataProviders(fromAssembly) .AddSubscribers(fromAssembly) From 8c9717db696a62d38fbcba3b923570862d06636a Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 18 Aug 2015 23:04:02 +0200 Subject: [PATCH 14/54] Domain event published through RabbitMQ --- .../EventFlow.RabbitMQ.Tests.csproj | 9 ++++++++ .../Integration/RabbitMqTests.cs | 21 ++++++++++++++++--- .../RabbitMqConsumer.cs | 4 +++- Source/EventFlow.RabbitMQ.Tests/app.config | 11 ++++++++++ .../EventFlow.RabbitMQ.Tests/packages.config | 1 + .../Integrations/RabbitMqMessageFactory.cs | 8 +------ Source/EventFlow/Aggregates/AggregateRoot.cs | 1 + Source/EventFlow/Aggregates/IMetadata.cs | 1 + Source/EventFlow/Aggregates/Metadata.cs | 6 ++++++ Source/EventFlow/Aggregates/MetadataKeys.cs | 1 + .../EventStores/EventJsonSerializer.cs | 16 ++++++++------ .../EventStores/IEventJsonSerializer.cs | 2 ++ 12 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 Source/EventFlow.RabbitMQ.Tests/app.config diff --git a/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj b/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj index 4fb5fe658..cf115af1a 100644 --- a/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj +++ b/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj @@ -33,6 +33,14 @@ true + + ..\..\packages\FluentAssertions.3.5.0\lib\net45\FluentAssertions.dll + True + + + ..\..\packages\FluentAssertions.3.5.0\lib\net45\FluentAssertions.Core.dll + True + ..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll True @@ -60,6 +68,7 @@ + diff --git a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs index 52b6df8be..be09230ce 100644 --- a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs +++ b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs @@ -24,18 +24,30 @@ using System.Linq; using System.Text; using System.Threading; +using EventFlow.Aggregates; +using EventFlow.EventStores; using EventFlow.Extensions; using EventFlow.RabbitMQ.Extensions; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates.Test; using EventFlow.TestHelpers.Aggregates.Test.Commands; +using EventFlow.TestHelpers.Aggregates.Test.Events; using EventFlow.TestHelpers.Aggregates.Test.ValueObjects; +using FluentAssertions; using NUnit.Framework; namespace EventFlow.RabbitMQ.Tests.Integration { public class RabbitMqTests { + public class RabbitMqCommittedEvent : ICommittedDomainEvent + { + public string AggregateId { get; set; } + public string Data { get; set; } + public string Metadata { get; set; } + public int AggregateSequenceNumber { get; set; } + } + [Test, Explicit("Needs RabbitMQ running localhost (https://github.com/rasmus/Vagrant.Boxes)")] public void Test() { @@ -48,14 +60,17 @@ public void Test() .CreateResolver(false); var commandBus = resolver.Resolve(); + var eventJsonSerializer = resolver.Resolve(); - commandBus.Publish(new PingCommand(TestId.New, PingId.New), CancellationToken.None); + var pingId = PingId.New; + commandBus.Publish(new PingCommand(TestId.New, pingId), CancellationToken.None); var rabbitMqMessage = consumer.GetMessages().Single(); var json = Encoding.UTF8.GetString(rabbitMqMessage.Message); - Console.WriteLine(json); + + var pingEvent = (IDomainEvent) eventJsonSerializer.Deserialize(json, new Metadata(rabbitMqMessage.Headers)); - // TODO: Real testing + pingEvent.AggregateEvent.PingId.Should().Be(pingId); } } } diff --git a/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs b/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs index cb2866a51..e814dc532 100644 --- a/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs +++ b/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs @@ -23,6 +23,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; using EventFlow.RabbitMQ.Integrations; using RabbitMQ.Client; @@ -100,7 +101,8 @@ public IReadOnlyCollection GetMessages(int count = 1) private static RabbitMqMessage CreateRabbitMqMessage(BasicDeliverEventArgs basicDeliverEventArgs) { - var headers = new Dictionary(); // TODO: Get real headers + var headers = basicDeliverEventArgs.BasicProperties.Headers + .ToDictionary(kv => kv.Key, kv => Encoding.UTF8.GetString((byte[])kv.Value)); return new RabbitMqMessage(basicDeliverEventArgs.Body, headers, basicDeliverEventArgs.RoutingKey); } diff --git a/Source/EventFlow.RabbitMQ.Tests/app.config b/Source/EventFlow.RabbitMQ.Tests/app.config new file mode 100644 index 000000000..7afb9c361 --- /dev/null +++ b/Source/EventFlow.RabbitMQ.Tests/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ.Tests/packages.config b/Source/EventFlow.RabbitMQ.Tests/packages.config index 4b1637d76..1d5c74d22 100644 --- a/Source/EventFlow.RabbitMQ.Tests/packages.config +++ b/Source/EventFlow.RabbitMQ.Tests/packages.config @@ -1,5 +1,6 @@  + diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs index 9c66afd8c..1c3d73067 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs @@ -20,8 +20,6 @@ // 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.Collections.Generic; -using System.Linq; using System.Text; using EventFlow.Aggregates; using EventFlow.EventStores; @@ -40,10 +38,6 @@ public RabbitMqMessageFactory( public RabbitMqMessage CreateMessage(IDomainEvent domainEvent) { - var headers = domainEvent.Metadata - .ToDictionary(kv => string.Format("eventflow-metadata-{0}", kv.Key), kv => kv.Value); - headers.Add("eventflow-encoding", "utf8"); - var serializedEvent = _eventJsonSerializer.Serialize( domainEvent.GetAggregateEvent(), domainEvent.Metadata); @@ -57,7 +51,7 @@ public RabbitMqMessage CreateMessage(IDomainEvent domainEvent) domainEvent.Metadata.EventName, domainEvent.Metadata.EventVersion); - return new RabbitMqMessage(message, headers, routingKey); + return new RabbitMqMessage(message, domainEvent.Metadata, routingKey); } } } diff --git a/Source/EventFlow/Aggregates/AggregateRoot.cs b/Source/EventFlow/Aggregates/AggregateRoot.cs index 3bd1eb6e3..819b46d41 100644 --- a/Source/EventFlow/Aggregates/AggregateRoot.cs +++ b/Source/EventFlow/Aggregates/AggregateRoot.cs @@ -74,6 +74,7 @@ protected virtual void Emit(TEvent aggregateEvent, IMetadata metadata = {MetadataKeys.TimestampEpoch, now.ToUnixTime().ToString()}, {MetadataKeys.AggregateSequenceNumber, (Version + 1).ToString()}, {MetadataKeys.AggregateName, GetType().Name.Replace("Aggregate", string.Empty)}, + {MetadataKeys.AggregateId, Id.Value} }; metadata = metadata == null diff --git a/Source/EventFlow/Aggregates/IMetadata.cs b/Source/EventFlow/Aggregates/IMetadata.cs index 9507166ce..f45794ea7 100644 --- a/Source/EventFlow/Aggregates/IMetadata.cs +++ b/Source/EventFlow/Aggregates/IMetadata.cs @@ -32,6 +32,7 @@ public interface IMetadata : IReadOnlyDictionary DateTimeOffset Timestamp { get; } long TimestampEpoch { get; } int AggregateSequenceNumber { get; } + string AggregateId { get; } IMetadata CloneWith(IEnumerable> keyValuePairs); } diff --git a/Source/EventFlow/Aggregates/Metadata.cs b/Source/EventFlow/Aggregates/Metadata.cs index fa93c81cc..eeea496b7 100644 --- a/Source/EventFlow/Aggregates/Metadata.cs +++ b/Source/EventFlow/Aggregates/Metadata.cs @@ -79,6 +79,12 @@ public int AggregateSequenceNumber set { this[MetadataKeys.AggregateSequenceNumber] = value.ToString(); } } + public string AggregateId + { + get { return this[MetadataKeys.AggregateId]; } + set { this[MetadataKeys.AggregateId] = value; } + } + public Metadata() { } public Metadata(IDictionary keyValuePairs) diff --git a/Source/EventFlow/Aggregates/MetadataKeys.cs b/Source/EventFlow/Aggregates/MetadataKeys.cs index 5d6d14927..ff6fc9db1 100644 --- a/Source/EventFlow/Aggregates/MetadataKeys.cs +++ b/Source/EventFlow/Aggregates/MetadataKeys.cs @@ -31,5 +31,6 @@ public sealed class MetadataKeys public const string TimestampEpoch = "timestamp_epoch"; public const string AggregateSequenceNumber = "aggregate_sequence_number"; public const string AggregateName = "aggregate_name"; + public const string AggregateId = "aggregate_id"; } } diff --git a/Source/EventFlow/EventStores/EventJsonSerializer.cs b/Source/EventFlow/EventStores/EventJsonSerializer.cs index d7de4e03f..22d57922e 100644 --- a/Source/EventFlow/EventStores/EventJsonSerializer.cs +++ b/Source/EventFlow/EventStores/EventJsonSerializer.cs @@ -66,25 +66,29 @@ public SerializedEvent Serialize(IAggregateEvent aggregateEvent, IEnumerable(committedDomainEvent.Metadata); - var eventDefinition = _eventDefinitionService.GetEventDefinition( metadata.EventName, metadata.EventVersion); - var aggregateEvent = (IAggregateEvent)_jsonSerializer.Deserialize(committedDomainEvent.Data, eventDefinition.Type); + var aggregateEvent = (IAggregateEvent)_jsonSerializer.Deserialize(json, eventDefinition.Type); var domainEvent = _domainEventFactory.Create( aggregateEvent, metadata, - committedDomainEvent.AggregateId, - committedDomainEvent.AggregateSequenceNumber); + metadata.AggregateId, + metadata.AggregateSequenceNumber); return domainEvent; } + public IDomainEvent Deserialize(ICommittedDomainEvent committedDomainEvent) + { + var metadata = (IMetadata)_jsonSerializer.Deserialize(committedDomainEvent.Metadata); + return Deserialize(committedDomainEvent.Data, metadata); + } + public IDomainEvent Deserialize( TIdentity id, ICommittedDomainEvent committedDomainEvent) diff --git a/Source/EventFlow/EventStores/IEventJsonSerializer.cs b/Source/EventFlow/EventStores/IEventJsonSerializer.cs index 9f7ad97bb..033d7b3b4 100644 --- a/Source/EventFlow/EventStores/IEventJsonSerializer.cs +++ b/Source/EventFlow/EventStores/IEventJsonSerializer.cs @@ -31,6 +31,8 @@ SerializedEvent Serialize( IAggregateEvent aggregateEvent, IEnumerable> metadatas); + IDomainEvent Deserialize(string json, IMetadata metadata); + IDomainEvent Deserialize( ICommittedDomainEvent committedDomainEvent); From b6b7e17fc63abbfcad8ba3587c27cd4263cf5cdd Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 18 Aug 2015 23:06:21 +0200 Subject: [PATCH 15/54] Remove properties from interface as they are no longer needed --- Source/EventFlow/EventStores/ICommittedDomainEvent.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs index 9ac27c087..557627a33 100644 --- a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs +++ b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs @@ -24,9 +24,7 @@ namespace EventFlow.EventStores { public interface ICommittedDomainEvent { - string AggregateId { get; set; } string Data { get; set; } string Metadata { get; set; } - int AggregateSequenceNumber { get; set; } } } From c849ad9a25906e3442615c5097d5f53ca86b74c2 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 18 Aug 2015 23:14:52 +0200 Subject: [PATCH 16/54] Added EventFlow.RabbitMQ NuGet package generation to the build --- .../EventFlow.RabbitMQ.csproj | 1 + .../EventFlow.RabbitMQ.nuspec | 23 +++++++++++++++++++ build.fsx | 17 ++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.nuspec diff --git a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj index a42f75d12..96f744fd3 100644 --- a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj +++ b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj @@ -76,6 +76,7 @@ + diff --git a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.nuspec b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.nuspec new file mode 100644 index 000000000..36f763fc8 --- /dev/null +++ b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.nuspec @@ -0,0 +1,23 @@ + + + + EventFlow.RabbitMQ + EventFlow - RabbitMQ integration + 0.0.0 + rasmus + RabbitMQ integration for EventFlow + en-US + https://raw.githubusercontent.com/rasmus/EventFlow/master/icon-256.png + https://github.com/rasmus/EventFlow + https://raw.githubusercontent.com/rasmus/EventFlow/master/LICENSE + Copyright (c) 2015 Rasmus Mikkelsen + true + CQRS ES event sourceing eventstore rabbitmq + @releaseNotes@ + @dependencies@ + @references@ + + + + + diff --git a/build.fsx b/build.fsx index e2205c9c2..b3b67fdf0 100644 --- a/build.fsx +++ b/build.fsx @@ -82,6 +82,22 @@ Target "CreatePackageEventFlowAutofac" (fun _ -> "Source/EventFlow.Autofac/EventFlow.Autofac.nuspec" ) +Target "CreatePackageEventFlowRabbitMQ" (fun _ -> + let binDir = "Source/EventFlow.RabbitMQ/bin/" + CopyFile binDir (binDir + buildMode + "/EventFlow.RabbitMQ.dll") + NuGet (fun p -> + {p with + OutputPath = dirPackages + WorkingDir = "Source/EventFlow.RabbitMQ" + Version = nugetVersion + ReleaseNotes = toLines releaseNotes.Notes + Dependencies = [ + "EventFlow", nugetVersionDep + "RabbitMQ.Client", GetPackageVersion "./packages/" "RabbitMQ.Client"] + Publish = false }) + "Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.nuspec" + ) + Target "CreatePackageEventFlowMsSql" (fun _ -> let binDir = "Source\\EventFlow.MsSql\\bin\\" + buildMode + "\\" let result = ExecProcess (fun info -> @@ -173,6 +189,7 @@ Target "Default" DoNothing ==> "UnitTest" ==> "CreatePackageEventFlow" ==> "CreatePackageEventFlowAutofac" + ==> "CreatePackageEventFlowRabbitMQ" ==> "CreatePackageEventFlowMsSql" ==> "CreatePackageEventFlowEventStoresMsSql" ==> "CreatePackageEventFlowReadStoresMsSql" From 56e0ae18b8bf394d9f223ddb69f01bb70e6f03fa Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 18 Aug 2015 23:26:26 +0200 Subject: [PATCH 17/54] Include aggregate name in routing key --- .../Integrations/RabbitMqMessageFactory.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs index 1c3d73067..61b89d4c0 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs @@ -47,8 +47,9 @@ public RabbitMqMessage CreateMessage(IDomainEvent domainEvent) // TODO: Add aggregate name to routing key var routingKey = string.Format( - "eventflow.domainevent.{0}.{1}", - domainEvent.Metadata.EventName, + "eventflow.domainevent.{0}.{1}.{2}", + domainEvent.Metadata[MetadataKeys.AggregateName].ToLowerInvariant(), // TODO: Transform from "MyAgg" to "my-agg" + domainEvent.Metadata.EventName.ToLowerInvariant(), // TODO: Transform from "MyEvnt" to "my-evnt" domainEvent.Metadata.EventVersion); return new RabbitMqMessage(message, domainEvent.Metadata, routingKey); From c92da2f6b22af895e5382efe69a48196cc17d83a Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 18 Aug 2015 23:27:39 +0200 Subject: [PATCH 18/54] More RabbitMQ properties set --- Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs | 1 + Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs | 5 ++++- Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs b/Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs index 6cfe308fe..9633251e5 100644 --- a/Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs +++ b/Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs @@ -27,5 +27,6 @@ namespace EventFlow.RabbitMQ public interface IRabbitMqConfiguration { Uri Uri { get; } + bool Persistent { get; } } } \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index 1a00346fc..cba7801e8 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -75,8 +75,11 @@ private async Task PublishAsync(IEnumerable messages, Canc { var basicProperties = model.CreateBasicProperties(); basicProperties.Headers = message.Headers.ToDictionary(kv => kv.Key, kv => (object)kv.Value); - basicProperties.Persistent = true; // TODO: get from config + basicProperties.Persistent = _configuration.Persistent; basicProperties.Timestamp = new AmqpTimestamp(DateTimeOffset.Now.ToUnixTime()); + basicProperties.ContentEncoding = "utf-8"; + basicProperties.ContentType = "application/json"; + basicProperties.MessageId = message.Headers[MetadataKeys.AggregateId]; model.BasicPublish("eventflow", message.RoutingKey, false, false, basicProperties, message.Message); } diff --git a/Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs b/Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs index abd54b8ab..1d66133d1 100644 --- a/Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs +++ b/Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs @@ -27,6 +27,7 @@ namespace EventFlow.RabbitMQ public class RabbitMqConfiguration : IRabbitMqConfiguration { public Uri Uri { get; } + public bool Persistent { get { return true; } } public static IRabbitMqConfiguration With(Uri uri) { From 438582d168bc3e31ce25b886e3695a065012d607 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 19 Aug 2015 19:27:37 +0200 Subject: [PATCH 19/54] Added exchange and routing key value objects --- .../Integration/RabbitMqTests.cs | 6 ++-- .../RabbitMqConsumer.cs | 10 +++++- .../EventFlow.RabbitMQ.csproj | 2 ++ Source/EventFlow.RabbitMQ/Exchange.cs | 33 +++++++++++++++++++ .../Integrations/RabbitMqMessage.cs | 13 +++++--- .../Integrations/RabbitMqMessageFactory.cs | 16 +++++---- .../Integrations/RabbitMqPublisher.cs | 5 ++- Source/EventFlow.RabbitMQ/RoutingKey.cs | 33 +++++++++++++++++++ 8 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 Source/EventFlow.RabbitMQ/Exchange.cs create mode 100644 Source/EventFlow.RabbitMQ/RoutingKey.cs diff --git a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs index be09230ce..0ad47a444 100644 --- a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs +++ b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs @@ -22,7 +22,6 @@ using System; using System.Linq; -using System.Text; using System.Threading; using EventFlow.Aggregates; using EventFlow.EventStores; @@ -66,9 +65,10 @@ public void Test() commandBus.Publish(new PingCommand(TestId.New, pingId), CancellationToken.None); var rabbitMqMessage = consumer.GetMessages().Single(); - var json = Encoding.UTF8.GetString(rabbitMqMessage.Message); - var pingEvent = (IDomainEvent) eventJsonSerializer.Deserialize(json, new Metadata(rabbitMqMessage.Headers)); + var pingEvent = (IDomainEvent) eventJsonSerializer.Deserialize( + rabbitMqMessage.Message, + new Metadata(rabbitMqMessage.Headers)); pingEvent.AggregateEvent.PingId.Should().Be(pingId); } diff --git a/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs b/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs index e814dc532..40d9bb2b3 100644 --- a/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs +++ b/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs @@ -48,6 +48,8 @@ public RabbitMqConsumer(Uri uri, string exchange, IEnumerable routingKey _connection = connectionFactory.CreateConnection(); _model = _connection.CreateModel(); + _model.ExchangeDeclare(exchange, ExchangeType.Topic, false); + var queueName = string.Format("test-{0}", Guid.NewGuid()); _model.QueueDeclare( queueName, @@ -103,7 +105,13 @@ private static RabbitMqMessage CreateRabbitMqMessage(BasicDeliverEventArgs basic { var headers = basicDeliverEventArgs.BasicProperties.Headers .ToDictionary(kv => kv.Key, kv => Encoding.UTF8.GetString((byte[])kv.Value)); - return new RabbitMqMessage(basicDeliverEventArgs.Body, headers, basicDeliverEventArgs.RoutingKey); + var message = Encoding.UTF8.GetString(basicDeliverEventArgs.Body); + + return new RabbitMqMessage( + message, + headers, + new Exchange(basicDeliverEventArgs.Exchange), + new RoutingKey(basicDeliverEventArgs.RoutingKey)); } public void Dispose() diff --git a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj index 96f744fd3..03480d68f 100644 --- a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj +++ b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj @@ -50,6 +50,7 @@ Properties\SolutionInfo.cs + @@ -64,6 +65,7 @@ + diff --git a/Source/EventFlow.RabbitMQ/Exchange.cs b/Source/EventFlow.RabbitMQ/Exchange.cs new file mode 100644 index 000000000..b8c5c777c --- /dev/null +++ b/Source/EventFlow.RabbitMQ/Exchange.cs @@ -0,0 +1,33 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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 EventFlow.ValueObjects; + +namespace EventFlow.RabbitMQ +{ + public class Exchange : SingleValueObject + { + public Exchange(string value) : base(value) + { + } + } +} diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs index 72308c5a8..edebe404c 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs @@ -26,17 +26,20 @@ namespace EventFlow.RabbitMQ.Integrations { public class RabbitMqMessage { - public byte[] Message { get; private set; } - public IReadOnlyDictionary Headers { get; private set; } - public string RoutingKey { get; private set; } + public string Message { get; } + public IReadOnlyDictionary Headers { get; } + public Exchange Exchange { get; } + public RoutingKey RoutingKey { get; } public RabbitMqMessage( - byte[] message, + string message, IReadOnlyDictionary headers, - string routingKey) + Exchange exchange, + RoutingKey routingKey) { Message = message; Headers = headers; + Exchange = exchange; RoutingKey = routingKey; } } diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs index 61b89d4c0..bb9d709f2 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs @@ -20,7 +20,6 @@ // 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.Text; using EventFlow.Aggregates; using EventFlow.EventStores; @@ -42,17 +41,20 @@ public RabbitMqMessage CreateMessage(IDomainEvent domainEvent) domainEvent.GetAggregateEvent(), domainEvent.Metadata); - var message = Encoding.UTF8.GetBytes(serializedEvent.SerializedData); - // TODO: Add aggregate name to routing key - var routingKey = string.Format( + var routingKey = new RoutingKey(string.Format( "eventflow.domainevent.{0}.{1}.{2}", domainEvent.Metadata[MetadataKeys.AggregateName].ToLowerInvariant(), // TODO: Transform from "MyAgg" to "my-agg" domainEvent.Metadata.EventName.ToLowerInvariant(), // TODO: Transform from "MyEvnt" to "my-evnt" - domainEvent.Metadata.EventVersion); - - return new RabbitMqMessage(message, domainEvent.Metadata, routingKey); + domainEvent.Metadata.EventVersion)); + var exchange = new Exchange("eventflow"); + + return new RabbitMqMessage( + serializedEvent.SerializedData, + domainEvent.Metadata, + exchange, + routingKey); } } } diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index cba7801e8..3ede702f1 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -23,6 +23,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -73,6 +74,8 @@ private async Task PublishAsync(IEnumerable messages, Canc { foreach (var message in messages) { + var bytes = Encoding.UTF8.GetBytes(message.Message); + var basicProperties = model.CreateBasicProperties(); basicProperties.Headers = message.Headers.ToDictionary(kv => kv.Key, kv => (object)kv.Value); basicProperties.Persistent = _configuration.Persistent; @@ -81,7 +84,7 @@ private async Task PublishAsync(IEnumerable messages, Canc basicProperties.ContentType = "application/json"; basicProperties.MessageId = message.Headers[MetadataKeys.AggregateId]; - model.BasicPublish("eventflow", message.RoutingKey, false, false, basicProperties, message.Message); + model.BasicPublish(message.Exchange.Value, message.RoutingKey.Value, false, false, basicProperties, bytes); } } diff --git a/Source/EventFlow.RabbitMQ/RoutingKey.cs b/Source/EventFlow.RabbitMQ/RoutingKey.cs new file mode 100644 index 000000000..800c1ca68 --- /dev/null +++ b/Source/EventFlow.RabbitMQ/RoutingKey.cs @@ -0,0 +1,33 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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 EventFlow.ValueObjects; + +namespace EventFlow.RabbitMQ +{ + public class RoutingKey : SingleValueObject + { + public RoutingKey(string value) : base(value) + { + } + } +} From 49f997eaf24be19ef9e6f27cd6c19c4f2192fae4 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 19 Aug 2015 19:51:55 +0200 Subject: [PATCH 20/54] Produce a better routing key --- .../Integration/RabbitMqTests.cs | 2 + .../Integrations/RabbitMqMessageFactory.cs | 5 ++- Source/EventFlow.Tests/EventFlow.Tests.csproj | 1 + .../Extensions/StringExtensionsTests.cs | 39 +++++++++++++++++++ Source/EventFlow/EventFlow.csproj | 1 + .../EventFlow/Extensions/StringExtensions.cs | 36 +++++++++++++++++ 6 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 Source/EventFlow.Tests/UnitTests/Extensions/StringExtensionsTests.cs create mode 100644 Source/EventFlow/Extensions/StringExtensions.cs diff --git a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs index 0ad47a444..41721a5ee 100644 --- a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs +++ b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs @@ -65,6 +65,8 @@ public void Test() commandBus.Publish(new PingCommand(TestId.New, pingId), CancellationToken.None); var rabbitMqMessage = consumer.GetMessages().Single(); + rabbitMqMessage.Exchange.Value.Should().Be("eventflow"); + rabbitMqMessage.RoutingKey.Value.Should().Be("eventflow.domainevent.test.ping-event.1"); var pingEvent = (IDomainEvent) eventJsonSerializer.Deserialize( rabbitMqMessage.Message, diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs index bb9d709f2..047c019a7 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs @@ -22,6 +22,7 @@ using EventFlow.Aggregates; using EventFlow.EventStores; +using EventFlow.Extensions; namespace EventFlow.RabbitMQ.Integrations { @@ -45,8 +46,8 @@ public RabbitMqMessage CreateMessage(IDomainEvent domainEvent) var routingKey = new RoutingKey(string.Format( "eventflow.domainevent.{0}.{1}.{2}", - domainEvent.Metadata[MetadataKeys.AggregateName].ToLowerInvariant(), // TODO: Transform from "MyAgg" to "my-agg" - domainEvent.Metadata.EventName.ToLowerInvariant(), // TODO: Transform from "MyEvnt" to "my-evnt" + domainEvent.Metadata[MetadataKeys.AggregateName].ToSlug(), + domainEvent.Metadata.EventName.ToSlug(), domainEvent.Metadata.EventVersion)); var exchange = new Exchange("eventflow"); diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index 7a1da909a..2756d40e7 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -77,6 +77,7 @@ + diff --git a/Source/EventFlow.Tests/UnitTests/Extensions/StringExtensionsTests.cs b/Source/EventFlow.Tests/UnitTests/Extensions/StringExtensionsTests.cs new file mode 100644 index 000000000..f707c4277 --- /dev/null +++ b/Source/EventFlow.Tests/UnitTests/Extensions/StringExtensionsTests.cs @@ -0,0 +1,39 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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 EventFlow.Extensions; +using FluentAssertions; +using NUnit.Framework; + +namespace EventFlow.Tests.UnitTests.Extensions +{ + public class StringExtensionsTests + { + [TestCase("EventName", "event-name")] + [TestCase("event-name", "event-name")] + [TestCase("Event", "event")] + public void ToSlug(string input, string expected) + { + input.ToSlug().Should().Be(expected); + } + } +} diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index b12785823..93c48456b 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -123,6 +123,7 @@ + diff --git a/Source/EventFlow/Extensions/StringExtensions.cs b/Source/EventFlow/Extensions/StringExtensions.cs new file mode 100644 index 000000000..51001a651 --- /dev/null +++ b/Source/EventFlow/Extensions/StringExtensions.cs @@ -0,0 +1,36 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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.Text.RegularExpressions; + +namespace EventFlow.Extensions +{ + public static class StringExtensions + { + private static readonly Regex RegexToSlug = new Regex("(?<=.)([A-Z])", RegexOptions.Compiled); + + public static string ToSlug(this string str) + { + return RegexToSlug.Replace(str, "-$0").ToLowerInvariant(); + } + } +} From 489f6b043cd5f786e203a6cd9a03c59f7ca3c0e2 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 19 Aug 2015 19:57:57 +0200 Subject: [PATCH 21/54] Allow developers to override implementations --- .../EventFlowOptionsRabbitMqExtensions.cs | 9 ++-- .../Integrations/RabbitMqConnectionFactory.cs | 2 +- Source/EventFlow/EventFlow.csproj | 1 + .../ServiceRegistrationExtensions.cs | 44 +++++++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 Source/EventFlow/Extensions/ServiceRegistrationExtensions.cs diff --git a/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs b/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs index 1b97ae44f..d2e45d434 100644 --- a/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs +++ b/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs @@ -21,6 +21,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using EventFlow.Configuration.Registrations; +using EventFlow.Extensions; using EventFlow.RabbitMQ.Integrations; using EventFlow.ReadStores; @@ -34,10 +35,10 @@ public static EventFlowOptions PublishToRabbitMq( { eventFlowOptions.RegisterServices(sr => { - sr.Register(); - sr.Register(); - sr.Register(); - sr.Register(); + sr.RegisterIfNotRegistered(Lifetime.Singleton); + sr.RegisterIfNotRegistered(); + sr.RegisterIfNotRegistered(); + sr.RegisterIfNotRegistered(); sr.Register(rc => configuration, Lifetime.Singleton); diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs index 7481c4b3c..b0462e709 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs @@ -32,7 +32,7 @@ namespace EventFlow.RabbitMQ.Integrations public class RabbitMqConnectionFactory : IRabbitMqConnectionFactory { private readonly AsyncLock _asyncLock = new AsyncLock(); - private readonly Dictionary _connectionFactories = new Dictionary(); + private readonly Dictionary _connectionFactories = new Dictionary(); public async Task CreateConnectionAsync(Uri uri, CancellationToken cancellationToken) { diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 93c48456b..958b538c2 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -123,6 +123,7 @@ + diff --git a/Source/EventFlow/Extensions/ServiceRegistrationExtensions.cs b/Source/EventFlow/Extensions/ServiceRegistrationExtensions.cs new file mode 100644 index 000000000..93f6ddd54 --- /dev/null +++ b/Source/EventFlow/Extensions/ServiceRegistrationExtensions.cs @@ -0,0 +1,44 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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 EventFlow.Configuration; +using EventFlow.Configuration.Registrations; + +namespace EventFlow.Extensions +{ + public static class ServiceRegistrationExtensions + { + public static IServiceRegistration RegisterIfNotRegistered( + this IServiceRegistration serviceRegistration, + Lifetime lifetime = Lifetime.AlwaysUnique) + where TImplementation : class, TService + where TService : class + { + if (!serviceRegistration.HasRegistrationFor()) + { + serviceRegistration.Register(lifetime); + } + + return serviceRegistration; + } + } +} From 5cb3570ac86d63cc0df2658891e64de29def5363 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 19 Aug 2015 20:10:17 +0200 Subject: [PATCH 22/54] Logging and validation --- Source/EventFlow.RabbitMQ/Exchange.cs | 2 ++ .../Integrations/RabbitMqConnectionFactory.cs | 15 ++++++++++++--- .../Integrations/RabbitMqMessage.cs | 11 +++++++++++ .../Integrations/RabbitMqMessageFactory.cs | 10 +++++++++- .../Integrations/RabbitMqPublisher.cs | 11 ++++++++++- Source/EventFlow.RabbitMQ/RoutingKey.cs | 2 ++ 6 files changed, 46 insertions(+), 5 deletions(-) diff --git a/Source/EventFlow.RabbitMQ/Exchange.cs b/Source/EventFlow.RabbitMQ/Exchange.cs index b8c5c777c..42026258e 100644 --- a/Source/EventFlow.RabbitMQ/Exchange.cs +++ b/Source/EventFlow.RabbitMQ/Exchange.cs @@ -26,6 +26,8 @@ namespace EventFlow.RabbitMQ { public class Exchange : SingleValueObject { + public static Exchange Default => new Exchange(string.Empty); + public Exchange(string value) : base(value) { } diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs index b0462e709..17c119c4a 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs @@ -25,15 +25,23 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Core; +using EventFlow.Logs; using RabbitMQ.Client; namespace EventFlow.RabbitMQ.Integrations { public class RabbitMqConnectionFactory : IRabbitMqConnectionFactory { + private readonly ILog _log; private readonly AsyncLock _asyncLock = new AsyncLock(); private readonly Dictionary _connectionFactories = new Dictionary(); + public RabbitMqConnectionFactory( + ILog log) + { + _log = log; + } + public async Task CreateConnectionAsync(Uri uri, CancellationToken cancellationToken) { var connectionFactory = await CreateConnectionFactoryAsync(uri, cancellationToken).ConfigureAwait(false); @@ -49,17 +57,18 @@ private async Task CreateConnectionFactoryAsync(Uri uri, Canc { return connectionFactory; } + _log.Verbose("Creating RabbitMQ connection factory to {0}", uri.Host); connectionFactory = new ConnectionFactory { Uri = uri.ToString(), - UseBackgroundThreadsForIO = true, + UseBackgroundThreadsForIO = true, // TODO: As soon as RabbitMQ supports async/await, set to false TopologyRecoveryEnabled = true, AutomaticRecoveryEnabled = true, ClientProperties = new Dictionary { { "eventflow-version", typeof(RabbitMqConnectionFactory).Assembly.GetName().Version.ToString() }, - { "machine-name", Environment.MachineName } + { "machine-name", Environment.MachineName }, }, }; @@ -68,4 +77,4 @@ private async Task CreateConnectionFactoryAsync(Uri uri, Canc } } } -} \ No newline at end of file +} diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs index edebe404c..7e9414eaf 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessage.cs @@ -20,6 +20,7 @@ // 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; using System.Collections.Generic; namespace EventFlow.RabbitMQ.Integrations @@ -37,10 +38,20 @@ public RabbitMqMessage( Exchange exchange, RoutingKey routingKey) { + if (string.IsNullOrEmpty(message)) throw new ArgumentNullException(nameof(message)); + if (headers == null) throw new ArgumentNullException(nameof(headers)); + if (exchange == null) throw new ArgumentNullException(nameof(exchange)); + if (routingKey == null) throw new ArgumentNullException(nameof(routingKey)); + Message = message; Headers = headers; Exchange = exchange; RoutingKey = routingKey; } + + public override string ToString() + { + return $"{{Exchange: {Exchange}, RoutingKey: {RoutingKey}, Headers: {Headers.Count}, Bytes: {Message.Length/2}}}"; + } } } diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs index 047c019a7..422330c69 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs @@ -23,16 +23,20 @@ using EventFlow.Aggregates; using EventFlow.EventStores; using EventFlow.Extensions; +using EventFlow.Logs; namespace EventFlow.RabbitMQ.Integrations { public class RabbitMqMessageFactory : IRabbitMqMessageFactory { + private readonly ILog _log; private readonly IEventJsonSerializer _eventJsonSerializer; public RabbitMqMessageFactory( + ILog log, IEventJsonSerializer eventJsonSerializer) { + _log = log; _eventJsonSerializer = eventJsonSerializer; } @@ -51,11 +55,15 @@ public RabbitMqMessage CreateMessage(IDomainEvent domainEvent) domainEvent.Metadata.EventVersion)); var exchange = new Exchange("eventflow"); - return new RabbitMqMessage( + var rabbitMqMessage = new RabbitMqMessage( serializedEvent.SerializedData, domainEvent.Metadata, exchange, routingKey); + + _log.Verbose("Create RabbitMQ message {0}", rabbitMqMessage); + + return rabbitMqMessage; } } } diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index 3ede702f1..a4a632da0 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -29,23 +29,27 @@ using EventFlow.Aggregates; using EventFlow.Core; using EventFlow.Extensions; +using EventFlow.Logs; using RabbitMQ.Client; namespace EventFlow.RabbitMQ.Integrations { public class RabbitMqPublisher : IRabbitMqPublisher { + private readonly ILog _log; private readonly IRabbitMqConnectionFactory _connectionFactory; private readonly IRabbitMqMessageFactory _messageFactory; private readonly IRabbitMqConfiguration _configuration; private readonly ITransientFaultHandler _transientFaultHandler; public RabbitMqPublisher( + ILog log, IRabbitMqConnectionFactory connectionFactory, IRabbitMqMessageFactory messageFactory, IRabbitMqConfiguration configuration, ITransientFaultHandler transientFaultHandler) { + _log = log; _connectionFactory = connectionFactory; _messageFactory = messageFactory; _configuration = configuration; @@ -65,10 +69,15 @@ await _transientFaultHandler.TryAsync( .ConfigureAwait(false); } - private async Task PublishAsync(IEnumerable messages, CancellationToken cancellationToken) + private async Task PublishAsync(IReadOnlyCollection messages, CancellationToken cancellationToken) { // TODO: Cache connection/model + _log.Verbose( + "Publishing {0} domain domain events to RabbitMQ host '{1}'", + messages.Count, + _configuration.Uri.Host); + using (var connection = await _connectionFactory.CreateConnectionAsync(_configuration.Uri, cancellationToken).ConfigureAwait(false)) using (var model = connection.CreateModel()) { diff --git a/Source/EventFlow.RabbitMQ/RoutingKey.cs b/Source/EventFlow.RabbitMQ/RoutingKey.cs index 800c1ca68..fefe82a24 100644 --- a/Source/EventFlow.RabbitMQ/RoutingKey.cs +++ b/Source/EventFlow.RabbitMQ/RoutingKey.cs @@ -20,6 +20,7 @@ // 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; using EventFlow.ValueObjects; namespace EventFlow.RabbitMQ @@ -28,6 +29,7 @@ public class RoutingKey : SingleValueObject { public RoutingKey(string value) : base(value) { + if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); } } } From 6741ee4f29bc34b3aa21de199ea73581fd7f5ee3 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 19 Aug 2015 20:46:47 +0200 Subject: [PATCH 23/54] Cache RabbitMQ connection and model so we don't drown the RabbitMQ server --- .../EventFlow.RabbitMQ.csproj | 1 + .../EventFlowOptionsRabbitMqExtensions.cs | 2 +- .../IRabbitMqConfiguration.cs | 1 + .../Integrations/RabbitConnection.cs | 79 ++++++++++++++++ .../Integrations/RabbitMqPublisher.cs | 90 ++++++++++++++----- .../RabbitMqConfiguration.cs | 14 ++- Source/EventFlow/EventFlow.csproj | 1 + .../Extensions/DisposableExtensions.cs | 45 ++++++++++ 8 files changed, 205 insertions(+), 28 deletions(-) create mode 100644 Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs create mode 100644 Source/EventFlow/Extensions/DisposableExtensions.cs diff --git a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj index 03480d68f..2ea455f3e 100644 --- a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj +++ b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj @@ -52,6 +52,7 @@ + diff --git a/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs b/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs index d2e45d434..e911dc8cb 100644 --- a/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs +++ b/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs @@ -37,7 +37,7 @@ public static EventFlowOptions PublishToRabbitMq( { sr.RegisterIfNotRegistered(Lifetime.Singleton); sr.RegisterIfNotRegistered(); - sr.RegisterIfNotRegistered(); + sr.RegisterIfNotRegistered(Lifetime.Singleton); sr.RegisterIfNotRegistered(); sr.Register(rc => configuration, Lifetime.Singleton); diff --git a/Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs b/Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs index 9633251e5..ae459642f 100644 --- a/Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs +++ b/Source/EventFlow.RabbitMQ/IRabbitMqConfiguration.cs @@ -28,5 +28,6 @@ public interface IRabbitMqConfiguration { Uri Uri { get; } bool Persistent { get; } + int ModelsPrConnection { get; } } } \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs new file mode 100644 index 000000000..c2c32d6ea --- /dev/null +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs @@ -0,0 +1,79 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Core; +using EventFlow.Extensions; +using EventFlow.Logs; +using RabbitMQ.Client; + +namespace EventFlow.RabbitMQ.Integrations +{ + public class RabbitConnection : IDisposable + { + private readonly ILog _log; + private readonly IConnection _connection; + private readonly AsyncLock _asyncLock; + private readonly ConcurrentBag _models; + + public RabbitConnection(ILog log, int maxModels, IConnection connection) + { + _connection = connection; + _log = log; + _asyncLock = new AsyncLock(maxModels); + _models = new ConcurrentBag(Enumerable.Range(0, maxModels).Select(_ => connection.CreateModel())); + } + + public async Task WithModelAsync(Func action, CancellationToken cancellationToken) + { + using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) + { + IModel model; + if (!_models.TryTake(out model)) + { + throw new InvalidOperationException( + "This should NEVER happen! If it does, please report a bug."); + } + + await action(model).ConfigureAwait(false); + + _models.Add(model); + } + + return 0; + } + + public void Dispose() + { + foreach (var model in _models) + { + model.DisposeSafe(_log, "Failed to dispose model"); + } + _connection.DisposeSafe(_log, "Failed to dispose connection"); + _log.Verbose("Disposing RabbitMQ connection"); + } + } +} diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index a4a632da0..d8e48da10 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -34,13 +34,15 @@ namespace EventFlow.RabbitMQ.Integrations { - public class RabbitMqPublisher : IRabbitMqPublisher + public class RabbitMqPublisher : IDisposable, IRabbitMqPublisher { private readonly ILog _log; private readonly IRabbitMqConnectionFactory _connectionFactory; private readonly IRabbitMqMessageFactory _messageFactory; private readonly IRabbitMqConfiguration _configuration; private readonly ITransientFaultHandler _transientFaultHandler; + private readonly AsyncLock _asyncLock = new AsyncLock(); + private readonly Dictionary _connections = new Dictionary(); public RabbitMqPublisher( ILog log, @@ -62,42 +64,84 @@ public async Task PublishAsync(IReadOnlyCollection domainEvents, C .Select(e => _messageFactory.CreateMessage(e)) .ToList(); - await _transientFaultHandler.TryAsync( - c => PublishAsync(message, c), - Label.Named("rabbitmq-publish"), - cancellationToken) - .ConfigureAwait(false); + var uri = _configuration.Uri; + RabbitConnection rabbitConnection = null; + try + { + rabbitConnection = await GetRabbitMqConnectionAsync(uri, cancellationToken).ConfigureAwait(false); + + await _transientFaultHandler.TryAsync( + c => rabbitConnection.WithModelAsync(m => PublishAsync(m, message, c), c), + Label.Named("rabbitmq-publish"), + cancellationToken) + .ConfigureAwait(false); + } + catch (Exception e) + { + if (rabbitConnection != null) + { + using (await _asyncLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + { + rabbitConnection.Dispose(); + _connections.Remove(uri); + } + } + _log.Warning(e, ""); + throw; + } } - private async Task PublishAsync(IReadOnlyCollection messages, CancellationToken cancellationToken) + private async Task GetRabbitMqConnectionAsync(Uri uri, CancellationToken cancellationToken) { - // TODO: Cache connection/model + using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) + { + RabbitConnection rabbitConnection; + if (_connections.TryGetValue(uri, out rabbitConnection)) + { + return rabbitConnection; + } + + var connection = await _connectionFactory.CreateConnectionAsync(uri, cancellationToken).ConfigureAwait(false); + rabbitConnection = new RabbitConnection(_log, _configuration.ModelsPrConnection, connection); + _connections.Add(uri, rabbitConnection); + return rabbitConnection; + } + } + + private Task PublishAsync(IModel model, IReadOnlyCollection messages, CancellationToken cancellationToken) + { _log.Verbose( "Publishing {0} domain domain events to RabbitMQ host '{1}'", messages.Count, _configuration.Uri.Host); - using (var connection = await _connectionFactory.CreateConnectionAsync(_configuration.Uri, cancellationToken).ConfigureAwait(false)) - using (var model = connection.CreateModel()) + foreach (var message in messages) { - foreach (var message in messages) - { - var bytes = Encoding.UTF8.GetBytes(message.Message); + var bytes = Encoding.UTF8.GetBytes(message.Message); - var basicProperties = model.CreateBasicProperties(); - basicProperties.Headers = message.Headers.ToDictionary(kv => kv.Key, kv => (object)kv.Value); - basicProperties.Persistent = _configuration.Persistent; - basicProperties.Timestamp = new AmqpTimestamp(DateTimeOffset.Now.ToUnixTime()); - basicProperties.ContentEncoding = "utf-8"; - basicProperties.ContentType = "application/json"; - basicProperties.MessageId = message.Headers[MetadataKeys.AggregateId]; + var basicProperties = model.CreateBasicProperties(); + basicProperties.Headers = message.Headers.ToDictionary(kv => kv.Key, kv => (object)kv.Value); + basicProperties.Persistent = _configuration.Persistent; + basicProperties.Timestamp = new AmqpTimestamp(DateTimeOffset.Now.ToUnixTime()); + basicProperties.ContentEncoding = "utf-8"; + basicProperties.ContentType = "application/json"; + basicProperties.MessageId = message.Headers[MetadataKeys.AggregateId]; - model.BasicPublish(message.Exchange.Value, message.RoutingKey.Value, false, false, basicProperties, bytes); - } + // TODO: Evil or not evil? Do a Task.Run here? + model.BasicPublish(message.Exchange.Value, message.RoutingKey.Value, false, false, basicProperties, bytes); } - return 0; + return Task.FromResult(0); + } + + public void Dispose() + { + foreach (var rabbitConnection in _connections.Values) + { + rabbitConnection.Dispose(); + } + _connections.Clear(); } } } diff --git a/Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs b/Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs index 1d66133d1..8c712be1b 100644 --- a/Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs +++ b/Source/EventFlow.RabbitMQ/RabbitMqConfiguration.cs @@ -27,16 +27,22 @@ namespace EventFlow.RabbitMQ public class RabbitMqConfiguration : IRabbitMqConfiguration { public Uri Uri { get; } - public bool Persistent { get { return true; } } + public bool Persistent { get; } + public int ModelsPrConnection { get; } - public static IRabbitMqConfiguration With(Uri uri) + public static IRabbitMqConfiguration With( + Uri uri, + bool persistent = true, + int modelsPrConnection = 5) { - return new RabbitMqConfiguration(uri); + return new RabbitMqConfiguration(uri, persistent, modelsPrConnection); } - private RabbitMqConfiguration(Uri uri) + private RabbitMqConfiguration(Uri uri, bool persistent, int modelsPrConnection) { Uri = uri; + Persistent = persistent; + ModelsPrConnection = modelsPrConnection; } } } diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 958b538c2..9d3f5bc22 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -114,6 +114,7 @@ + diff --git a/Source/EventFlow/Extensions/DisposableExtensions.cs b/Source/EventFlow/Extensions/DisposableExtensions.cs new file mode 100644 index 000000000..e293abed2 --- /dev/null +++ b/Source/EventFlow/Extensions/DisposableExtensions.cs @@ -0,0 +1,45 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; +using EventFlow.Logs; + +namespace EventFlow.Extensions +{ + public static class DisposableExtensions + { + public static void DisposeSafe( + this IDisposable disposable, + ILog log, + string message) + { + try + { + disposable.Dispose(); + } + catch (Exception e) + { + log.Warning(e, message); + } + } + } +} From d04ff387d11aa66cb603942de1dc107dcf19c367 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 20 Aug 2015 19:27:24 +0200 Subject: [PATCH 24/54] Make it possible to use the message publisher for other than domain events --- .../Integrations/IRabbitMqPublisher.cs | 4 ++-- .../Integrations/RabbitMqPublisher.cs | 14 ++++++-------- .../RabbitMqDomainEventPublisher.cs | 10 ++++++++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqPublisher.cs index 2d0f3af99..2edb2018f 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqPublisher.cs @@ -23,12 +23,12 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using EventFlow.Aggregates; namespace EventFlow.RabbitMQ.Integrations { public interface IRabbitMqPublisher { - Task PublishAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken); + Task PublishAsync(CancellationToken cancellationToken, params RabbitMqMessage[] rabbitMqMessages); + Task PublishAsync(IReadOnlyCollection rabbitMqMessages, CancellationToken cancellationToken); } } diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index d8e48da10..e8d148b8c 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -38,7 +38,6 @@ public class RabbitMqPublisher : IDisposable, IRabbitMqPublisher { private readonly ILog _log; private readonly IRabbitMqConnectionFactory _connectionFactory; - private readonly IRabbitMqMessageFactory _messageFactory; private readonly IRabbitMqConfiguration _configuration; private readonly ITransientFaultHandler _transientFaultHandler; private readonly AsyncLock _asyncLock = new AsyncLock(); @@ -47,23 +46,22 @@ public class RabbitMqPublisher : IDisposable, IRabbitMqPublisher public RabbitMqPublisher( ILog log, IRabbitMqConnectionFactory connectionFactory, - IRabbitMqMessageFactory messageFactory, IRabbitMqConfiguration configuration, ITransientFaultHandler transientFaultHandler) { _log = log; _connectionFactory = connectionFactory; - _messageFactory = messageFactory; _configuration = configuration; _transientFaultHandler = transientFaultHandler; } - public async Task PublishAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) + public Task PublishAsync(CancellationToken cancellationToken, params RabbitMqMessage[] rabbitMqMessages) { - var message = domainEvents - .Select(e => _messageFactory.CreateMessage(e)) - .ToList(); + return PublishAsync(rabbitMqMessages, cancellationToken); + } + public async Task PublishAsync(IReadOnlyCollection rabbitMqMessages, CancellationToken cancellationToken) + { var uri = _configuration.Uri; RabbitConnection rabbitConnection = null; try @@ -71,7 +69,7 @@ public async Task PublishAsync(IReadOnlyCollection domainEvents, C rabbitConnection = await GetRabbitMqConnectionAsync(uri, cancellationToken).ConfigureAwait(false); await _transientFaultHandler.TryAsync( - c => rabbitConnection.WithModelAsync(m => PublishAsync(m, message, c), c), + c => rabbitConnection.WithModelAsync(m => PublishAsync(m, rabbitMqMessages, c), c), Label.Named("rabbitmq-publish"), cancellationToken) .ConfigureAwait(false); diff --git a/Source/EventFlow.RabbitMQ/RabbitMqDomainEventPublisher.cs b/Source/EventFlow.RabbitMQ/RabbitMqDomainEventPublisher.cs index ed0b337a2..cede1f2d4 100644 --- a/Source/EventFlow.RabbitMQ/RabbitMqDomainEventPublisher.cs +++ b/Source/EventFlow.RabbitMQ/RabbitMqDomainEventPublisher.cs @@ -21,6 +21,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -32,18 +33,23 @@ namespace EventFlow.RabbitMQ public class RabbitMqDomainEventPublisher : IReadStoreManager { private readonly IRabbitMqPublisher _rabbitMqPublisher; + private readonly IRabbitMqMessageFactory _rabbitMqMessageFactory; public RabbitMqDomainEventPublisher( - IRabbitMqPublisher rabbitMqPublisher) + IRabbitMqPublisher rabbitMqPublisher, + IRabbitMqMessageFactory rabbitMqMessageFactory) { _rabbitMqPublisher = rabbitMqPublisher; + _rabbitMqMessageFactory = rabbitMqMessageFactory; } public Task UpdateReadStoresAsync( IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { - return _rabbitMqPublisher.PublishAsync(domainEvents, cancellationToken); + var rabbitMqMessages = domainEvents.Select(e => _rabbitMqMessageFactory.CreateMessage(e)).ToList(); + + return _rabbitMqPublisher.PublishAsync(rabbitMqMessages, cancellationToken); } } } From fd196494656da1d67efb105c90c6044040d1933d Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 20 Aug 2015 19:31:29 +0200 Subject: [PATCH 25/54] Log message --- Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index e8d148b8c..a2e20f99e 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -84,7 +84,7 @@ await _transientFaultHandler.TryAsync( _connections.Remove(uri); } } - _log.Warning(e, ""); + _log.Error(e, "Failed to publish domain events to RabbitMQ"); throw; } } From 92931316b47b0c9795f0c50951d3d214ff6b776a Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 20 Aug 2015 19:41:16 +0200 Subject: [PATCH 26/54] Fix tests... don't break backward compatibility with committed events --- .../EventStores/EventJsonSerializer.cs | 33 +++++++++++-------- .../EventStores/ICommittedDomainEvent.cs | 2 ++ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Source/EventFlow/EventStores/EventJsonSerializer.cs b/Source/EventFlow/EventStores/EventJsonSerializer.cs index 22d57922e..22fec0f4e 100644 --- a/Source/EventFlow/EventStores/EventJsonSerializer.cs +++ b/Source/EventFlow/EventStores/EventJsonSerializer.cs @@ -68,25 +68,13 @@ public SerializedEvent Serialize(IAggregateEvent aggregateEvent, IEnumerable(committedDomainEvent.Metadata); - return Deserialize(committedDomainEvent.Data, metadata); + return Deserialize(committedDomainEvent.AggregateId, committedDomainEvent.Data, metadata); } public IDomainEvent Deserialize( @@ -97,5 +85,22 @@ public IDomainEvent Deserialize( { return (IDomainEvent)Deserialize(committedDomainEvent); } + + private IDomainEvent Deserialize(string aggregateId, string json, IMetadata metadata) + { + var eventDefinition = _eventDefinitionService.GetEventDefinition( + metadata.EventName, + metadata.EventVersion); + + var aggregateEvent = (IAggregateEvent)_jsonSerializer.Deserialize(json, eventDefinition.Type); + + var domainEvent = _domainEventFactory.Create( + aggregateEvent, + metadata, + aggregateId, + metadata.AggregateSequenceNumber); + + return domainEvent; + } } } diff --git a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs index 557627a33..9ac27c087 100644 --- a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs +++ b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs @@ -24,7 +24,9 @@ namespace EventFlow.EventStores { public interface ICommittedDomainEvent { + string AggregateId { get; set; } string Data { get; set; } string Metadata { get; set; } + int AggregateSequenceNumber { get; set; } } } From f6704cf62c430f19fedabd4246f91b24472338dd Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 20 Aug 2015 20:05:02 +0200 Subject: [PATCH 27/54] Minor test to see if the publisher can actually handle load --- .../Integration/RabbitMqTests.cs | 96 +++++++++++++++---- .../RabbitMqConsumer.cs | 2 - .../Integrations/RabbitMqPublisher.cs | 2 - Source/EventFlow/EventFlow.csproj | 1 + Source/EventFlow/Logs/ConsoleLog.cs | 6 +- Source/EventFlow/Logs/NullLog.cs | 41 ++++++++ 6 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 Source/EventFlow/Logs/NullLog.cs diff --git a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs index 41721a5ee..afbc244a5 100644 --- a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs +++ b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs @@ -21,12 +21,16 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Concurrent; using System.Linq; using System.Threading; using EventFlow.Aggregates; +using EventFlow.Configuration; using EventFlow.EventStores; using EventFlow.Extensions; +using EventFlow.Logs; using EventFlow.RabbitMQ.Extensions; +using EventFlow.RabbitMQ.Integrations; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates.Test; using EventFlow.TestHelpers.Aggregates.Test.Commands; @@ -37,27 +41,23 @@ namespace EventFlow.RabbitMQ.Tests.Integration { + [Explicit("Needs RabbitMQ running localhost (https://github.com/rasmus/Vagrant.Boxes)")] public class RabbitMqTests { - public class RabbitMqCommittedEvent : ICommittedDomainEvent + private readonly Uri _uri; + private readonly ConcurrentBag _exceptions = new ConcurrentBag(); + + public RabbitMqTests() { - public string AggregateId { get; set; } - public string Data { get; set; } - public string Metadata { get; set; } - public int AggregateSequenceNumber { get; set; } + _uri = new Uri("amqp://localhost"); } - [Test, Explicit("Needs RabbitMQ running localhost (https://github.com/rasmus/Vagrant.Boxes)")] - public void Test() + [Test] + public void Scenario() { - var uri = new Uri("amqp://localhost"); - using (var consumer = new RabbitMqConsumer(uri, "eventflow", new[] {"#"})) + using (var consumer = new RabbitMqConsumer(_uri, "eventflow", new[] { "#" })) + using (var resolver = BuildResolver()) { - var resolver = EventFlowOptions.New - .PublishToRabbitMq(RabbitMqConfiguration.With(uri)) - .AddDefaults(EventFlowTestHelpers.Assembly) - .CreateResolver(false); - var commandBus = resolver.Resolve(); var eventJsonSerializer = resolver.Resolve(); @@ -67,13 +67,77 @@ public void Test() var rabbitMqMessage = consumer.GetMessages().Single(); rabbitMqMessage.Exchange.Value.Should().Be("eventflow"); rabbitMqMessage.RoutingKey.Value.Should().Be("eventflow.domainevent.test.ping-event.1"); - - var pingEvent = (IDomainEvent) eventJsonSerializer.Deserialize( + + var pingEvent = (IDomainEvent)eventJsonSerializer.Deserialize( rabbitMqMessage.Message, new Metadata(rabbitMqMessage.Headers)); pingEvent.AggregateEvent.PingId.Should().Be(pingId); } } + + [Test, Timeout(20000)] + public void PublisherPerformance() + { + var exchange = new Exchange("eventflow"); + var routingKey = new RoutingKey("performance"); + const int threadCount = 100; + const int messagesPrThread = 200; + + using (var consumer = new RabbitMqConsumer(_uri, "eventflow", new[] {"#"})) + using (var resolver = BuildResolver(o => o.RegisterServices(sr => sr.Register()))) + { + var rabbitMqPublisher = resolver.Resolve(); + var threads = Enumerable.Range(0, threadCount) + .Select(_ => + { + var thread = new Thread(o => SendMessages(rabbitMqPublisher, messagesPrThread, exchange, routingKey)); + thread.Start(); + return thread; + }) + .ToList(); + + foreach (var thread in threads) + { + thread.Join(); + } + + var rabbitMqMessages = consumer.GetMessages(threadCount * messagesPrThread); + rabbitMqMessages.Should().HaveCount(threadCount*messagesPrThread); + _exceptions.Should().BeEmpty(); + } + } + + private void SendMessages(IRabbitMqPublisher rabbitMqPublisher, int count, Exchange exchange, RoutingKey routingKey) + { + var guid = Guid.NewGuid(); + + try + { + for (var i = 0; i < count; i++) + { + var rabbitMqMessage = new RabbitMqMessage( + $"{guid}-{i}", + new Metadata(), + exchange, + routingKey); + rabbitMqPublisher.PublishAsync(CancellationToken.None, rabbitMqMessage).Wait(); + } + } + catch (Exception e) + { + _exceptions.Add(e); + } + } + + private IRootResolver BuildResolver(Func configure = null) + { + configure = configure ?? (e => e); + + return configure(EventFlowOptions.New + .PublishToRabbitMq(RabbitMqConfiguration.With(_uri)) + .AddDefaults(EventFlowTestHelpers.Assembly)) + .CreateResolver(false); + } } } diff --git a/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs b/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs index 40d9bb2b3..95d9b998e 100644 --- a/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs +++ b/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs @@ -75,8 +75,6 @@ public RabbitMqConsumer(Uri uri, string exchange, IEnumerable routingKey private void OnReceived(object sender, BasicDeliverEventArgs basicDeliverEventArgs) { - Console.WriteLine("Received message: {0}", basicDeliverEventArgs.RoutingKey); - lock (_receivedMessages) { _receivedMessages.Add(basicDeliverEventArgs); diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index a2e20f99e..4d41f4d40 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -26,7 +26,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using EventFlow.Aggregates; using EventFlow.Core; using EventFlow.Extensions; using EventFlow.Logs; @@ -124,7 +123,6 @@ private Task PublishAsync(IModel model, IReadOnlyCollection + diff --git a/Source/EventFlow/Logs/ConsoleLog.cs b/Source/EventFlow/Logs/ConsoleLog.cs index f03be2755..e196efd3e 100644 --- a/Source/EventFlow/Logs/ConsoleLog.cs +++ b/Source/EventFlow/Logs/ConsoleLog.cs @@ -26,9 +26,9 @@ namespace EventFlow.Logs { public class ConsoleLog : Log { - protected override bool IsVerboseEnabled { get { return true; } } - protected override bool IsDebugEnabled { get { return true; } } - protected override bool IsInformationEnabled { get { return true; } } + protected override bool IsVerboseEnabled => true; + protected override bool IsDebugEnabled => true; + protected override bool IsInformationEnabled => true; protected override void Write(LogLevel logLevel, string format, params object[] args) { diff --git a/Source/EventFlow/Logs/NullLog.cs b/Source/EventFlow/Logs/NullLog.cs new file mode 100644 index 000000000..d18ea072e --- /dev/null +++ b/Source/EventFlow/Logs/NullLog.cs @@ -0,0 +1,41 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; + +namespace EventFlow.Logs +{ + public class NullLog : Log + { + protected override bool IsVerboseEnabled => false; + protected override bool IsInformationEnabled => false; + protected override bool IsDebugEnabled => false; + + protected override void Write(LogLevel logLevel, string format, params object[] args) + { + } + + protected override void Write(LogLevel logLevel, Exception exception, string format, params object[] args) + { + } + } +} From 5f2645bb9a0ddf9a452c9283bf985b3dcb45f6a5 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 20 Aug 2015 20:22:25 +0200 Subject: [PATCH 28/54] Don't use read model interface to subscribe to events --- RELEASE_NOTES.md | 7 ++++ .../Integration/RabbitMqTests.cs | 2 +- .../EventFlowOptionsRabbitMqExtensions.cs | 8 ++--- .../RabbitMqDomainEventPublisher.cs | 8 ++--- Source/EventFlow/EventFlow.csproj | 1 + .../EventFlowOptionsSubscriberExtensions.cs | 14 +++++--- .../Subscribers/DomainEventPublisher.cs | 10 +++++- .../Subscribers/ISubscribeSynchronousToAll.cs | 36 +++++++++++++++++++ 8 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 Source/EventFlow/Subscribers/ISubscribeSynchronousToAll.cs diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 748145f18..bb5187ca9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,13 @@ * Breaking: `EventFlowOptions AddDefaults(...)` now also adds event definitions + * New: [RabbitMQ](http://www.rabbitmq.com/) is now supported through the new + NuGet package called `EventFlow.RabbitMQ` which enables domain events to be + published to the bus + * New: If you want to subscribe to all domain events, you can implement + and register a service that implements `ISubscribeSynchronousToAll`. Services + that implement this will automatically be added using the + `AddSubscribers(...)` or `AddDefaults(...)` extension to `EventFlowOptions` ### New in 0.10.642 (released 2015-08-17) diff --git a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs index afbc244a5..4346905b5 100644 --- a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs +++ b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs @@ -52,7 +52,7 @@ public RabbitMqTests() _uri = new Uri("amqp://localhost"); } - [Test] + [Test, Timeout(10000)] public void Scenario() { using (var consumer = new RabbitMqConsumer(_uri, "eventflow", new[] { "#" })) diff --git a/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs b/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs index e911dc8cb..e367ef180 100644 --- a/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs +++ b/Source/EventFlow.RabbitMQ/Extensions/EventFlowOptionsRabbitMqExtensions.cs @@ -23,7 +23,7 @@ using EventFlow.Configuration.Registrations; using EventFlow.Extensions; using EventFlow.RabbitMQ.Integrations; -using EventFlow.ReadStores; +using EventFlow.Subscribers; namespace EventFlow.RabbitMQ.Extensions { @@ -36,13 +36,13 @@ public static EventFlowOptions PublishToRabbitMq( eventFlowOptions.RegisterServices(sr => { sr.RegisterIfNotRegistered(Lifetime.Singleton); - sr.RegisterIfNotRegistered(); + sr.RegisterIfNotRegistered(Lifetime.Singleton); sr.RegisterIfNotRegistered(Lifetime.Singleton); - sr.RegisterIfNotRegistered(); + sr.RegisterIfNotRegistered(Lifetime.Singleton); sr.Register(rc => configuration, Lifetime.Singleton); - sr.Register(); + sr.Register(); }); return eventFlowOptions; diff --git a/Source/EventFlow.RabbitMQ/RabbitMqDomainEventPublisher.cs b/Source/EventFlow.RabbitMQ/RabbitMqDomainEventPublisher.cs index cede1f2d4..911285535 100644 --- a/Source/EventFlow.RabbitMQ/RabbitMqDomainEventPublisher.cs +++ b/Source/EventFlow.RabbitMQ/RabbitMqDomainEventPublisher.cs @@ -26,11 +26,11 @@ using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.RabbitMQ.Integrations; -using EventFlow.ReadStores; +using EventFlow.Subscribers; namespace EventFlow.RabbitMQ { - public class RabbitMqDomainEventPublisher : IReadStoreManager + public class RabbitMqDomainEventPublisher : ISubscribeSynchronousToAll { private readonly IRabbitMqPublisher _rabbitMqPublisher; private readonly IRabbitMqMessageFactory _rabbitMqMessageFactory; @@ -43,9 +43,7 @@ public RabbitMqDomainEventPublisher( _rabbitMqMessageFactory = rabbitMqMessageFactory; } - public Task UpdateReadStoresAsync( - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) + public Task HandleAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { var rabbitMqMessages = domainEvents.Select(e => _rabbitMqMessageFactory.CreateMessage(e)).ToList(); diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index a4594e745..c352b3a59 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -190,6 +190,7 @@ + diff --git a/Source/EventFlow/Extensions/EventFlowOptionsSubscriberExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsSubscriberExtensions.cs index 07774a1bd..b21fc047f 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsSubscriberExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsSubscriberExtensions.cs @@ -56,7 +56,11 @@ public static EventFlowOptions AddSubscribers( { var subscribeSynchronousToTypes = fromAssembly .GetTypes() - .Where(t => t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (ISubscribeSynchronousTo<,,>))); + .Where(t => t + .GetInterfaces() + .Any(i => + (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISubscribeSynchronousTo<,,>)) || + i == typeof(ISubscribeSynchronousToAll))); return eventFlowOptions .AddSubscribers(subscribeSynchronousToTypes); } @@ -70,13 +74,13 @@ public static EventFlowOptions AddSubscribers( var t = subscribeSynchronousToType; var subscribeTos = t .GetInterfaces() - .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (ISubscribeSynchronousTo<,,>)) + .Where(i => + (i.IsGenericType && i.GetGenericTypeDefinition() == typeof (ISubscribeSynchronousTo<,,>)) || + i == typeof(ISubscribeSynchronousToAll)) .ToList(); if (!subscribeTos.Any()) { - throw new ArgumentException(string.Format( - "Type '{0}' is not a ISubscribeSynchronousTo", - t.Name)); + throw new ArgumentException($"Type '{t.Name}' is not a ISubscribeSynchronousTo"); } eventFlowOptions.RegisterServices(sr => diff --git a/Source/EventFlow/Subscribers/DomainEventPublisher.cs b/Source/EventFlow/Subscribers/DomainEventPublisher.cs index e90c1b45f..46dda9770 100644 --- a/Source/EventFlow/Subscribers/DomainEventPublisher.cs +++ b/Source/EventFlow/Subscribers/DomainEventPublisher.cs @@ -32,13 +32,16 @@ namespace EventFlow.Subscribers public class DomainEventPublisher : IDomainEventPublisher { private readonly IDispatchToEventSubscribers _dispatchToEventSubscribers; + private readonly IReadOnlyCollection _subscribeSynchronousToAlls; private readonly IReadOnlyCollection _readStoreManagers; public DomainEventPublisher( IDispatchToEventSubscribers dispatchToEventSubscribers, - IEnumerable readStoreManagers) + IEnumerable readStoreManagers, + IEnumerable subscribeSynchronousToAlls) { _dispatchToEventSubscribers = dispatchToEventSubscribers; + _subscribeSynchronousToAlls = subscribeSynchronousToAlls.ToList(); _readStoreManagers = readStoreManagers.ToList(); } @@ -56,6 +59,11 @@ public async Task PublishAsync( // Update subscriptions AFTER read stores have been updated await _dispatchToEventSubscribers.DispatchAsync(domainEvents, cancellationToken).ConfigureAwait(false); + + // Send to handlers that listen to all events + var handle = _subscribeSynchronousToAlls + .Select(s => s.HandleAsync(domainEvents, cancellationToken)); + await Task.WhenAll(handle).ConfigureAwait(false); } } } diff --git a/Source/EventFlow/Subscribers/ISubscribeSynchronousToAll.cs b/Source/EventFlow/Subscribers/ISubscribeSynchronousToAll.cs new file mode 100644 index 000000000..9219faaaa --- /dev/null +++ b/Source/EventFlow/Subscribers/ISubscribeSynchronousToAll.cs @@ -0,0 +1,36 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; + +namespace EventFlow.Subscribers +{ + public interface ISubscribeSynchronousToAll + { + Task HandleAsync( + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken); + } +} \ No newline at end of file From 6f079d8d2a715f57c0bc7f66925d90ce66c36b65 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 20 Aug 2015 20:35:25 +0200 Subject: [PATCH 29/54] Update documentation --- Documentation/RabbitMQ.md | 35 +++++++++++++++++++++++++++++++++++ README.md | 11 +++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 Documentation/RabbitMQ.md diff --git a/Documentation/RabbitMQ.md b/Documentation/RabbitMQ.md new file mode 100644 index 000000000..d02708126 --- /dev/null +++ b/Documentation/RabbitMQ.md @@ -0,0 +1,35 @@ +# RabbitMQ + +Configuring EventFlow to publish events to [RabbitMQ](http://www.rabbitmq.com/) +is simple, just install the NuGet package `EventFlow.RabbitMQ` and add this to +your EventFlow setup. + +```csharp +var uri = new Uri("amqp://localhost"); + +var resolver = EventFlowOptions.with + .PublishToRabbitMq(RabbitMqConfiguration.With(uri)) + ... + .CreateResolver(); +``` + +Events are published to a exchange named `eventflow` with routing keys in the +following format. + +``` +eventflow.domainevent.[Aggregate name].[Event name].[Event version] +``` + +Which will be the following for an event named `CreateUser` version `1` for the +`MyUserAggregate`. + +``` +eventflow.domainevent.my-user.create-user.1 +``` + +Note the lowercasing and adding of `-` whenever there's a capital letter. + +All the above is the default behavior, if you don't like it replace e.g. the +service `IRabbitMqMessageFactory` to customize what routing key or exchange to +use. Have a look at how [EventFlow](https://github.com/rasmus/EventFlow) has +done its implementation to get started. diff --git a/README.md b/README.md index 9f67afc28..4f55990e0 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,14 @@ to the documentation. read model storage types. * In-memory - only for test * Microsoft SQL Server -* [**Queries**](./Documentation/Queries.md): Value objects that represent +* [**Queries:**](./Documentation/Queries.md): Value objects that represent a query without specifying how its executed, that is let to a query handler -* [**Event upgrade**](./Documentation/EventUpgrade.md): As events committed to - the event store is never changed, EventFlow uses the concept of event upgraders - to deprecate events and replace them with new during aggregate load. +* [**Event upgrade:**](./Documentation/EventUpgrade.md): As events committed to + the event store is never changed, EventFlow uses the concept of event + upgraders to deprecate events and replace them with new during aggregate load. +* [**Event publishing:**] Sometimes you want other applications or services to + consume and act on domains. For this EventFlow supports event publishing. + * [RabbitMQ](./Documentation/RabbitMQ.md) * [**Metadata**](./Documentation/Metadata.md): Additional information for each aggregate event, e.g. the IP of the user behind the event being emitted. EventFlow ships with From 0e0eeea51e0c37db5b5f42cc097a53b7c1f469dc Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 20 Aug 2015 21:08:25 +0200 Subject: [PATCH 30/54] Minor markdown fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f55990e0..e964fe329 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ to the documentation. * [**Event upgrade:**](./Documentation/EventUpgrade.md): As events committed to the event store is never changed, EventFlow uses the concept of event upgraders to deprecate events and replace them with new during aggregate load. -* [**Event publishing:**] Sometimes you want other applications or services to +* **Event publishing:** Sometimes you want other applications or services to consume and act on domains. For this EventFlow supports event publishing. * [RabbitMQ](./Documentation/RabbitMQ.md) * [**Metadata**](./Documentation/Metadata.md): From 75c10593f917f31cebb0c9392585c74c162174eb Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 20 Aug 2015 21:13:10 +0200 Subject: [PATCH 31/54] Removed a completed todo --- .../EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs index 422330c69..d77f7be56 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqMessageFactory.cs @@ -46,8 +46,6 @@ public RabbitMqMessage CreateMessage(IDomainEvent domainEvent) domainEvent.GetAggregateEvent(), domainEvent.Metadata); - // TODO: Add aggregate name to routing key - var routingKey = new RoutingKey(string.Format( "eventflow.domainevent.{0}.{1}.{2}", domainEvent.Metadata[MetadataKeys.AggregateName].ToSlug(), From b24c703428c1307710d0ad4ab8a87db6ce80d54e Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 21 Aug 2015 06:44:13 +0200 Subject: [PATCH 32/54] Re-add the model after use --- .../Integrations/RabbitConnection.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs index c2c32d6ea..be4cfea00 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs @@ -58,9 +58,14 @@ public async Task WithModelAsync(Func action, CancellationTok "This should NEVER happen! If it does, please report a bug."); } - await action(model).ConfigureAwait(false); - - _models.Add(model); + try + { + await action(model).ConfigureAwait(false); + } + finally + { + _models.Add(model); + } } return 0; From dac3dfb8895b8036d38a57a779c9e3eef9fd91ff Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 21 Aug 2015 06:49:05 +0200 Subject: [PATCH 33/54] Don't reset connection on cancel --- Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index 4d41f4d40..8442c6893 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -73,6 +73,10 @@ await _transientFaultHandler.TryAsync( cancellationToken) .ConfigureAwait(false); } + catch (OperationCanceledException) + { + throw; + } catch (Exception e) { if (rabbitConnection != null) From 91d0f8521ce686d95c1b81964bfdc053abf5404f Mon Sep 17 00:00:00 2001 From: The Gitter Badger Date: Fri, 21 Aug 2015 05:03:05 +0000 Subject: [PATCH 34/54] Added Gitter badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c8e8c0687..ddb4fe47e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # EventFlow +[![Join the chat at https://gitter.im/rasmus/EventFlow](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/rasmus/EventFlow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + [![NuGet Status](http://img.shields.io/nuget/v/EventFlow.svg?style=flat)](https://www.nuget.org/packages/EventFlow/) [![Build status](https://ci.appveyor.com/api/projects/status/51yvhvbd909e4o82/branch/develop?svg=true)](https://ci.appveyor.com/project/rasmusnu/eventflow) [![License](https://img.shields.io/github/license/rasmus/eventflow.svg)](./LICENSE) From 01ea4d8f9dfb67e081fe6713467e5286bfe54741 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 21 Aug 2015 20:34:39 +0200 Subject: [PATCH 35/54] Started on unit tests of RabbitMQ publishing --- .../EventFlow.RabbitMQ.Tests.csproj | 9 ++ .../Integrations/RabbitMqPublisherTests.cs | 93 +++++++++++++++++++ Source/EventFlow.RabbitMQ.Tests/app.config | 4 + .../EventFlow.RabbitMQ.Tests/packages.config | 2 + .../EventFlow.RabbitMQ.csproj | 1 + .../Integrations/IRabbitConnection.cs | 34 +++++++ .../IRabbitMqConnectionFactory.cs | 3 +- .../Integrations/RabbitConnection.cs | 2 +- .../Integrations/RabbitMqConnectionFactory.cs | 11 ++- .../Integrations/RabbitMqPublisher.cs | 11 +-- 10 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 Source/EventFlow.RabbitMQ.Tests/UnitTests/Integrations/RabbitMqPublisherTests.cs create mode 100644 Source/EventFlow.RabbitMQ/Integrations/IRabbitConnection.cs diff --git a/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj b/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj index cf115af1a..46f18c59a 100644 --- a/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj +++ b/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj @@ -41,6 +41,10 @@ ..\..\packages\FluentAssertions.3.5.0\lib\net45\FluentAssertions.Core.dll True + + ..\..\packages\Moq.4.2.1507.0118\lib\net40\Moq.dll + True + ..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll True @@ -49,6 +53,10 @@ ..\..\packages\NUnit.2.6.4\lib\nunit.framework.dll True + + ..\..\packages\AutoFixture.3.31.3\lib\net40\Ploeh.AutoFixture.dll + True + ..\..\packages\RabbitMQ.Client.3.5.4\lib\net40\RabbitMQ.Client.dll True @@ -66,6 +74,7 @@ + diff --git a/Source/EventFlow.RabbitMQ.Tests/UnitTests/Integrations/RabbitMqPublisherTests.cs b/Source/EventFlow.RabbitMQ.Tests/UnitTests/Integrations/RabbitMqPublisherTests.cs new file mode 100644 index 000000000..a616d423a --- /dev/null +++ b/Source/EventFlow.RabbitMQ.Tests/UnitTests/Integrations/RabbitMqPublisherTests.cs @@ -0,0 +1,93 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Core; +using EventFlow.Logs; +using EventFlow.RabbitMQ.Integrations; +using EventFlow.TestHelpers; +using Moq; +using NUnit.Framework; +using Ploeh.AutoFixture; +using RabbitMQ.Client; + +namespace EventFlow.RabbitMQ.Tests.UnitTests.Integrations +{ + public class RabbitMqPublisherTests : TestsFor + { + private Mock _rabbitMqConnectionFactoryMock; + private Mock _rabbitMqConfigurationMock; + private Mock _logMock; + private Mock _modelMock; + + [SetUp] + public void SetUp() + { + _rabbitMqConnectionFactoryMock = InjectMock(); + _rabbitMqConfigurationMock = InjectMock(); + _logMock = InjectMock(); + + Fixture.Inject>(new TransientFaultHandler( + _logMock.Object, + new RabbitMqRetryStrategy())); + + var basicPropertiesMock = new Mock(); + _modelMock = new Mock(); + var rabbitConnectionMock = new Mock(); + + _rabbitMqConnectionFactoryMock + .Setup(f => f.CreateConnectionAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(rabbitConnectionMock.Object)); + _rabbitMqConfigurationMock + .Setup(c => c.Uri) + .Returns(new Uri("amqp://localhost")); + rabbitConnectionMock + .Setup(c => c.WithModelAsync(It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>((a, c) => + { + a(_modelMock.Object).Wait(c); + }) + .Returns(Task.FromResult(0)); + _modelMock + .Setup(m => m.CreateBasicProperties()) + .Returns(basicPropertiesMock.Object); + } + + [Test] + public async Task PublishIsCalled() + { + // Arrange + var rabbitMqMessages = Fixture.CreateMany().ToList(); + + // Act + await Sut.PublishAsync(rabbitMqMessages, CancellationToken.None); + + // Assert + _modelMock.Verify( + m => m.BasicPublish(It.IsAny(), It.IsAny(), false, false, It.IsAny(), It.IsAny()), + Times.Exactly(rabbitMqMessages.Count)); + } + } +} diff --git a/Source/EventFlow.RabbitMQ.Tests/app.config b/Source/EventFlow.RabbitMQ.Tests/app.config index 7afb9c361..39a6ec36f 100644 --- a/Source/EventFlow.RabbitMQ.Tests/app.config +++ b/Source/EventFlow.RabbitMQ.Tests/app.config @@ -6,6 +6,10 @@ + + + + \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ.Tests/packages.config b/Source/EventFlow.RabbitMQ.Tests/packages.config index 1d5c74d22..14e60320a 100644 --- a/Source/EventFlow.RabbitMQ.Tests/packages.config +++ b/Source/EventFlow.RabbitMQ.Tests/packages.config @@ -1,6 +1,8 @@  + + diff --git a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj index 2ea455f3e..c30fbb3ac 100644 --- a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj +++ b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj @@ -52,6 +52,7 @@ + diff --git a/Source/EventFlow.RabbitMQ/Integrations/IRabbitConnection.cs b/Source/EventFlow.RabbitMQ/Integrations/IRabbitConnection.cs new file mode 100644 index 000000000..ea53293f5 --- /dev/null +++ b/Source/EventFlow.RabbitMQ/Integrations/IRabbitConnection.cs @@ -0,0 +1,34 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; + +namespace EventFlow.RabbitMQ.Integrations +{ + public interface IRabbitConnection : IDisposable + { + Task WithModelAsync(Func action, CancellationToken cancellationToken); + } +} diff --git a/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqConnectionFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqConnectionFactory.cs index 5028353d9..418fa4953 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqConnectionFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/IRabbitMqConnectionFactory.cs @@ -23,12 +23,11 @@ using System; using System.Threading; using System.Threading.Tasks; -using RabbitMQ.Client; namespace EventFlow.RabbitMQ.Integrations { public interface IRabbitMqConnectionFactory { - Task CreateConnectionAsync(Uri uri, CancellationToken cancellationToken); + Task CreateConnectionAsync(Uri uri, CancellationToken cancellationToken); } } diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs index be4cfea00..564fa40be 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitConnection.cs @@ -32,7 +32,7 @@ namespace EventFlow.RabbitMQ.Integrations { - public class RabbitConnection : IDisposable + public class RabbitConnection : IRabbitConnection { private readonly ILog _log; private readonly IConnection _connection; diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs index 17c119c4a..0572a2477 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs @@ -33,19 +33,24 @@ namespace EventFlow.RabbitMQ.Integrations public class RabbitMqConnectionFactory : IRabbitMqConnectionFactory { private readonly ILog _log; + private readonly IRabbitMqConfiguration _configuration; private readonly AsyncLock _asyncLock = new AsyncLock(); private readonly Dictionary _connectionFactories = new Dictionary(); public RabbitMqConnectionFactory( - ILog log) + ILog log, + IRabbitMqConfiguration configuration) { _log = log; + _configuration = configuration; } - public async Task CreateConnectionAsync(Uri uri, CancellationToken cancellationToken) + public async Task CreateConnectionAsync(Uri uri, CancellationToken cancellationToken) { var connectionFactory = await CreateConnectionFactoryAsync(uri, cancellationToken).ConfigureAwait(false); - return connectionFactory.CreateConnection(); + var connection = connectionFactory.CreateConnection(); + + return new RabbitConnection(_log, _configuration.ModelsPrConnection, connection); } private async Task CreateConnectionFactoryAsync(Uri uri, CancellationToken cancellationToken) diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index 8442c6893..c0b1e0137 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -40,7 +40,7 @@ public class RabbitMqPublisher : IDisposable, IRabbitMqPublisher private readonly IRabbitMqConfiguration _configuration; private readonly ITransientFaultHandler _transientFaultHandler; private readonly AsyncLock _asyncLock = new AsyncLock(); - private readonly Dictionary _connections = new Dictionary(); + private readonly Dictionary _connections = new Dictionary(); public RabbitMqPublisher( ILog log, @@ -62,7 +62,7 @@ public Task PublishAsync(CancellationToken cancellationToken, params RabbitMqMes public async Task PublishAsync(IReadOnlyCollection rabbitMqMessages, CancellationToken cancellationToken) { var uri = _configuration.Uri; - RabbitConnection rabbitConnection = null; + IRabbitConnection rabbitConnection = null; try { rabbitConnection = await GetRabbitMqConnectionAsync(uri, cancellationToken).ConfigureAwait(false); @@ -92,18 +92,17 @@ await _transientFaultHandler.TryAsync( } } - private async Task GetRabbitMqConnectionAsync(Uri uri, CancellationToken cancellationToken) + private async Task GetRabbitMqConnectionAsync(Uri uri, CancellationToken cancellationToken) { using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) { - RabbitConnection rabbitConnection; + IRabbitConnection rabbitConnection; if (_connections.TryGetValue(uri, out rabbitConnection)) { return rabbitConnection; } - var connection = await _connectionFactory.CreateConnectionAsync(uri, cancellationToken).ConfigureAwait(false); - rabbitConnection = new RabbitConnection(_log, _configuration.ModelsPrConnection, connection); + rabbitConnection = await _connectionFactory.CreateConnectionAsync(uri, cancellationToken).ConfigureAwait(false); _connections.Add(uri, rabbitConnection); return rabbitConnection; From 2a7cf1416545b54c2ec6bc69c7651945ebf2db39 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 21 Aug 2015 20:45:27 +0200 Subject: [PATCH 36/54] Check that dispose is called on the connection --- .../Integrations/RabbitMqPublisherTests.cs | 42 ++++++++++++++++--- .../Integrations/RabbitMqPublisher.cs | 6 ++- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/Source/EventFlow.RabbitMQ.Tests/UnitTests/Integrations/RabbitMqPublisherTests.cs b/Source/EventFlow.RabbitMQ.Tests/UnitTests/Integrations/RabbitMqPublisherTests.cs index a616d423a..148a4189d 100644 --- a/Source/EventFlow.RabbitMQ.Tests/UnitTests/Integrations/RabbitMqPublisherTests.cs +++ b/Source/EventFlow.RabbitMQ.Tests/UnitTests/Integrations/RabbitMqPublisherTests.cs @@ -41,6 +41,7 @@ public class RabbitMqPublisherTests : TestsFor private Mock _rabbitMqConfigurationMock; private Mock _logMock; private Mock _modelMock; + private Mock _rabbitConnectionMock; [SetUp] public void SetUp() @@ -55,30 +56,43 @@ public void SetUp() var basicPropertiesMock = new Mock(); _modelMock = new Mock(); - var rabbitConnectionMock = new Mock(); + _rabbitConnectionMock = new Mock(); _rabbitMqConnectionFactoryMock .Setup(f => f.CreateConnectionAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(rabbitConnectionMock.Object)); + .Returns(Task.FromResult(_rabbitConnectionMock.Object)); _rabbitMqConfigurationMock .Setup(c => c.Uri) .Returns(new Uri("amqp://localhost")); - rabbitConnectionMock + _modelMock + .Setup(m => m.CreateBasicProperties()) + .Returns(basicPropertiesMock.Object); + } + + private void ArrangeWorkingConnection() + { + _rabbitConnectionMock .Setup(c => c.WithModelAsync(It.IsAny>(), It.IsAny())) .Callback, CancellationToken>((a, c) => { a(_modelMock.Object).Wait(c); }) .Returns(Task.FromResult(0)); - _modelMock - .Setup(m => m.CreateBasicProperties()) - .Returns(basicPropertiesMock.Object); + } + + private void ArrangeBrokenConnection() + where TException : Exception, new() + { + _rabbitConnectionMock + .Setup(c => c.WithModelAsync(It.IsAny>(), It.IsAny())) + .Throws(); } [Test] public async Task PublishIsCalled() { // Arrange + ArrangeWorkingConnection(); var rabbitMqMessages = Fixture.CreateMany().ToList(); // Act @@ -88,6 +102,22 @@ public async Task PublishIsCalled() _modelMock.Verify( m => m.BasicPublish(It.IsAny(), It.IsAny(), false, false, It.IsAny(), It.IsAny()), Times.Exactly(rabbitMqMessages.Count)); + _rabbitConnectionMock.Verify(c => c.Dispose(), Times.Never); + } + + [Test] + public void ConnectionIsDisposedOnException() + { + // Arrange + ArrangeBrokenConnection(); + var rabbitMqMessages = Fixture.CreateMany().ToList(); + + // Act + Assert.Throws( + async () => await Sut.PublishAsync(rabbitMqMessages, CancellationToken.None)); + + // Assert + _rabbitConnectionMock.Verify(c => c.Dispose(), Times.Once); } } } diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs index c0b1e0137..d531657a1 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqPublisher.cs @@ -68,7 +68,7 @@ public async Task PublishAsync(IReadOnlyCollection rabbitMqMess rabbitConnection = await GetRabbitMqConnectionAsync(uri, cancellationToken).ConfigureAwait(false); await _transientFaultHandler.TryAsync( - c => rabbitConnection.WithModelAsync(m => PublishAsync(m, rabbitMqMessages, c), c), + c => rabbitConnection.WithModelAsync(m => PublishAsync(m, rabbitMqMessages), c), Label.Named("rabbitmq-publish"), cancellationToken) .ConfigureAwait(false); @@ -109,7 +109,9 @@ private async Task GetRabbitMqConnectionAsync(Uri uri, Cancel } } - private Task PublishAsync(IModel model, IReadOnlyCollection messages, CancellationToken cancellationToken) + private Task PublishAsync( + IModel model, + IReadOnlyCollection messages) { _log.Verbose( "Publishing {0} domain domain events to RabbitMQ host '{1}'", From 61ae853335f3ccd8e652ff03feb0a2d757fef5b8 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 21 Aug 2015 21:38:10 +0200 Subject: [PATCH 37/54] Minor text fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ddb4fe47e..f32633889 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ the [dos and don'ts](./Documentation/DoesAndDonts.md) and the * **Highly configurable and extendable** * **Easy to use** * **No use of threads or background workers making it "web friendly"** -* **Cancellation:** All methods that does IO work or might delay execution, - takes a `CancellationToken` argument to allow you to cancel the operation +* **Cancellation:** All methods that does IO work or might delay execution (due to + retries), takes a `CancellationToken` argument to allow you to cancel the operation ### Overview From b69da195d10dc6ae7bfee0b54550fbd557419298 Mon Sep 17 00:00:00 2001 From: Jaco Coetzee Date: Sat, 22 Aug 2015 13:46:47 +0200 Subject: [PATCH 38/54] Added an AutofacAggregateFactory to EventFlow.Autofac Added: - RegisterType() method to IServiceRegistration to simplify registering by type - support for parameters in AutofacResolver.Resolve methods using overloads - UseAutofacAggregateFactory() to EventFlowOptionsAutofacExtensions --- EventFlow.sln | 6 + .../EventFlow.Autofac.Tests.csproj | 116 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 36 ++++++ .../Aggregates/AggregateFactoryTests.cs | 104 ++++++++++++++++ Source/EventFlow.Autofac.Tests/app.config | 11 ++ .../EventFlow.Autofac.Tests/packages.config | 6 + .../Aggregates/AggregateFactory.cs | 49 ++++++++ .../EventFlow.Autofac.csproj | 2 + .../EventFlowOptionsAutofacExtensions.cs | 9 ++ Source/EventFlow.Tests/EventFlow.Tests.csproj | 3 + .../Configuration/IServiceRegistration.cs | 2 + .../Registrations/AutofacResolver.cs | 11 ++ .../AutofacServiceRegistration.cs | 5 + 13 files changed, 360 insertions(+) create mode 100644 Source/EventFlow.Autofac.Tests/EventFlow.Autofac.Tests.csproj create mode 100644 Source/EventFlow.Autofac.Tests/Properties/AssemblyInfo.cs create mode 100644 Source/EventFlow.Autofac.Tests/UnitTests/Aggregates/AggregateFactoryTests.cs create mode 100644 Source/EventFlow.Autofac.Tests/app.config create mode 100644 Source/EventFlow.Autofac.Tests/packages.config create mode 100644 Source/EventFlow.Autofac/Aggregates/AggregateFactory.cs diff --git a/EventFlow.sln b/EventFlow.sln index ba7bc05fd..2c035b172 100644 --- a/EventFlow.sln +++ b/EventFlow.sln @@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.RabbitMQ", "Sourc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.RabbitMQ.Tests", "Source\EventFlow.RabbitMQ.Tests\EventFlow.RabbitMQ.Tests.csproj", "{BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.Autofac.Tests", "Source\EventFlow.Autofac.Tests\EventFlow.Autofac.Tests.csproj", "{EDCD8854-6224-4329-87C2-9ADD7D153070}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -101,6 +103,10 @@ Global {BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A}.Release|Any CPU.Build.0 = Release|Any CPU + {EDCD8854-6224-4329-87C2-9ADD7D153070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDCD8854-6224-4329-87C2-9ADD7D153070}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDCD8854-6224-4329-87C2-9ADD7D153070}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDCD8854-6224-4329-87C2-9ADD7D153070}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/EventFlow.Autofac.Tests/EventFlow.Autofac.Tests.csproj b/Source/EventFlow.Autofac.Tests/EventFlow.Autofac.Tests.csproj new file mode 100644 index 000000000..10a1aaf97 --- /dev/null +++ b/Source/EventFlow.Autofac.Tests/EventFlow.Autofac.Tests.csproj @@ -0,0 +1,116 @@ + + + + Debug + AnyCPU + {EDCD8854-6224-4329-87C2-9ADD7D153070} + Library + Properties + EventFlow.Autofac.Tests + EventFlow.Autofac.Tests + v4.5.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + True + + + ..\..\packages\FluentAssertions.3.5.0\lib\net45\FluentAssertions.dll + True + + + ..\..\packages\FluentAssertions.3.5.0\lib\net45\FluentAssertions.Core.dll + True + + + ..\..\packages\NUnit.2.6.4\lib\nunit.framework.dll + True + + + + + + + + + + + + + + + + + + + + + {26f06682-3364-4c22-b9b2-2f2653d0be0d} + EventFlow.Autofac + + + {571d291c-5e4c-43af-855f-7c4e2f318f4c} + EventFlow.TestHelpers + + + {11131251-778d-4d2e-bdd1-4844a789bca9} + EventFlow + + + + + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.Autofac.Tests/Properties/AssemblyInfo.cs b/Source/EventFlow.Autofac.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..abbfb612f --- /dev/null +++ b/Source/EventFlow.Autofac.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("EventFlow.Autofac.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("EventFlow.Autofac.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("edcd8854-6224-4329-87c2-9add7d153070")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Source/EventFlow.Autofac.Tests/UnitTests/Aggregates/AggregateFactoryTests.cs b/Source/EventFlow.Autofac.Tests/UnitTests/Aggregates/AggregateFactoryTests.cs new file mode 100644 index 000000000..e12b2fb4a --- /dev/null +++ b/Source/EventFlow.Autofac.Tests/UnitTests/Aggregates/AggregateFactoryTests.cs @@ -0,0 +1,104 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen, Jaco Coetzee +// 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 Autofac; +using EventFlow.Aggregates; +using EventFlow.Autofac.Extensions; +using EventFlow.Configuration; +using EventFlow.TestHelpers.Aggregates.Test; +using FluentAssertions; +using NUnit.Framework; + +namespace EventFlow.Autofac.Tests.UnitTests.Aggregates +{ + [TestFixture] + public class AggregateFactoryTests + { + [Test] + public async void AutofacAggregateFactoryResolvesConstructorParameters() + { + // Arrange + var containerBuilder = new ContainerBuilder(); + using (var resolver = EventFlowOptions.New + .UseAutofacContainerBuilder(containerBuilder) + .UseAutofacAggregateFactory() + .RegisterServices(f => f.RegisterType(typeof(TestAggregate))) + .RegisterServices(f => f.RegisterType(typeof(TestAggregateWithResolver))) + .RegisterServices(f => f.RegisterType(typeof(TestAggregateWithPinger))) + .RegisterServices(f => f.RegisterType(typeof(Pinger))) + .CreateResolver(false)) + { + // Arrange + var id = TestId.New; + var sut = resolver.Resolve(); + + // Act + var a = await sut.CreateNewAggregateAsync(id); + var ar = await sut.CreateNewAggregateAsync(id); + var ap = await sut.CreateNewAggregateAsync(id); + + // Assert + a.Id.Should().Be(id); + ar.Resolver.Should().BeAssignableTo(); + ap.Pinger.Should().BeOfType(); + } + } + + public class Pinger + { + public void Ping() + { + // void + } + } + + public class TestAggregate : AggregateRoot + { + public TestAggregate(TestId id) + : base(id) + { + } + } + + public class TestAggregateWithPinger : AggregateRoot + { + public TestAggregateWithPinger(TestId id, Pinger pinger) + : base(id) + { + Pinger = pinger; + } + + public Pinger Pinger { get; } + } + + public class TestAggregateWithResolver : AggregateRoot + { + public TestAggregateWithResolver(TestId id, IResolver resolver) + : base(id) + { + Resolver = resolver; + } + + public IResolver Resolver { get; } + } + } +} diff --git a/Source/EventFlow.Autofac.Tests/app.config b/Source/EventFlow.Autofac.Tests/app.config new file mode 100644 index 000000000..7afb9c361 --- /dev/null +++ b/Source/EventFlow.Autofac.Tests/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.Autofac.Tests/packages.config b/Source/EventFlow.Autofac.Tests/packages.config new file mode 100644 index 000000000..1e2ac56ac --- /dev/null +++ b/Source/EventFlow.Autofac.Tests/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.Autofac/Aggregates/AggregateFactory.cs b/Source/EventFlow.Autofac/Aggregates/AggregateFactory.cs new file mode 100644 index 000000000..c9387d653 --- /dev/null +++ b/Source/EventFlow.Autofac/Aggregates/AggregateFactory.cs @@ -0,0 +1,49 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// 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 EventFlow.Aggregates; +using EventFlow.Configuration; +using Autofac; +using System.Threading.Tasks; +using EventFlow.Configuration.Registrations; + +namespace EventFlow.Autofac.Aggregates +{ + public class AggregateFactory : IAggregateFactory + { + private AutofacResolver _resolver; + + public AggregateFactory(IResolver resolver) + { + _resolver = (AutofacResolver)resolver; + } + + public Task CreateNewAggregateAsync(TIdentity id) + where TAggregate : IAggregateRoot + where TIdentity : IIdentity + { + var aggregate = _resolver.Resolve(new TypedParameter(typeof(TIdentity), id)); + return Task.FromResult(aggregate); + } + } +} + diff --git a/Source/EventFlow.Autofac/EventFlow.Autofac.csproj b/Source/EventFlow.Autofac/EventFlow.Autofac.csproj index 600a700e8..554bcc08d 100644 --- a/Source/EventFlow.Autofac/EventFlow.Autofac.csproj +++ b/Source/EventFlow.Autofac/EventFlow.Autofac.csproj @@ -63,6 +63,7 @@ Properties\SolutionInfo.cs + @@ -76,6 +77,7 @@ EventFlow +