diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c00a66eb7..c6c31cc5d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,24 @@ -### New in 0.22 (not released yet) +### New in 0.23 (not released yet) + +* Breaking: EventFlow no longer ignores columns named `Id` in MSSQL read models. + If you were dependent on this, use the `MsSqlReadModelIgnoreColumn` attribute +* Fixed: Instead of using `MethodInfo.Invoke` to call methods on reflected + types, e.g. when a command is published, EventFlow now compiles an expression + tree instead. This has a slight initial overhead, but provides a significant + performance improvement for subsequent calls +* Fixed: Read model stores are only invoked if there's any read model updates +* Fixed: EventFlow now correctly throws an `ArgumentException` if EventFlow has + been incorrectly configure with known versioned types, e.g. an event + is emitted that hasn't been added during EventFlow initialization. EventFlow + would handle the save operation correctly, but if EventFlow was reinitialized + and the event was loaded _before_ it being emitted again, an exception would + be thrown as EventFlow would know which type to use. Please make sure to + correctly load all event, command and job types before use +* Fixed: `IReadModelFactory<>.CreateAsync(...)` is now correctly used in + read store mangers +* Fixed: Versioned type naming convention now allows numbers + +### New in 0.22.1393 (released 2015-11-19) * New: To customize how a specific read model is initially created, implement a specific `IReadModelFactory<>` that can bootstrap that read model diff --git a/Source/EventFlow.Hangfire/Integration/HangfireJobScheduler.cs b/Source/EventFlow.Hangfire/Integration/HangfireJobScheduler.cs index 9cfd245b4..000b3d863 100644 --- a/Source/EventFlow.Hangfire/Integration/HangfireJobScheduler.cs +++ b/Source/EventFlow.Hangfire/Integration/HangfireJobScheduler.cs @@ -67,7 +67,7 @@ public Task ScheduleAsync(IJob job, TimeSpan delay, CancellationToken ca private Task ScheduleAsync(IJob job, Func schedule) { - var jobDefinition = _jobDefinitionService.GetJobDefinition(job.GetType()); + var jobDefinition = _jobDefinitionService.GetDefinition(job.GetType()); var json = _jsonSerializer.Serialize(job); var id = schedule(_backgroundJobClient, jobDefinition, json); diff --git a/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs b/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs index 829d549d1..0d7434eb5 100644 --- a/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs +++ b/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs @@ -21,68 +21,74 @@ // 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.ComponentModel.DataAnnotations.Schema; -using EventFlow.MsSql.Tests.IntegrationTests.ReadStores.ReadModels; +using EventFlow.ReadStores; using EventFlow.ReadStores.MsSql; +using EventFlow.ReadStores.MsSql.Attributes; using EventFlow.TestHelpers; using FluentAssertions; using NUnit.Framework; -#pragma warning disable 618 namespace EventFlow.MsSql.Tests.UnitTests.ReadModels { public class ReadModelSqlGeneratorTests : TestsFor { - [Table("FancyTable")] - public class TestTableAttribute : MssqlReadModel { } - [Test] public void CreateInsertSql_ProducesCorrectSql() { // Act - var sql = Sut.CreateInsertSql(); + var sql = Sut.CreateInsertSql(); // Assert - sql.Should().Be( - "INSERT INTO [ReadModel-ThingyAggregate] " + - "(AggregateId, CreateTime, DomainErrorAfterFirstReceived, LastAggregateSequenceNumber, PingsReceived, UpdatedTime) " + - "VALUES " + - "(@AggregateId, @CreateTime, @DomainErrorAfterFirstReceived, @LastAggregateSequenceNumber, @PingsReceived, @UpdatedTime)"); + sql.Should().Be("INSERT INTO [ReadModel-TestAttributes] (Id, UpdatedTime) VALUES (@Id, @UpdatedTime)"); } [Test] public void CreateUpdateSql_ProducesCorrectSql() { // Act - var sql = Sut.CreateUpdateSql(); + var sql = Sut.CreateUpdateSql(); // Assert - sql.Should().Be( - "UPDATE [ReadModel-ThingyAggregate] SET " + - "CreateTime = @CreateTime, DomainErrorAfterFirstReceived = @DomainErrorAfterFirstReceived, " + - "LastAggregateSequenceNumber = @LastAggregateSequenceNumber, " + - "PingsReceived = @PingsReceived, UpdatedTime = @UpdatedTime " + - "WHERE AggregateId = @AggregateId"); + sql.Should().Be("UPDATE [ReadModel-TestAttributes] SET UpdatedTime = @UpdatedTime WHERE Id = @Id"); } [Test] public void CreateSelectSql_ProducesCorrectSql() { // Act - var sql = Sut.CreateSelectSql(); + var sql = Sut.CreateSelectSql(); // Assert - sql.Should().Be("SELECT * FROM [ReadModel-ThingyAggregate] WHERE AggregateId = @EventFlowReadModelId"); + sql.Should().Be("SELECT * FROM [ReadModel-TestAttributes] WHERE Id = @EventFlowReadModelId"); } [Test] public void GetTableName_UsesTableAttribute() { // Act - var tableName = Sut.GetTableName(); + var tableName = Sut.GetTableName(); // Assert - tableName.Should().Be("[FancyTable]"); + tableName.Should().Be("[Fancy]"); + } + + public class TestAttributesReadModel : IReadModel + { + [MsSqlReadModelIdentityColumn] + public string Id { get; set; } + + public DateTimeOffset UpdatedTime { get; set; } + + [MsSqlReadModelIgnoreColumn] + public string Secret { get; set; } + } + + [Table("Fancy")] + public class TestTableAttributeReadModel : IReadModel + { } } -} +} \ No newline at end of file diff --git a/Source/EventFlow.ReadStores.MsSql/Attributes/MsSqlReadModelIgnoreColumnAttribute.cs b/Source/EventFlow.ReadStores.MsSql/Attributes/MsSqlReadModelIgnoreColumnAttribute.cs new file mode 100644 index 000000000..2ade31b73 --- /dev/null +++ b/Source/EventFlow.ReadStores.MsSql/Attributes/MsSqlReadModelIgnoreColumnAttribute.cs @@ -0,0 +1,33 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// Copyright (c) 2015 eBay Software Foundation +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace EventFlow.ReadStores.MsSql.Attributes +{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class MsSqlReadModelIgnoreColumnAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/Source/EventFlow.ReadStores.MsSql/EventFlow.ReadStores.MsSql.csproj b/Source/EventFlow.ReadStores.MsSql/EventFlow.ReadStores.MsSql.csproj index 5c99c607f..cae1345d3 100644 --- a/Source/EventFlow.ReadStores.MsSql/EventFlow.ReadStores.MsSql.csproj +++ b/Source/EventFlow.ReadStores.MsSql/EventFlow.ReadStores.MsSql.csproj @@ -50,6 +50,7 @@ Properties\SolutionInfo.cs + diff --git a/Source/EventFlow.ReadStores.MsSql/ReadModelSqlGenerator.cs b/Source/EventFlow.ReadStores.MsSql/ReadModelSqlGenerator.cs index 5dbd62a21..19c155a37 100644 --- a/Source/EventFlow.ReadStores.MsSql/ReadModelSqlGenerator.cs +++ b/Source/EventFlow.ReadStores.MsSql/ReadModelSqlGenerator.cs @@ -115,7 +115,6 @@ protected IEnumerable GetInsertColumns() where TReadModel : IReadModel { return GetPropertyInfos(typeof (TReadModel)) - .Where(p => p.Name != "Id") // TODO: Maybe use the key attribute to mark this .Select(p => p.Name); } @@ -165,6 +164,7 @@ protected IReadOnlyCollection GetPropertyInfos(Type readModelType) { return t .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.GetCustomAttribute() == null) .OrderBy(p => p.Name) .ToList(); }); diff --git a/Source/EventFlow.TestHelpers/Aggregates/Commands/ThingyImportCommand.cs b/Source/EventFlow.TestHelpers/Aggregates/Commands/ThingyImportCommand.cs new file mode 100644 index 000000000..905f46fa8 --- /dev/null +++ b/Source/EventFlow.TestHelpers/Aggregates/Commands/ThingyImportCommand.cs @@ -0,0 +1,71 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// Copyright (c) 2015 eBay Software Foundation +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Commands; +using EventFlow.TestHelpers.Aggregates.Entities; +using EventFlow.TestHelpers.Aggregates.ValueObjects; + +namespace EventFlow.TestHelpers.Aggregates.Commands +{ + public class ThingyImportCommand : Command + { + public IReadOnlyCollection PingIds { get; } + public IReadOnlyCollection ThingyMessages { get; } + + public ThingyImportCommand( + ThingyId aggregateId, + IEnumerable pingIds, + IEnumerable thingyMessages) + : base(aggregateId) + { + PingIds = pingIds.ToList(); + ThingyMessages = thingyMessages.ToList(); + } + } + + public class ThingyImportCommandHandler : CommandHandler + { + public override Task ExecuteAsync( + ThingyAggregate aggregate, + ThingyImportCommand command, + CancellationToken cancellationToken) + { + foreach (var pingId in command.PingIds) + { + aggregate.Ping(pingId); + } + + foreach (var thingyMessage in command.ThingyMessages) + { + aggregate.AddMessage(thingyMessage); + } + + return Task.FromResult(0); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj b/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj index 0fce90091..0777e0d39 100644 --- a/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj +++ b/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj @@ -70,6 +70,7 @@ + diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs index 33d15419e..82b0266e4 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs @@ -29,6 +29,7 @@ using EventFlow.TestHelpers.Aggregates.Commands; using EventFlow.TestHelpers.Aggregates.Entities; using EventFlow.TestHelpers.Aggregates.Queries; +using EventFlow.TestHelpers.Aggregates.ValueObjects; using EventFlow.TestHelpers.Extensions; using FluentAssertions; using NUnit.Framework; @@ -111,6 +112,28 @@ public async Task CanStoreMultipleMessages() returnedThingyMessages.ShouldAllBeEquivalentTo(thingyMessages); } + [Test] + public async Task CanHandleMultipleMessageAtOnce() + { + // Arrange + var thingyId = ThingyId.New; + var pingIds = Many(5); + var thingyMessages = Many(7); + + // Act + await CommandBus.PublishAsync(new ThingyImportCommand( + thingyId, + pingIds, + thingyMessages)) + .ConfigureAwait(false); + var returnedThingyMessages = await QueryProcessor.ProcessAsync(new ThingyGetMessagesQuery(thingyId)).ConfigureAwait(false); + var thingy = await QueryProcessor.ProcessAsync(new ThingyGetQuery(thingyId)).ConfigureAwait(false); + + // Assert + thingy.PingsReceived.Should().Be(pingIds.Count); + returnedThingyMessages.ShouldAllBeEquivalentTo(returnedThingyMessages); + } + [Test] public async Task PurgeRemovesReadModels() { diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index 4859ff901..ec0c38ddd 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -92,9 +92,12 @@ + + + @@ -112,7 +115,10 @@ - + + + + diff --git a/Source/EventFlow.Tests/UnitTests/Commands/CommandDefinitionServiceTests.cs b/Source/EventFlow.Tests/UnitTests/Commands/CommandDefinitionServiceTests.cs index e3689d413..c8801564c 100644 --- a/Source/EventFlow.Tests/UnitTests/Commands/CommandDefinitionServiceTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Commands/CommandDefinitionServiceTests.cs @@ -21,18 +21,18 @@ // 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 EventFlow.Aggregates; using EventFlow.Commands; using EventFlow.Core; -using EventFlow.TestHelpers; -using FluentAssertions; +using EventFlow.Tests.UnitTests.Core.VersionedTypes; using NUnit.Framework; namespace EventFlow.Tests.UnitTests.Commands { [TestFixture] - public class CommandDefinitionServiceTests : TestsFor + public class CommandDefinitionServiceTests : VersionedTypeDefinitionServiceTestSuite { [CommandVersion("Fancy", 42)] public class TestCommandWithLongName : Command, IIdentity> @@ -55,50 +55,32 @@ public class OldTestCommandV5 : Command, IIdentity> public OldTestCommandV5(IIdentity aggregateId) : base(aggregateId) { } } - [TestCase(typeof(TestCommand), 1, "TestCommand")] - [TestCase(typeof(TestCommandV2), 2, "TestCommand")] - [TestCase(typeof(OldTestCommandV5), 5, "TestCommand")] - [TestCase(typeof(TestCommandWithLongName), 42, "Fancy")] - public void GetCommandDefinition_EventWithVersion(Type commandType, int expectedVersion, string expectedName) + public override IEnumerable GetTestCases() { - // Act - var commandDefinition = Sut.GetCommandDefinition(commandType); - - // Assert - commandDefinition.Name.Should().Be(expectedName); - commandDefinition.Version.Should().Be(expectedVersion); - commandDefinition.Type.Should().Be(commandType); - } - - [TestCase("TestCommand", 1, typeof(TestCommand))] - [TestCase("TestCommand", 2, typeof(TestCommandV2))] - [TestCase("TestCommand", 5, typeof(OldTestCommandV5))] - [TestCase("Fancy", 42, typeof(TestCommandWithLongName))] - public void LoadCommandFollowedByGetCommandDefinition_ReturnsCorrectAnswer(string commandName, int commandVersion, Type expectedCommandType) - { - // Arrange - Sut.LoadCommands(new [] + yield return new VersionTypeTestCase { - typeof(TestCommand), - typeof(TestCommandV2), - typeof(OldTestCommandV5), - typeof(TestCommandWithLongName) - }); - - // Act - var commandDefinition = Sut.GetCommandDefinition(commandName, commandVersion); - - // Assert - commandDefinition.Name.Should().Be(commandName); - commandDefinition.Version.Should().Be(commandVersion); - commandDefinition.Type.Should().Be(expectedCommandType); - } - - [Test] - public void CanLoadNull() - { - // Act - Sut.LoadCommands(null); + Name = "TestCommand", + Type = typeof(TestCommand), + Version = 1, + }; + yield return new VersionTypeTestCase + { + Name = "TestCommand", + Type = typeof(TestCommandV2), + Version = 2, + }; + yield return new VersionTypeTestCase + { + Name = "TestCommand", + Type = typeof(OldTestCommandV5), + Version = 5, + }; + yield return new VersionTypeTestCase + { + Name = "Fancy", + Type = typeof(TestCommandWithLongName), + Version = 42, + }; } } -} +} \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/Core/ReflectionHelperTests.cs b/Source/EventFlow.Tests/UnitTests/Core/ReflectionHelperTests.cs new file mode 100644 index 000000000..edd9331fd --- /dev/null +++ b/Source/EventFlow.Tests/UnitTests/Core/ReflectionHelperTests.cs @@ -0,0 +1,102 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// Copyright (c) 2015 eBay Software Foundation +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using EventFlow.Core; +using FluentAssertions; +using NUnit.Framework; + +namespace EventFlow.Tests.UnitTests.Core +{ + public class ReflectionHelperTests + { + [Test] + public void CompileMethodInvocation() + { + // Act + var caller = ReflectionHelper.CompileMethodInvocation>(typeof (Calculator), "Add", typeof(int), typeof(int)); + var result = caller(new Calculator(), 1, 2); + + // Assert + result.Should().Be(3); + } + + [Test] + public void CompileMethodInvocation_CanUpcast() + { + // Arrange + var a = (INumber) new Number {I = 1}; + var b = (INumber) new Number {I = 2}; + + // Act + var caller = ReflectionHelper.CompileMethodInvocation>(typeof(Calculator), "Add", typeof(Number), typeof(Number)); + var result = caller(new Calculator(), a, b); + + // Assert + var c = (Number) result; + c.I.Should().Be(3); + } + + [Test] + public void CompileMethodInvocation_CanDoBothUpcastAndPass() + { + // Arrange + var a = (INumber)new Number { I = 1 }; + const int b = 2; + + // Act + var caller = ReflectionHelper.CompileMethodInvocation>(typeof(Calculator), "Add", typeof(Number), typeof(int)); + var result = caller(new Calculator(), a, b); + + // Assert + var c = (Number)result; + c.I.Should().Be(3); + } + + public interface INumber { } + + public class Number : INumber + { + public int I { get; set; } + } + + public class Calculator + { + public int Add(int a, int b) + { + return a + b; + } + + public Number Add(Number a, Number b) + { + return new Number {I = Add(a.I, b.I)}; + } + + public Number Add(Number a, int b) + { + return new Number { I = Add(a.I, b) }; + } + } + } +} diff --git a/Source/EventFlow.Tests/UnitTests/Core/VersionedTypes/VersionedTypeDefinitionServiceTestSuite.cs b/Source/EventFlow.Tests/UnitTests/Core/VersionedTypes/VersionedTypeDefinitionServiceTestSuite.cs new file mode 100644 index 000000000..8f27ad61a --- /dev/null +++ b/Source/EventFlow.Tests/UnitTests/Core/VersionedTypes/VersionedTypeDefinitionServiceTestSuite.cs @@ -0,0 +1,214 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// Copyright (c) 2015 eBay Software Foundation +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using EventFlow.Core.VersionedTypes; +using EventFlow.TestHelpers; +using FluentAssertions; +using NUnit.Framework; +using Ploeh.AutoFixture; + +namespace EventFlow.Tests.UnitTests.Core.VersionedTypes +{ + public abstract class VersionedTypeDefinitionServiceTestSuite : TestsFor + where TSut : VersionedTypeDefinitionService + where TAttribute : VersionedTypeAttribute + where TDefinition : VersionedTypeDefinition + { + public class VersionTypeTestCase + { + public Type Type { get; set; } + public int Version { get; set; } + public string Name { get; set; } + } + + [TestCaseSource(nameof(GetTestCases))] + public void GetDefinition_WithValidNameAndVersion_ReturnsCorrectAnswer(VersionTypeTestCase testCase) + { + // Arrange + Arrange_LoadAllTestTypes(); + + // Act + var eventDefinition = Sut.GetDefinition(testCase.Name, testCase.Version); + + // Assert + eventDefinition.Name.Should().Be(testCase.Name); + eventDefinition.Version.Should().Be(testCase.Version); + eventDefinition.Type.Should().Be(testCase.Type); + } + + [TestCaseSource(nameof(GetTestCases))] + public void GetDefinition_WithValidType_ReturnsCorrectAnswer(VersionTypeTestCase testCase) + { + // Arrange + Arrange_LoadAllTestTypes(); + + // Act + var eventDefinition = Sut.GetDefinition(testCase.Type); + + // Assert + eventDefinition.Name.Should().Be(testCase.Name); + eventDefinition.Version.Should().Be(testCase.Version); + eventDefinition.Type.Should().Be(testCase.Type); + } + + [Test] + public void GetDefinitions_WithName_ReturnsList() + { + // Assert + Arrange_LoadAllTestTypes(); + var nameWithMultipleDefinitions = GetTestCases() + .GroupBy(c => c.Name) + .Where(g => g.Count() > 1) + .OrderByDescending(g => g.Count()) + .First().Key; + + // Assert + var result = Sut.GetDefinitions(nameWithMultipleDefinitions).ToList(); + + // Assert + result.Should().HaveCount(i => i > 1); + result.Should().OnlyContain(d => d.Name == nameWithMultipleDefinitions); + } + + [Test] + public void GetDefinitionShouldFailForUnknownEvents() + { + // Act + Assert + Assert.Throws(() => Sut.GetDefinition(GetTestCases().First().Type)); + } + + [Test] + public void CanLoadSameEventMultipleTimes() + { + // Arrange + var types = GetTestCases().Select(c => c.Type).ToList(); + + // Act + Assert.DoesNotThrow(() => + { + Sut.Load(types); + Sut.Load(types); + }); + } + + [Test] + public void TryGetDefinition_WithInvalidName_ReturnsFalse() + { + // Arrange + TDefinition definition; + + // Act + var found = Sut.TryGetDefinition(Fixture.Create(), 0, out definition); + + // Assert + found.Should().BeFalse(); + } + + [Test] + public void TryGetDefinition_WithInvalidType_ReturnsFalse() + { + // Arrange + TDefinition definition; + + // Act + var found = Sut.TryGetDefinition(typeof(object), out definition); + + // Assert + found.Should().BeFalse(); + } + + [Test] + public void GetDefinition_WithInvalidType_ThrowsException() + { + // Act + Assert + Assert.Throws(() => Sut.GetDefinition(typeof(object))); + } + + [Test] + public void GetDefinition_WithInvalidName_ThrowsException() + { + // Act + Assert + Assert.Throws(() => Sut.GetDefinition(Fixture.Create(), 0)); + } + + [Test] + public void GetDefinitions_WithInvalidName_ReturnsEmpty() + { + // Act + var result = Sut.GetDefinitions(Fixture.Create()); + + // Assert + result.Should().BeEmpty(); + } + + [Test] + public void GetAllDefinitions_WhenNoneLoaded_IsEmpty() + { + // Act + var result = Sut.GetAllDefinitions(); + + // Assert + result.Should().BeEmpty(); + } + + [Test] + public void GetAllDefinitions_WhenAllLoaded_ReturnsAll() + { + // Arrange + var expectedTypes = Arrange_LoadAllTestTypes(); + + // Act + var result = Sut.GetAllDefinitions().Select(d => d.Type).ToList(); + + // Assert + result.ShouldAllBeEquivalentTo(expectedTypes); + } + + [Test] + public void Load_CalledWithInvalidType_ThrowsException() + { + // Act + Assert + Assert.Throws(() => Sut.Load(typeof (object))); + } + + [Test] + public void CanLoadNull() + { + // Act + Assert + Assert.DoesNotThrow(() => Sut.Load(null)); + } + + private IReadOnlyCollection Arrange_LoadAllTestTypes() + { + var types = GetTestCases().Select(t => t.Type).ToList(); + Sut.Load(types); + return types; + } + + public abstract IEnumerable GetTestCases(); + } +} \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs b/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs index 12d4c644a..9265f425b 100644 --- a/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs +++ b/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs @@ -21,18 +21,16 @@ // 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 EventFlow.Aggregates; using EventFlow.Core; using EventFlow.EventStores; -using EventFlow.TestHelpers; -using FluentAssertions; -using NUnit.Framework; +using EventFlow.Tests.UnitTests.Core.VersionedTypes; namespace EventFlow.Tests.UnitTests.EventStores { - [TestFixture] - public class EventDefinitionServiceTests : TestsFor + public class EventDefinitionServiceTests : VersionedTypeDefinitionServiceTestSuite { [EventVersion("Fancy", 42)] public class TestEventWithLongName : AggregateEvent, IIdentity> { } @@ -43,60 +41,40 @@ public class TestEventV2 : AggregateEvent, IIdentity> public class OldTestEventV5 : AggregateEvent, IIdentity> { } - [TestCase(typeof(TestEvent), 1, "TestEvent")] - [TestCase(typeof(TestEventV2), 2, "TestEvent")] - [TestCase(typeof(OldTestEventV5), 5, "TestEvent")] - [TestCase(typeof(TestEventWithLongName), 42, "Fancy")] - public void GetEventDefinition_EventWithVersion(Type eventType, int expectedVersion, string expectedName) - { - // Act - var eventDefinition = Sut.GetEventDefinition(eventType); - - // Assert - eventDefinition.Name.Should().Be(expectedName); - eventDefinition.Version.Should().Be(expectedVersion); - eventDefinition.Type.Should().Be(eventType); - } + public class OldThe5ThEventV4 : AggregateEvent, IIdentity> { } - [TestCase("TestEvent", 1, typeof(TestEvent))] - [TestCase("TestEvent", 2, typeof(TestEventV2))] - [TestCase("TestEvent", 5, typeof(OldTestEventV5))] - [TestCase("Fancy", 42, typeof(TestEventWithLongName))] - public void LoadEventsFollowedByGetEventDefinition_ReturnsCorrectAnswer(string eventName, int eventVersion, Type expectedEventType) + public override IEnumerable GetTestCases() { - // Arrange - Sut.LoadEvents(new [] + yield return new VersionTypeTestCase { - typeof(TestEvent), - typeof(TestEventV2), - typeof(OldTestEventV5), - typeof(TestEventWithLongName) - }); - - // Act - var eventDefinition = Sut.GetEventDefinition(eventName, eventVersion); - - // Assert - eventDefinition.Name.Should().Be(eventName); - eventDefinition.Version.Should().Be(eventVersion); - eventDefinition.Type.Should().Be(expectedEventType); - } - - [Test] - public void CanLoadSameEventMultipleTimes() - { - Assert.DoesNotThrow(() => - { - Sut.LoadEvents(new[] { typeof(TestEvent), typeof(TestEvent) }); - Sut.LoadEvents(new[] { typeof(TestEvent) }); - }); - } - - [Test] - public void CanLoadNull() - { - // Act - Sut.LoadEvents(null); + Name = "TestEvent", + Type = typeof(TestEvent), + Version = 1, + }; + yield return new VersionTypeTestCase + { + Name = "TestEvent", + Type = typeof(TestEventV2), + Version = 2, + }; + yield return new VersionTypeTestCase + { + Name = "TestEvent", + Type = typeof(OldTestEventV5), + Version = 5, + }; + yield return new VersionTypeTestCase + { + Name = "Fancy", + Type = typeof(TestEventWithLongName), + Version = 42, + }; + yield return new VersionTypeTestCase + { + Name = "The5ThEvent", + Type = typeof(OldThe5ThEventV4), + Version = 4, + }; } } -} +} \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/Jobs/JobDefinitionServiceTests.cs b/Source/EventFlow.Tests/UnitTests/Jobs/JobDefinitionServiceTests.cs new file mode 100644 index 000000000..3e928047f --- /dev/null +++ b/Source/EventFlow.Tests/UnitTests/Jobs/JobDefinitionServiceTests.cs @@ -0,0 +1,99 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// Copyright (c) 2015 eBay Software Foundation +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Configuration; +using EventFlow.Jobs; +using EventFlow.Tests.UnitTests.Core.VersionedTypes; +using NUnit.Framework; + +namespace EventFlow.Tests.UnitTests.Jobs +{ + [TestFixture] + public class JobDefinitionServiceTests : VersionedTypeDefinitionServiceTestSuite + { + [JobVersion("Fancy", 42)] + public class TestJobWithLongName : IJob + { + public Task ExecuteAsync(IResolver resolver, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + } + + public class TestJob : IJob + { + public Task ExecuteAsync(IResolver resolver, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + } + + public class TestJobV2 : IJob + { + public Task ExecuteAsync(IResolver resolver, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + } + + public class OldTestJobV5 : IJob + { + public Task ExecuteAsync(IResolver resolver, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + } + + public override IEnumerable GetTestCases() + { + yield return new VersionTypeTestCase + { + Name = "TestJob", + Type = typeof(TestJob), + Version = 1, + }; + yield return new VersionTypeTestCase + { + Name = "TestJob", + Type = typeof(TestJobV2), + Version = 2, + }; + yield return new VersionTypeTestCase + { + Name = "TestJob", + Type = typeof(OldTestJobV5), + Version = 5, + }; + yield return new VersionTypeTestCase + { + Name = "Fancy", + Type = typeof(TestJobWithLongName), + Version = 42, + }; + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/MultipleAggregateReadStoreManagerTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/MultipleAggregateReadStoreManagerTests.cs new file mode 100644 index 000000000..52ee6848f --- /dev/null +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/MultipleAggregateReadStoreManagerTests.cs @@ -0,0 +1,107 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// Copyright (c) 2015 eBay Software Foundation +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.ReadStores; +using EventFlow.TestHelpers.Aggregates.Events; +using Moq; +using NUnit.Framework; + +namespace EventFlow.Tests.UnitTests.ReadStores +{ + public class MultipleAggregateReadStoreManagerTests : ReadStoreManagerTestSuite, ReadStoreManagerTestReadModel, IReadModelLocator>> + { + private Mock _readModelLocator; + + [SetUp] + public void SetUp() + { + _readModelLocator = InjectMock(); + + _readModelLocator.Setup(l => l.GetReadModelIds(It.IsAny())).Returns(new[] {A()}); + } + + [Test] + public async Task LocatorShouldNotBeInvokedForIrelevantDomainEvents() + { + // Arrange + var events = new[] + { + ToDomainEvent(A()) + }; + + // Act + await Sut.UpdateReadStoresAsync(events, CancellationToken.None).ConfigureAwait(false); + + // Assert + _readModelLocator.Verify(l => l.GetReadModelIds(It.IsAny()), Times.Never); + } + + [Test] + public async Task LocatorShouldOnlyBeInvokedForIrelevantDomainEvents() + { + // Arrange + var events = new[] + { + ToDomainEvent(A()), + ToDomainEvent(A()) + }; + + // Act + await Sut.UpdateReadStoresAsync(events, CancellationToken.None).ConfigureAwait(false); + + // Assert + _readModelLocator.Verify(l => l.GetReadModelIds(It.IsAny()), Times.Once); + } + + [Test] + public async Task IfNoReadModelIdsAreReturned_ThenDontInvokeTheReadModelStore() + { + // Arrange + _readModelLocator.Setup(l => l.GetReadModelIds(It.IsAny())).Returns(Enumerable.Empty()); + var events = new[] + { + ToDomainEvent(A()), + }; + + // Act + await Sut.UpdateReadStoresAsync(events, CancellationToken.None).ConfigureAwait(false); + + // Assert + _readModelLocator.Verify(l => l.GetReadModelIds(It.IsAny()), Times.Once); + ReadModelStoreMock.Verify( + s => s.UpdateAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny, ReadModelEnvelope, CancellationToken, Task>>>(), + It.IsAny()), + Times.Never); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTestReadModel.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTestReadModel.cs new file mode 100644 index 000000000..793a675e6 --- /dev/null +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTestReadModel.cs @@ -0,0 +1,39 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// Copyright (c) 2015 eBay Software Foundation +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using EventFlow.Aggregates; +using EventFlow.ReadStores; +using EventFlow.TestHelpers.Aggregates; +using EventFlow.TestHelpers.Aggregates.Events; + +namespace EventFlow.Tests.UnitTests.ReadStores +{ + public class ReadStoreManagerTestReadModel : IReadModel, + IAmReadModelFor + { + public void Apply(IReadModelContext context, IDomainEvent domainEvent) + { + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTestSuite.cs similarity index 71% rename from Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTests.cs rename to Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTestSuite.cs index ec47f9ef4..e20fec3ea 100644 --- a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTests.cs +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTestSuite.cs @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// The MIT License (MIT) // // Copyright (c) 2015 Rasmus Mikkelsen // Copyright (c) 2015 eBay Software Foundation @@ -20,7 +20,8 @@ // 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; @@ -28,52 +29,44 @@ using EventFlow.Aggregates; using EventFlow.ReadStores; using EventFlow.TestHelpers; -using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Events; using Moq; using NUnit.Framework; namespace EventFlow.Tests.UnitTests.ReadStores { - public class ReadStoreManagerTests : TestsFor, ReadStoreManagerTests.TestReadModel>> + public abstract class ReadStoreManagerTestSuite : TestsFor + where T : IReadStoreManager { - public class TestReadModel : IReadModel, - IAmReadModelFor - { - public void Apply(IReadModelContext context, IDomainEvent domainEvent) - { - } - } - - private Mock> _readModelStoreMock; + protected Mock> ReadModelStoreMock { get; private set; } [SetUp] - public void SetUp() + public void SetUpReadStoreManagerTestSuite() { - _readModelStoreMock = InjectMock>(); + ReadModelStoreMock = InjectMock>(); } [Test] public async Task ReadStoreIsUpdatedWithRelevantEvents() { // Arrange - var events = new [] + var events = new[] { ToDomainEvent(A()), - ToDomainEvent(A()), + ToDomainEvent(A()) }; // Act await Sut.UpdateReadStoresAsync(events, CancellationToken.None).ConfigureAwait(false); // Assert - _readModelStoreMock.Verify( + ReadModelStoreMock.Verify( s => s.UpdateAsync( It.Is>(l => l.Count == 1), It.IsAny(), - It.IsAny, ReadModelEnvelope, CancellationToken, Task>>>(), + It.IsAny, ReadModelEnvelope, CancellationToken, Task>>>(), It.IsAny()), Times.Once); } } -} +} \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/SingleAggregateReadStoreManagerTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/SingleAggregateReadStoreManagerTests.cs new file mode 100644 index 000000000..9aa28dd5d --- /dev/null +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/SingleAggregateReadStoreManagerTests.cs @@ -0,0 +1,32 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// Copyright (c) 2015 eBay Software Foundation +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using EventFlow.ReadStores; + +namespace EventFlow.Tests.UnitTests.ReadStores +{ + public class SingleAggregateReadStoreManagerTests : ReadStoreManagerTestSuite, ReadStoreManagerTestReadModel>> + { + } +} \ No newline at end of file diff --git a/Source/EventFlow/Aggregates/AggregateRoot.cs b/Source/EventFlow/Aggregates/AggregateRoot.cs index dde5be1d5..3144b2b83 100644 --- a/Source/EventFlow/Aggregates/AggregateRoot.cs +++ b/Source/EventFlow/Aggregates/AggregateRoot.cs @@ -20,7 +20,8 @@ // 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; @@ -53,6 +54,7 @@ public abstract class AggregateRoot : IAggregateRoot); + var aggregateType = typeof(TAggregate); ApplyMethods = typeof(TAggregate) .GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) @@ -66,7 +68,7 @@ static AggregateRoot() }) .ToDictionary( mi => mi.GetParameters()[0].ParameterType, - mi => (Action)((a, e) => mi.Invoke(a, new object[] { e }))); + mi => ReflectionHelper.CompileMethodInvocation>(aggregateType, "Apply", mi.GetParameters()[0].ParameterType)); } protected AggregateRoot(TIdentity id) diff --git a/Source/EventFlow/Aggregates/AggregateState.cs b/Source/EventFlow/Aggregates/AggregateState.cs index 44c603787..eeba7d33b 100644 --- a/Source/EventFlow/Aggregates/AggregateState.cs +++ b/Source/EventFlow/Aggregates/AggregateState.cs @@ -40,8 +40,9 @@ public abstract class AggregateState : IEv static AggregateState() { var aggregateEventType = typeof (IAggregateEvent); + var eventApplier = typeof (TEventApplier); - ApplyMethods = typeof (TEventApplier) + ApplyMethods = eventApplier .GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Where(mi => { @@ -53,7 +54,7 @@ static AggregateState() }) .ToDictionary( mi => mi.GetParameters()[0].ParameterType, - mi => (Action>) ((ea, e) => mi.Invoke(ea, new []{ e } ))); + mi => ReflectionHelper.CompileMethodInvocation>>(eventApplier, "Apply", mi.GetParameters()[0].ParameterType)); } protected AggregateState() diff --git a/Source/EventFlow/CommandBus.cs b/Source/EventFlow/CommandBus.cs index 3464f66b6..a86b48c0a 100644 --- a/Source/EventFlow/CommandBus.cs +++ b/Source/EventFlow/CommandBus.cs @@ -146,24 +146,26 @@ private Task> ExecuteCommandAsync() + .ToList(); if (!commandHandlers.Any()) { throw new NoCommandHandlersException(string.Format( "No command handlers registered for the command '{0}' on aggregate '{1}'", commandType.PrettyPrint(), - commandExecutionDetails.AggregateType.PrettyPrint())); + typeof(TAggregate).PrettyPrint())); } if (commandHandlers.Count > 1) { throw new InvalidOperationException(string.Format( "Too many command handlers the command '{0}' on aggregate '{1}'. These were found: {2}", commandType.PrettyPrint(), - commandExecutionDetails.AggregateType.PrettyPrint(), + typeof(TAggregate).PrettyPrint(), string.Join(", ", commandHandlers.Select(h => h.GetType().PrettyPrint())))); } - var commandHandler = (ICommandHandler) commandHandlers.Single(); + var commandHandler = commandHandlers.Single(); return _transientFaultHandler.TryAsync( async c => @@ -186,7 +188,6 @@ private Task> ExecuteCommandAsync Invoker { get; set; } } @@ -205,13 +206,12 @@ private static CommandExecutionDetails GetCommandExecutionDetails(Type commandTy var commandHandlerType = typeof(ICommandHandler<,,,>) .MakeGenericType(commandTypes[0], commandTypes[1], commandTypes[2], commandType); - var invoker = commandHandlerType.GetMethod("ExecuteAsync"); + var invokeExecuteAsync = ReflectionHelper.CompileMethodInvocation>(commandHandlerType, "ExecuteAsync"); return new CommandExecutionDetails { - AggregateType = commandTypes[0], CommandHandlerType = commandHandlerType, - Invoker = ((h, a, command, c) => (Task)invoker.Invoke(h, new object[] { a, command, c })) + Invoker = invokeExecuteAsync }; }); } diff --git a/Source/EventFlow/Commands/CommandDefinitionService.cs b/Source/EventFlow/Commands/CommandDefinitionService.cs index dacb30fc7..6ddefce48 100644 --- a/Source/EventFlow/Commands/CommandDefinitionService.cs +++ b/Source/EventFlow/Commands/CommandDefinitionService.cs @@ -22,41 +22,20 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; -using System.Collections.Generic; using EventFlow.Core.VersionedTypes; using EventFlow.Logs; namespace EventFlow.Commands { - public class CommandDefinitionService : VersionedTypeDefinitionService, ICommandDefinitionService + public class CommandDefinitionService : VersionedTypeDefinitionService, ICommandDefinitionService { public CommandDefinitionService(ILog log) : base(log) { } - public void LoadCommands(IEnumerable commandTypes) - { - Load(commandTypes); - } - - public CommandDefinition GetCommandDefinition(Type commandType) - { - return GetDefinition(commandType); - } - - public CommandDefinition GetCommandDefinition(string commandName, int version) - { - return GetDefinition(commandName, version); - } - - public bool TryGetCommandDefinition(string name, int version, out CommandDefinition definition) - { - return TryGetDefinition(name, version, out definition); - } - protected override CommandDefinition CreateDefinition(int version, Type type, string name) { return new CommandDefinition(version, type, name); } } -} +} \ No newline at end of file diff --git a/Source/EventFlow/Commands/ICommandDefinitionService.cs b/Source/EventFlow/Commands/ICommandDefinitionService.cs index 688b87f0c..918ce4f4e 100644 --- a/Source/EventFlow/Commands/ICommandDefinitionService.cs +++ b/Source/EventFlow/Commands/ICommandDefinitionService.cs @@ -21,16 +21,12 @@ // 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 EventFlow.Core.VersionedTypes; namespace EventFlow.Commands { - public interface ICommandDefinitionService + public interface ICommandDefinitionService : IVersionedTypeDefinitionService { - void LoadCommands(IEnumerable commandTypes); - CommandDefinition GetCommandDefinition(Type commandType); - CommandDefinition GetCommandDefinition(string commandName, int version); - bool TryGetCommandDefinition(string name, int version, out CommandDefinition definition); } -} +} \ No newline at end of file diff --git a/Source/EventFlow/Commands/SerializedCommandPublisher.cs b/Source/EventFlow/Commands/SerializedCommandPublisher.cs index de577e426..df0312aa4 100644 --- a/Source/EventFlow/Commands/SerializedCommandPublisher.cs +++ b/Source/EventFlow/Commands/SerializedCommandPublisher.cs @@ -57,7 +57,7 @@ public async Task PublishSerilizedCommandAsync(string name, int versi _log.Verbose($"Executing serilized command '{name}' v{version}"); CommandDefinition commandDefinition; - if (!_commandDefinitionService.TryGetCommandDefinition(name, version, out commandDefinition)) + if (!_commandDefinitionService.TryGetDefinition(name, version, out commandDefinition)) { throw new ArgumentException($"No command definition found for command '{name}' v{version}"); } diff --git a/Source/EventFlow/Configuration/Bootstraps/DefinitionServicesInitilizer.cs b/Source/EventFlow/Configuration/Bootstraps/DefinitionServicesInitilizer.cs index f074728c2..942915346 100644 --- a/Source/EventFlow/Configuration/Bootstraps/DefinitionServicesInitilizer.cs +++ b/Source/EventFlow/Configuration/Bootstraps/DefinitionServicesInitilizer.cs @@ -50,9 +50,9 @@ public DefinitionServicesInitilizer( public Task BootAsync(CancellationToken cancellationToken) { - _commandDefinitionService.LoadCommands(_loadedVersionedTypes.Commands); - _eventDefinitionService.LoadEvents(_loadedVersionedTypes.Events); - _jobDefinitionService.LoadJobs(_loadedVersionedTypes.Jobs); + _commandDefinitionService.Load(_loadedVersionedTypes.Commands); + _eventDefinitionService.Load(_loadedVersionedTypes.Events); + _jobDefinitionService.Load(_loadedVersionedTypes.Jobs); return Task.FromResult(0); } diff --git a/Source/EventFlow/Core/ReflectionHelper.cs b/Source/EventFlow/Core/ReflectionHelper.cs index 4fb00d1d5..506e2f0df 100644 --- a/Source/EventFlow/Core/ReflectionHelper.cs +++ b/Source/EventFlow/Core/ReflectionHelper.cs @@ -20,10 +20,15 @@ // 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 System.Linq; +using System.Linq.Expressions; using System.Reflection; +using EventFlow.Extensions; namespace EventFlow.Core { @@ -39,5 +44,85 @@ public static string GetCodeBase(Assembly assembly, bool includeFileName = false Path.GetDirectoryName(path); return codeBase; } + + /// + /// Handles correct upcast. If no upcast was needed, then this could be exchanged to an Expression.Call + /// and an Expression.Lambda. + /// + public static TResult CompileMethodInvocation(Type type, string methodName, params Type[] methodSignature) + { + var methodInfo = methodSignature == null || !methodSignature.Any() + ? type.GetMethods(BindingFlags.Instance | BindingFlags.Public).SingleOrDefault(m => m.Name == methodName) + : type.GetMethod(methodName, methodSignature); + + if (methodInfo == null) + { + throw new ArgumentException($"Type '{type.PrettyPrint()}' doesn't have a method called '{methodName}'"); + } + + var genericArguments = typeof (TResult).GetGenericArguments(); + var methodArgumentList = methodInfo.GetParameters().Select(p => p.ParameterType).ToList(); + var funcArgumentList = genericArguments.Skip(1).Take(methodArgumentList.Count).ToList(); + + if (funcArgumentList.Count != methodArgumentList.Count) + { + throw new ArgumentException("Incorrect number of arguments"); + } + + var instanceArgument = Expression.Parameter(genericArguments[0]);; + + var argumentPairs = funcArgumentList.Zip(methodArgumentList, (s, d) => new {Source = s, Destination = d}).ToList(); + if (argumentPairs.All(a => a.Source == a.Destination)) + { + // No need to do anything fancy, the types are the same + var parameters = funcArgumentList.Select(Expression.Parameter).ToList(); + return Expression.Lambda(Expression.Call(instanceArgument, methodInfo, parameters), new [] { instanceArgument }.Concat(parameters)).Compile(); + } + + var lambdaArgument = new List + { + instanceArgument, + }; + var instanceVariable = Expression.Variable(type); + var blockVariables = new List + { + instanceVariable, + }; + var blockExpressions = new List + { + Expression.Assign(instanceVariable, Expression.ConvertChecked(instanceArgument, type)) + }; + var callArguments = new List(); + + foreach (var a in argumentPairs) + { + if (a.Source == a.Destination) + { + var sourceParameter = Expression.Parameter(a.Source); + lambdaArgument.Add(sourceParameter); + callArguments.Add(sourceParameter); + } + else + { + var sourceParameter = Expression.Parameter(a.Source); + var destinationVariable = Expression.Variable(a.Destination); + var assignToDestination = Expression.Assign(destinationVariable, Expression.Convert(sourceParameter, a.Destination)); + + lambdaArgument.Add(sourceParameter); + callArguments.Add(destinationVariable); + blockVariables.Add(destinationVariable); + blockExpressions.Add(assignToDestination); + } + } + + var callExpression = Expression.Call(instanceVariable, methodInfo, callArguments); + blockExpressions.Add(callExpression); + + var block = Expression.Block(blockVariables, blockExpressions); + + var lambdaExpression = Expression.Lambda(block, lambdaArgument); + + return lambdaExpression.Compile(); + } } -} +} \ No newline at end of file diff --git a/Source/EventFlow/Core/VersionedTypes/IVersionedTypeDefinitionService.cs b/Source/EventFlow/Core/VersionedTypes/IVersionedTypeDefinitionService.cs new file mode 100644 index 000000000..f8b9e1a10 --- /dev/null +++ b/Source/EventFlow/Core/VersionedTypes/IVersionedTypeDefinitionService.cs @@ -0,0 +1,43 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// Copyright (c) 2015 eBay Software Foundation +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace EventFlow.Core.VersionedTypes +{ + public interface IVersionedTypeDefinitionService + where TAttribute : VersionedTypeAttribute + where TDefinition : VersionedTypeDefinition + { + void Load(IReadOnlyCollection types); + IEnumerable GetDefinitions(string name); + bool TryGetDefinition(string name, int version, out TDefinition definition); + IEnumerable GetAllDefinitions(); + TDefinition GetDefinition(string name, int version); + TDefinition GetDefinition(Type type); + bool TryGetDefinition(Type type, out TDefinition definition); + void Load(params Type[] types); + } +} \ No newline at end of file diff --git a/Source/EventFlow/Core/VersionedTypes/VersionedTypeDefinitionService.cs b/Source/EventFlow/Core/VersionedTypes/VersionedTypeDefinitionService.cs index e1881d2e2..2e122dee1 100644 --- a/Source/EventFlow/Core/VersionedTypes/VersionedTypeDefinitionService.cs +++ b/Source/EventFlow/Core/VersionedTypes/VersionedTypeDefinitionService.cs @@ -21,7 +21,9 @@ // 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.Collections.Generic; using System.Linq; using System.Reflection; @@ -31,18 +33,19 @@ namespace EventFlow.Core.VersionedTypes { - public abstract class VersionedTypeDefinitionService + public abstract class VersionedTypeDefinitionService : IVersionedTypeDefinitionService where TAttribute : VersionedTypeAttribute where TDefinition : VersionedTypeDefinition { // ReSharper disable once StaticMemberInGenericType private static readonly Regex NameRegex = new Regex( - @"^(Old){0,1}(?[a-zA-Z]+)(V(?[0-9]+)){0,1}$", + @"^(Old){0,1}(?[a-zA-Z0-9]+?)(V(?[0-9]+)){0,1}$", RegexOptions.Compiled); + private readonly object _syncRoot = new object(); private readonly ILog _log; - private readonly Dictionary _definitionsByType = new Dictionary(); - private readonly Dictionary _definitionsByName = new Dictionary(); + private readonly ConcurrentDictionary _definitionsByType = new ConcurrentDictionary(); + private readonly ConcurrentDictionary> _definitionByNameAndVersion = new ConcurrentDictionary>(); protected VersionedTypeDefinitionService( ILog log) @@ -50,57 +53,105 @@ protected VersionedTypeDefinitionService( _log = log; } - protected void Load(IEnumerable types) + public void Load(params Type[] types) + { + Load((IReadOnlyCollection) types); + } + + public void Load(IReadOnlyCollection types) { if (types == null) { return; } - var definitions = types.Select(GetDefinition).ToList(); - if (!definitions.Any()) + var invalidTypes = types + .Where(t => !typeof (TTypeCheck) + .IsAssignableFrom(t)) + .ToList(); + if (invalidTypes.Any()) { - return; + throw new ArgumentException($"The following types are not of type '{typeof(TTypeCheck).PrettyPrint()}': {string.Join(", ", invalidTypes.Select(t => t.PrettyPrint()))}"); } - _log.Verbose(() => - { - var assemblies = definitions - .Select(d => d.Type.Assembly.GetName().Name) - .Distinct() - .OrderBy(n => n) - .ToList(); - return string.Format( - "Added {0} versioned types to '{1}' from these assemblies: {2}", - definitions.Count, - GetType().PrettyPrint(), - string.Join(", ", assemblies)); - }); - - foreach (var definition in definitions) + lock (_syncRoot) { - var key = GetKey(definition.Name, definition.Version); - if (!_definitionsByName.ContainsKey(key)) + var definitions = types + .Distinct() + .Where(t => !_definitionsByType.ContainsKey(t)) + .Select(CreateDefinition) + .ToList(); + if (!definitions.Any()) { - _definitionsByName.Add(key, definition); + return; } - else + + _log.Verbose(() => + { + var assemblies = definitions + .Select(d => d.Type.Assembly.GetName().Name) + .Distinct() + .OrderBy(n => n) + .ToList(); + return string.Format( + "Added {0} versioned types to '{1}' from these assemblies: {2}", + definitions.Count, + GetType().PrettyPrint(), + string.Join(", ", assemblies)); + }); + + foreach (var definition in definitions) { - _log.Information( - "Already loaded versioned type '{0}' v{1}, skipping it", - definition.Name, - definition.Version); + _definitionsByType.TryAdd(definition.Type, definition); + + Dictionary versions; + if (!_definitionByNameAndVersion.TryGetValue(definition.Name, out versions)) + { + versions = new Dictionary(); + _definitionByNameAndVersion.TryAdd(definition.Name, versions); + } + + if (versions.ContainsKey(definition.Version)) + { + _log.Information( + "Already loaded versioned type '{0}' v{1}, skipping it", + definition.Name, + definition.Version); + continue; + } + + versions.Add(definition.Version, definition); } } } - protected bool TryGetDefinition(string name, int version, out TDefinition definition) + public IEnumerable GetDefinitions(string name) + { + Dictionary versions; + return _definitionByNameAndVersion.TryGetValue(name, out versions) + ? versions.Values.OrderBy(d => d.Version) + : Enumerable.Empty(); + } + + public IEnumerable GetAllDefinitions() + { + return _definitionByNameAndVersion.SelectMany(kv => kv.Value.Values); + } + + public bool TryGetDefinition(string name, int version, out TDefinition definition) { - var key = GetKey(name, version); - return _definitionsByName.TryGetValue(key, out definition); + Dictionary versions; + if (_definitionByNameAndVersion.TryGetValue(name, out versions)) + { + return versions.TryGetValue(version, out definition); + } + + definition = null; + + return false; } - protected TDefinition GetDefinition(string name, int version) + public TDefinition GetDefinition(string name, int version) { TDefinition definition; if (!TryGetDefinition(name, version, out definition)) @@ -111,17 +162,28 @@ protected TDefinition GetDefinition(string name, int version) return definition; } - protected TDefinition GetDefinition(Type type) + public TDefinition GetDefinition(Type type) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - if (_definitionsByType.ContainsKey(type)) + if (type == null) throw new ArgumentNullException(nameof(type)); + + TDefinition definition; + if (!_definitionsByType.TryGetValue(type, out definition)) { - return _definitionsByType[type]; + throw new ArgumentException($"No definition for type '{type.PrettyPrint()}', have you remembered to load it during EventFlow initialization"); } + return definition; + } + + public bool TryGetDefinition(Type type, out TDefinition definition) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + return _definitionsByType.TryGetValue(type, out definition); + } + + private TDefinition CreateDefinition(Type type) + { var definition = CreateDefinitions(type).FirstOrDefault(d => d != null); if (definition == null) { @@ -132,11 +194,11 @@ protected TDefinition GetDefinition(Type type) _log.Verbose(() => $"{GetType().PrettyPrint()}: Added versioned type definition '{definition}'"); - _definitionsByType.Add(type, definition); - return definition; } + protected abstract TDefinition CreateDefinition(int version, Type type, string name); + private IEnumerable CreateDefinitions(Type versionedType) { yield return CreateDefinitionFromAttribute(versionedType); @@ -178,12 +240,5 @@ private TDefinition CreateDefinitionFromAttribute(Type versionedType) versionedType, attribute.Name); } - - protected abstract TDefinition CreateDefinition(int version, Type type, string name); - - private static string GetKey(string versionedTypeName, int version) - { - return $"{versionedTypeName} - v{version}"; - } } -} +} \ No newline at end of file diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index b68c499ec..84e750982 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -117,6 +117,7 @@ + diff --git a/Source/EventFlow/EventStores/EventDefinitionService.cs b/Source/EventFlow/EventStores/EventDefinitionService.cs index c57419378..2475277f5 100644 --- a/Source/EventFlow/EventStores/EventDefinitionService.cs +++ b/Source/EventFlow/EventStores/EventDefinitionService.cs @@ -21,35 +21,21 @@ // 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 EventFlow.Aggregates; using EventFlow.Core.VersionedTypes; using EventFlow.Logs; namespace EventFlow.EventStores { - public class EventDefinitionService : VersionedTypeDefinitionService, IEventDefinitionService + public class EventDefinitionService : VersionedTypeDefinitionService, IEventDefinitionService { public EventDefinitionService(ILog log) : base(log) { } - public void LoadEvents(IEnumerable eventTypes) - { - Load(eventTypes); - } - - public EventDefinition GetEventDefinition(Type eventType) - { - return GetDefinition(eventType); - } - - public EventDefinition GetEventDefinition(string eventName, int version) - { - return GetDefinition(eventName, version); - } - protected override EventDefinition CreateDefinition(int version, Type type, string name) { return new EventDefinition(version, type, name); diff --git a/Source/EventFlow/EventStores/EventJsonSerializer.cs b/Source/EventFlow/EventStores/EventJsonSerializer.cs index b6936ef64..bda571c2c 100644 --- a/Source/EventFlow/EventStores/EventJsonSerializer.cs +++ b/Source/EventFlow/EventStores/EventJsonSerializer.cs @@ -47,7 +47,7 @@ public EventJsonSerializer( public SerializedEvent Serialize(IAggregateEvent aggregateEvent, IEnumerable> metadatas) { - var eventDefinition = _eventDefinitionService.GetEventDefinition(aggregateEvent.GetType()); + var eventDefinition = _eventDefinitionService.GetDefinition(aggregateEvent.GetType()); var metadata = new Metadata(metadatas .Where(kv => kv.Key != MetadataKeys.EventName && kv.Key != MetadataKeys.EventVersion) // TODO: Fix this @@ -89,7 +89,7 @@ public IDomainEvent Deserialize( private IDomainEvent Deserialize(string aggregateId, string json, IMetadata metadata) { - var eventDefinition = _eventDefinitionService.GetEventDefinition( + var eventDefinition = _eventDefinitionService.GetDefinition( metadata.EventName, metadata.EventVersion); diff --git a/Source/EventFlow/EventStores/EventUpgradeManager.cs b/Source/EventFlow/EventStores/EventUpgradeManager.cs index 0c6ab288e..9d4bea084 100644 --- a/Source/EventFlow/EventStores/EventUpgradeManager.cs +++ b/Source/EventFlow/EventStores/EventUpgradeManager.cs @@ -21,8 +21,8 @@ // 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; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -130,11 +130,13 @@ private static EventUpgraderCacheItem GetCache(Type aggregateType) var arguments = aggregateRootInterface.GetGenericArguments(); var eventUpgraderType = typeof(IEventUpgrader<,>).MakeGenericType(t, arguments[0]); - var methodInfo = eventUpgraderType.GetMethod("Upgrade"); + + var invokeUpgrade = ReflectionHelper.CompileMethodInvocation>>(eventUpgraderType, "Upgrade"); + return new EventUpgraderCacheItem( eventUpgraderType, - (o, e) => ((IEnumerable)methodInfo.Invoke(o, new object[] { e })).Cast()); + invokeUpgrade); }); } } -} +} \ No newline at end of file diff --git a/Source/EventFlow/EventStores/IEventDefinitionService.cs b/Source/EventFlow/EventStores/IEventDefinitionService.cs index f5720f7e1..2a247774d 100644 --- a/Source/EventFlow/EventStores/IEventDefinitionService.cs +++ b/Source/EventFlow/EventStores/IEventDefinitionService.cs @@ -21,15 +21,12 @@ // 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 EventFlow.Core.VersionedTypes; namespace EventFlow.EventStores { - public interface IEventDefinitionService + public interface IEventDefinitionService : IVersionedTypeDefinitionService { - void LoadEvents(IEnumerable eventTypes); - EventDefinition GetEventDefinition(Type eventType); - EventDefinition GetEventDefinition(string eventName, int version); } -} +} \ No newline at end of file diff --git a/Source/EventFlow/Jobs/IJobDefinitionService.cs b/Source/EventFlow/Jobs/IJobDefinitionService.cs index f60dfd819..c0ed73da0 100644 --- a/Source/EventFlow/Jobs/IJobDefinitionService.cs +++ b/Source/EventFlow/Jobs/IJobDefinitionService.cs @@ -21,16 +21,12 @@ // 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 EventFlow.Core.VersionedTypes; namespace EventFlow.Jobs { - public interface IJobDefinitionService + public interface IJobDefinitionService : IVersionedTypeDefinitionService { - void LoadJobs(IEnumerable jobTypes); - JobDefinition GetJobDefinition(Type jobType); - JobDefinition GetJobDefinition(string jobName, int version); - bool TryGetJobDefinition(string name, int version, out JobDefinition definition); } } \ No newline at end of file diff --git a/Source/EventFlow/Jobs/InstantJobScheduler.cs b/Source/EventFlow/Jobs/InstantJobScheduler.cs index 8ab1788fb..8435dd18a 100644 --- a/Source/EventFlow/Jobs/InstantJobScheduler.cs +++ b/Source/EventFlow/Jobs/InstantJobScheduler.cs @@ -51,7 +51,7 @@ public InstantJobScheduler( public async Task ScheduleNowAsync(IJob job, CancellationToken cancellationToken) { - var jobDefinition = _jobDefinitionService.GetJobDefinition(job.GetType()); + var jobDefinition = _jobDefinitionService.GetDefinition(job.GetType()); var json = _jsonSerializer.Serialize(job); _log.Verbose(() => $"Executing job '{jobDefinition.Name}' v{jobDefinition.Version}: {json}"); diff --git a/Source/EventFlow/Jobs/JobDefinitionService.cs b/Source/EventFlow/Jobs/JobDefinitionService.cs index d510b42f5..7feb892ff 100644 --- a/Source/EventFlow/Jobs/JobDefinitionService.cs +++ b/Source/EventFlow/Jobs/JobDefinitionService.cs @@ -20,41 +20,21 @@ // 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 EventFlow.Core.VersionedTypes; using EventFlow.Logs; namespace EventFlow.Jobs { - public class JobDefinitionService : VersionedTypeDefinitionService, IJobDefinitionService + public class JobDefinitionService : VersionedTypeDefinitionService, IJobDefinitionService { public JobDefinitionService(ILog log) : base(log) { } - public void LoadJobs(IEnumerable jobTypes) - { - Load(jobTypes); - } - - public JobDefinition GetJobDefinition(Type jobType) - { - return GetDefinition(jobType); - } - - public JobDefinition GetJobDefinition(string jobName, int version) - { - return GetDefinition(jobName, version); - } - - public bool TryGetJobDefinition(string name, int version, out JobDefinition definition) - { - return TryGetDefinition(name, version, out definition); - } - protected override JobDefinition CreateDefinition(int version, Type type, string name) { return new JobDefinition(version, type, name); diff --git a/Source/EventFlow/Jobs/JobRunner.cs b/Source/EventFlow/Jobs/JobRunner.cs index 8f45dac8d..9d3e297be 100644 --- a/Source/EventFlow/Jobs/JobRunner.cs +++ b/Source/EventFlow/Jobs/JobRunner.cs @@ -61,7 +61,7 @@ public void Execute(string jobName, int version, string json, CancellationToken public Task ExecuteAsync(string jobName, int version, string json, CancellationToken cancellationToken) { JobDefinition jobDefinition; - if (!_jobDefinitionService.TryGetJobDefinition(jobName, version, out jobDefinition)) + if (!_jobDefinitionService.TryGetDefinition(jobName, version, out jobDefinition)) { throw UnknownJobException.With(jobName, version); } diff --git a/Source/EventFlow/Provided/Jobs/PublishCommandJob.cs b/Source/EventFlow/Provided/Jobs/PublishCommandJob.cs index 81129ca37..99e2f8e1c 100644 --- a/Source/EventFlow/Provided/Jobs/PublishCommandJob.cs +++ b/Source/EventFlow/Provided/Jobs/PublishCommandJob.cs @@ -53,7 +53,7 @@ public Task ExecuteAsync(IResolver resolver, CancellationToken cancellationToken var jsonSerializer = resolver.Resolve(); var commandBus = resolver.Resolve(); - var commandDefinition = commandDefinitionService.GetCommandDefinition(Name, Version); + var commandDefinition = commandDefinitionService.GetDefinition(Name, Version); var command = (ICommand) jsonSerializer.Deserialize(Data, commandDefinition.Type); return command.PublishAsync(commandBus, cancellationToken); @@ -75,7 +75,7 @@ public static PublishCommandJob Create( IJsonSerializer jsonSerializer) { var data = jsonSerializer.Serialize(command); - var commandDefinition = commandDefinitionService.GetCommandDefinition(command.GetType()); + var commandDefinition = commandDefinitionService.GetDefinition(command.GetType()); return new PublishCommandJob( data, diff --git a/Source/EventFlow/Queries/IQuery.cs b/Source/EventFlow/Queries/IQuery.cs index 9a7c1ba19..8bfcb6306 100644 --- a/Source/EventFlow/Queries/IQuery.cs +++ b/Source/EventFlow/Queries/IQuery.cs @@ -20,10 +20,15 @@ // 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.Queries { - public interface IQuery + public interface IQuery + { + } + + public interface IQuery : IQuery { } } diff --git a/Source/EventFlow/Queries/IQueryHandler.cs b/Source/EventFlow/Queries/IQueryHandler.cs index a99d4b597..47d86f299 100644 --- a/Source/EventFlow/Queries/IQueryHandler.cs +++ b/Source/EventFlow/Queries/IQueryHandler.cs @@ -26,9 +26,13 @@ namespace EventFlow.Queries { - public interface IQueryHandler + public interface IQueryHandler + { + } + + public interface IQueryHandler : IQueryHandler where TQuery : IQuery { Task ExecuteQueryAsync(TQuery query, CancellationToken cancellationToken); } -} +} \ No newline at end of file diff --git a/Source/EventFlow/Queries/QueryProcessor.cs b/Source/EventFlow/Queries/QueryProcessor.cs index 8069fe093..0b9e29786 100644 --- a/Source/EventFlow/Queries/QueryProcessor.cs +++ b/Source/EventFlow/Queries/QueryProcessor.cs @@ -38,7 +38,7 @@ public class QueryProcessor : IQueryProcessor private class CacheItem { public Type QueryHandlerType { get; set; } - public Func HandlerFunc { get; set; } + public Func HandlerFunc { get; set; } } private readonly ILog _log; @@ -60,7 +60,7 @@ public async Task ProcessAsync(IQuery query, Cancella queryType, CreateCacheItem); - var queryHandler = _resolver.Resolve(cacheItem.QueryHandlerType); + var queryHandler = (IQueryHandler) _resolver.Resolve(cacheItem.QueryHandlerType); _log.Verbose(() => $"Executing query '{queryType.PrettyPrint()}' ({cacheItem.QueryHandlerType.PrettyPrint()}) by using query handler '{queryHandler.GetType().PrettyPrint()}'"); var task = (Task) cacheItem.HandlerFunc(queryHandler, query, cancellationToken); @@ -84,12 +84,15 @@ private static CacheItem CreateCacheItem(Type queryType) .GetInterfaces() .Single(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IQuery<>)); var queryHandlerType = typeof(IQueryHandler<,>).MakeGenericType(queryType, queryInterfaceType.GetGenericArguments()[0]); - var methodInfo = queryHandlerType.GetMethod("ExecuteQueryAsync"); + var invokeExecuteQueryAsync = ReflectionHelper.CompileMethodInvocation>( + queryHandlerType, + "ExecuteQueryAsync", + queryType, typeof(CancellationToken)); return new CacheItem { QueryHandlerType = queryHandlerType, - HandlerFunc = (h, q, c) => methodInfo.Invoke(h, new object[]{q, c}) - }; + HandlerFunc = invokeExecuteQueryAsync + }; } } -} +} \ No newline at end of file diff --git a/Source/EventFlow/ReadStores/ReadModelDomainEventApplier.cs b/Source/EventFlow/ReadStores/ReadModelDomainEventApplier.cs index fb5583e10..e9642a6d0 100644 --- a/Source/EventFlow/ReadStores/ReadModelDomainEventApplier.cs +++ b/Source/EventFlow/ReadStores/ReadModelDomainEventApplier.cs @@ -27,6 +27,7 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Core; namespace EventFlow.ReadStores { @@ -54,10 +55,13 @@ public Task UpdateReadModelAsync( t => { var domainEventType = typeof(IDomainEvent<,,>).MakeGenericType(domainEvent.AggregateType, domainEvent.GetIdentity().GetType(), t); - var methodInfo = readModelType.GetMethod("Apply", new[] { typeof(IReadModelContext), domainEventType }); + + var methodSignature = new[] {typeof (IReadModelContext), domainEventType}; + var methodInfo = readModelType.GetMethod("Apply", methodSignature); + return methodInfo == null ? null - : (Action)((r, c, e) => methodInfo.Invoke(r, new object[] { c, e })); + : ReflectionHelper.CompileMethodInvocation>(readModelType, "Apply", methodSignature); }); if (applyMethod != null) diff --git a/Source/EventFlow/ReadStores/ReadStoreManager.cs b/Source/EventFlow/ReadStores/ReadStoreManager.cs index 6c0fb7d9b..ae9983699 100644 --- a/Source/EventFlow/ReadStores/ReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/ReadStoreManager.cs @@ -108,6 +108,16 @@ public async Task UpdateReadStoresAsync( var readModelContext = new ReadModelContext(Resolver); var readModelUpdates = BuildReadModelUpdates(relevantDomainEvents); + if (!readModelUpdates.Any()) + { + Log.Verbose(() => string.Format( + "No read model updates after building for read model '{0}' in store '{1}' with these events: {2}", + typeof(TReadModel).PrettyPrint(), + typeof(TReadModelStore).PrettyPrint(), + string.Join(", ", relevantDomainEvents.Select(e => e.ToString())))); + return; + } + await ReadModelStore.UpdateAsync( readModelUpdates, readModelContext, diff --git a/Source/EventFlow/ReadStores/SingleAggregateReadStoreManager.cs b/Source/EventFlow/ReadStores/SingleAggregateReadStoreManager.cs index 959b2d583..32ef75760 100644 --- a/Source/EventFlow/ReadStores/SingleAggregateReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/SingleAggregateReadStoreManager.cs @@ -67,7 +67,7 @@ protected override async Task> UpdateAsync( ReadModelEnvelope readModelEnvelope, CancellationToken cancellationToken) { - var readModel = readModelEnvelope.ReadModel ?? new TReadModel(); + var readModel = readModelEnvelope.ReadModel ?? await ReadModelFactory.CreateAsync(readModelEnvelope.ReadModelId, cancellationToken).ConfigureAwait(false); await ReadModelDomainEventApplier.UpdateReadModelAsync(readModel, domainEvents, readModelContext, cancellationToken).ConfigureAwait(false); diff --git a/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs b/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs index 95f1193b7..7bffcb9d9 100644 --- a/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs +++ b/Source/EventFlow/Subscribers/DispatchToEventSubscribers.cs @@ -25,11 +25,11 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.Configuration; +using EventFlow.Core; using EventFlow.Extensions; using EventFlow.Logs; @@ -100,12 +100,13 @@ private static SubscriberInfomation GetSubscriberInfomation(Type domainEventType .GetGenericArguments(); var handlerType = typeof(ISubscribeSynchronousTo<,,>).MakeGenericType(arguments[0], arguments[1], arguments[2]); - var methodInfo = handlerType.GetMethod("HandleAsync", BindingFlags.Instance | BindingFlags.Public); + var invokeHandleAsync = ReflectionHelper.CompileMethodInvocation>(handlerType, "HandleAsync"); + return new SubscriberInfomation { SubscriberType = handlerType, - HandleMethod = (Func) ((h, e, c) => (Task) methodInfo.Invoke(h, new object[] {e, c})) - }; + HandleMethod = invokeHandleAsync, + }; }); } } diff --git a/appveyor.yml b/appveyor.yml index a315b40c1..017e775e0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ init: - git config --global core.autocrlf input -version: 0.22.{build} +version: 0.23.{build} skip_tags: true