From 2a34c45277868bff03a6713f4eb32e911e2f0ca5 Mon Sep 17 00:00:00 2001 From: Jeremy Stafford Date: Sun, 3 Feb 2019 13:59:39 -0700 Subject: [PATCH] added coverage for aggregate root. added circle ci config --- .circleci/config.yml | 23 ++ .../DDD/ValueObjectTests.cs | 4 +- .../EventSourcing/AggregateEventTests.cs | 25 ++ .../EventSourcing/AggregateRootTests.cs | 102 +++++ .../EventSourcing/AggregateTests.cs | 86 ---- ...uteAsAssemblyQualifiednameStrategyTests.cs | 40 -- ...yAssemblyQualifiedTypeNameStrategyTests.cs | 52 --- .../EventSourcing/ByAttributeStrategyTests.cs | 63 --- .../EventSourcing/ByJsonTypeStrategyTests.cs | 57 --- .../EventSourcing/EntityTests.cs | 134 ------ .../EventSourcing/EventInfoTests.cs | 148 ------- .../EventSourcing/EventTypeCacheTests.cs | 57 --- .../EventSourcing/RepositoryTests.cs | 383 ------------------ .../EventSourcing/TestRoot.cs | 110 +++++ .../Provausio.Practices.Tests.csproj | 4 + .../EventSourcing/Aggregate/AggregateEvent.cs | 40 ++ .../EventSourcing/Aggregate/AggregateRoot.cs | 155 +++++++ .../EventSourcing/Aggregate/Entity.cs | 24 ++ .../HandlerNotRegisteredException.cs | 15 + .../Aggregate/IAggregateEvent.cs | 19 + .../EventSourcing/AggregateRoot.cs | 136 ------- ...ttributeAsAssemblyQualifiedNameStrategy.cs | 30 -- .../ByAssemblyQualifiedTypeNameStrategy.cs | 32 -- .../Deserialization/ByAttributeStrategy.cs | 36 -- .../Deserialization/ByJsonTypeStrategy.cs | 15 - .../EventDeserializationFactory.cs | 52 --- .../EventDeserializationStrategy.cs | 21 - .../Deserialization/EventNamespace.cs | 51 --- .../IEventDeserializationFactory.cs | 7 - .../IEventDeserializationStrategy.cs | 7 - .../EventSourcing/DynamicWrapper.cs | 236 ----------- .../EventSourcing/EventInfo.cs | 105 ----- .../EventSourcing/EventMetadata.cs | 20 - .../EventSourcing/EventStreamResult.cs | 23 -- .../EventSourcing/EventTypeCache.cs | 194 --------- .../EventSourcing/IAggregateRoot.cs | 126 ------ .../EventSourcing/IProjection.cs | 30 -- .../EventSourcing/IRepository.cs | 36 -- .../EventSourcing/IStoreEvents.cs | 18 - .../EventSourcing/ISubscriptionAdapter.cs | 45 -- .../EventSourcing/ISubscriptionProvider.cs | 7 - .../EventSourcing/JsonEventData.cs | 14 - .../EventSourcing/Projection.cs | 150 ------- .../EventSourcing/ProjectionLoader.cs | 51 --- .../Properties/PropertyInfo.cs | 3 + .../Provausio.Practices.csproj | 6 +- 46 files changed, 527 insertions(+), 2465 deletions(-) create mode 100644 .circleci/config.yml create mode 100644 src/Provausio.Practices.Tests/EventSourcing/AggregateEventTests.cs create mode 100644 src/Provausio.Practices.Tests/EventSourcing/AggregateRootTests.cs delete mode 100644 src/Provausio.Practices.Tests/EventSourcing/AggregateTests.cs delete mode 100644 src/Provausio.Practices.Tests/EventSourcing/AttributeAsAssemblyQualifiednameStrategyTests.cs delete mode 100644 src/Provausio.Practices.Tests/EventSourcing/ByAssemblyQualifiedTypeNameStrategyTests.cs delete mode 100644 src/Provausio.Practices.Tests/EventSourcing/ByAttributeStrategyTests.cs delete mode 100644 src/Provausio.Practices.Tests/EventSourcing/ByJsonTypeStrategyTests.cs delete mode 100644 src/Provausio.Practices.Tests/EventSourcing/EntityTests.cs delete mode 100644 src/Provausio.Practices.Tests/EventSourcing/EventInfoTests.cs delete mode 100644 src/Provausio.Practices.Tests/EventSourcing/EventTypeCacheTests.cs delete mode 100644 src/Provausio.Practices.Tests/EventSourcing/RepositoryTests.cs create mode 100644 src/Provausio.Practices.Tests/EventSourcing/TestRoot.cs create mode 100644 src/Provausio.Practices/EventSourcing/Aggregate/AggregateEvent.cs create mode 100644 src/Provausio.Practices/EventSourcing/Aggregate/AggregateRoot.cs create mode 100644 src/Provausio.Practices/EventSourcing/Aggregate/Entity.cs create mode 100644 src/Provausio.Practices/EventSourcing/Aggregate/HandlerNotRegisteredException.cs create mode 100644 src/Provausio.Practices/EventSourcing/Aggregate/IAggregateEvent.cs delete mode 100644 src/Provausio.Practices/EventSourcing/AggregateRoot.cs delete mode 100644 src/Provausio.Practices/EventSourcing/Deserialization/AttributeAsAssemblyQualifiedNameStrategy.cs delete mode 100644 src/Provausio.Practices/EventSourcing/Deserialization/ByAssemblyQualifiedTypeNameStrategy.cs delete mode 100644 src/Provausio.Practices/EventSourcing/Deserialization/ByAttributeStrategy.cs delete mode 100644 src/Provausio.Practices/EventSourcing/Deserialization/ByJsonTypeStrategy.cs delete mode 100644 src/Provausio.Practices/EventSourcing/Deserialization/EventDeserializationFactory.cs delete mode 100644 src/Provausio.Practices/EventSourcing/Deserialization/EventDeserializationStrategy.cs delete mode 100644 src/Provausio.Practices/EventSourcing/Deserialization/EventNamespace.cs delete mode 100644 src/Provausio.Practices/EventSourcing/Deserialization/IEventDeserializationFactory.cs delete mode 100644 src/Provausio.Practices/EventSourcing/Deserialization/IEventDeserializationStrategy.cs delete mode 100644 src/Provausio.Practices/EventSourcing/DynamicWrapper.cs delete mode 100644 src/Provausio.Practices/EventSourcing/EventInfo.cs delete mode 100644 src/Provausio.Practices/EventSourcing/EventMetadata.cs delete mode 100644 src/Provausio.Practices/EventSourcing/EventStreamResult.cs delete mode 100644 src/Provausio.Practices/EventSourcing/EventTypeCache.cs delete mode 100644 src/Provausio.Practices/EventSourcing/IAggregateRoot.cs delete mode 100644 src/Provausio.Practices/EventSourcing/IProjection.cs delete mode 100644 src/Provausio.Practices/EventSourcing/IRepository.cs delete mode 100644 src/Provausio.Practices/EventSourcing/IStoreEvents.cs delete mode 100644 src/Provausio.Practices/EventSourcing/ISubscriptionAdapter.cs delete mode 100644 src/Provausio.Practices/EventSourcing/ISubscriptionProvider.cs delete mode 100644 src/Provausio.Practices/EventSourcing/JsonEventData.cs delete mode 100644 src/Provausio.Practices/EventSourcing/Projection.cs delete mode 100644 src/Provausio.Practices/EventSourcing/ProjectionLoader.cs create mode 100644 src/Provausio.Practices/Properties/PropertyInfo.cs diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..10caf65 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,23 @@ +version: 2 +jobs: + build: + docker: + - image: microsoft/dotnet:2.2-sdk + branches: + only: + - master + steps: + - checkout + - run: + name: Restore Packages + command: dotnet restore src + - run: + name: Build Solution (should auto-pack) + command: dotnet build src -c Release /p:BuildNumber=${CIRCLE_BUILD_NUM} + - run: + name: Run unit tests + command: dotnet test src + - run: + name: Publish NuGet Packages + command: dotnet nuget push src/**/bin/Release/**.nupkg --source https://api.nuget.org/v3/index.json --api-key ${PROV_NUGET_KEY} + \ No newline at end of file diff --git a/src/Provausio.Practices.Tests/DDD/ValueObjectTests.cs b/src/Provausio.Practices.Tests/DDD/ValueObjectTests.cs index 14d5834..be97b08 100644 --- a/src/Provausio.Practices.Tests/DDD/ValueObjectTests.cs +++ b/src/Provausio.Practices.Tests/DDD/ValueObjectTests.cs @@ -1,5 +1,5 @@ using System; -using DAS.Infrastructure; +using Provausio.Common; using Provausio.Practices.DDD; using Xunit; @@ -231,7 +231,7 @@ public static TimestampObj FromString(string input) var coll = ObjectPropertyCollection.FromKvpString(input); return new TimestampObj { - Timestamp = DAS.Infrastructure.Timestamp.FromMilliseconds(coll[nameof(Timestamp)]), + Timestamp = Common.Timestamp.FromMilliseconds(coll[nameof(Timestamp)]), Prop1 = coll[nameof(Prop1)] }; } diff --git a/src/Provausio.Practices.Tests/EventSourcing/AggregateEventTests.cs b/src/Provausio.Practices.Tests/EventSourcing/AggregateEventTests.cs new file mode 100644 index 0000000..d61d19a --- /dev/null +++ b/src/Provausio.Practices.Tests/EventSourcing/AggregateEventTests.cs @@ -0,0 +1,25 @@ +using Provausio.Practices.EventSourcing.Aggregate; +using Xunit; + +namespace Provausio.Practices.Tests.EventSourcing +{ + public class AggregateEventTests + { + [Fact] + public void Create_GeneratesAnId() + { + // arrange + + // act + var ev = AggregateEvent.Create(); + + Assert.NotEmpty(ev.Id.ToString()); + Assert.NotNull(ev.Id); + } + } + + public class TestEvent1 : AggregateEvent + { + + } +} diff --git a/src/Provausio.Practices.Tests/EventSourcing/AggregateRootTests.cs b/src/Provausio.Practices.Tests/EventSourcing/AggregateRootTests.cs new file mode 100644 index 0000000..232ec6a --- /dev/null +++ b/src/Provausio.Practices.Tests/EventSourcing/AggregateRootTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Provausio.Practices.EventSourcing.Aggregate; +using Xunit; + +namespace Provausio.Practices.Tests.EventSourcing +{ + public class AggregateRootTests + { + [Fact] + public void Raise_QueuesEvent() + { + // arrange + + // act + var test = TestRoot.Create("foo", 1); + + // assert + Assert.Equal(1, test.UncommittedEvents.Count); + Assert.IsType(test.UncommittedEvents.First()); + } + + [Fact] + public void Raise_NewRoot_VersionIsNotIncremented() + { + // arrange + var test = TestRoot.Create("foo", 1); + + // act + test.Action1(); + + // assert + Assert.Equal(-1, test.Version); + } + + [Fact] + public void Raise_ExistingRoot_VersionIsIncremented() + { + // arrange + var eventStream = new List + { + new CreationEvent {Version = 0, RootId = "abc"}, + new Action1Occured {Version = 1, RootId = "abc"}, + new EntityCreated {Version = 2, RootId = "abc"} + }; + + // act + var test = new TestRoot() {Id = "abc"}; + test.LoadFromStream(eventStream); + + // assert + Assert.Equal(2, test.Version); + } + + [Fact] + public void LoadFromStream_BringsObjectToState() + { + // arrange + var id = Guid.NewGuid(); + var test = new TestRoot { Id = id.ToString() }; + var create = new CreationEvent { Id = id, RootId = id.ToString(), Version = 0, Prop1 = "foo", Prop2 = 1 }; + var action1 = new Action1Occured { RootId = id.ToString(), Version = 1 }; + + // act + test.LoadFromStream(new AggregateEvent[] { create, action1 }); + + // assert + Assert.Equal(0, test.UncommittedEvents.Count); + Assert.Equal(id.ToString(), test.Id); + Assert.True(test.Action1Occured); + Assert.Equal(1, test.Version); + } + + [Fact] + public void CreateEntity_CreatesEntity() + { + // arrange + var test = TestRoot.Create("foo", 1); + + // act + test.CreateEntity("entity-foo"); + + // assert + Assert.Equal("entity-foo", test.EntityProp); + } + + [Fact] + public void UpdateEntity_ProxiesEventsToEntity() + { + // arrange + var test = TestRoot.Create("foo", 1); + test.CreateEntity("entity-foo"); + + // act + test.UpdateEntity("entity-bar"); + + // assert + Assert.Equal("entity-bar", test.EntityProp); + } + } +} diff --git a/src/Provausio.Practices.Tests/EventSourcing/AggregateTests.cs b/src/Provausio.Practices.Tests/EventSourcing/AggregateTests.cs deleted file mode 100644 index d93b2d2..0000000 --- a/src/Provausio.Practices.Tests/EventSourcing/AggregateTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Collections.Generic; -using Provausio.Practices.EventSourcing; -using Xunit; - -namespace Provausio.Practices.Tests.EventSourcing -{ - public class AggregateTests - { - [Fact] - public void Ctor_VersionIsNew() - { - // arrange - - // act - var agg = new FakeAggregate(); - - // assert - Assert.Equal(-1L, agg.Version); - } - - [Theory] - [InlineData(1)] - [InlineData(3)] - [InlineData(5)] - [InlineData(8)] - [InlineData(13)] - public void LoadFromHistory_AdvancesVersion(int count) - { - // arrange - var agg = new FakeAggregate(); - var events = new List>(); - for(var incomingVersion = 0; incomingVersion < count; incomingVersion++) - { - var newEvent = new FakeEvent {Version = incomingVersion}; - events.Add(newEvent); - } - - // act - agg.LoadFromHistory(events); - - // assert - Assert.Equal(count - 1, agg.Version); - } - - [Fact] - public void LoadFromHistory_InvalidVersion_SameVersion_Throws() - { - // arrange - var agg = new FakeAggregate(); - var incomingEvent = new FakeEvent {Version = 5}; - var incomingEvents = new List> {incomingEvent, incomingEvent}; - - // act - - // assert - Assert.Throws(() => agg.LoadFromHistory(incomingEvents)); - } - - [Fact] - public void LoadFromHistory_InvalidVersion_LowerVersion_Throws() - { - // arrange - var agg = new FakeAggregate(); - var firstEvent = new FakeEvent { Version = 5 }; - var newEvent = new FakeEvent {Version = 4}; - var incomingEvents = new List> {firstEvent, newEvent}; - - // act - - // assert - Assert.Throws(() => agg.LoadFromHistory(incomingEvents)); - } - - private class FakeAggregate : AggregateRoot - { - - protected override EventInfo BuildSnapshot() - { - return null; - } - } - - private class FakeEvent : EventInfo { } - } -} diff --git a/src/Provausio.Practices.Tests/EventSourcing/AttributeAsAssemblyQualifiednameStrategyTests.cs b/src/Provausio.Practices.Tests/EventSourcing/AttributeAsAssemblyQualifiednameStrategyTests.cs deleted file mode 100644 index d4c04a8..0000000 --- a/src/Provausio.Practices.Tests/EventSourcing/AttributeAsAssemblyQualifiednameStrategyTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Provausio.Practices.EventSourcing; -using Provausio.Practices.EventSourcing.Deserialization; -using Xunit; - -namespace Provausio.Practices.Tests.EventSourcing -{ - public class AttributeAsAssemblyQualifiednameStrategyTests - { - // This test will randomly fail but only when run on appveyor. Not sure why. - //[Fact] - public void DeserializeEvent_NewClass_Deserializes() - { - // arrange - var oldclass = new OldClass { Id = Guid.NewGuid() }; - var data = oldclass.GetData(); - var metadata = oldclass.Metadata.GetData(); - var strategy = new AttributeAsAssemblyQualifiedNameStrategy(typeof(OldClass)); - - // act - var info = strategy.DeserializeEvent(data, metadata); - - // assert - Assert.NotNull(info); - } - - [EventNamespace("OldClass")] - // this is a simplified namespace name, not an actual AQN - [EventNamespace("DAS.Practices.Tests.EventSourcing.AttributeAsAssemblyQualifiednameStrategyTests+OldClass, DAS.Practices.Tests, Version=1.0")] - private class NewClass : EventInfo - { - public Guid Id { get; set; } - } - - private class OldClass : EventInfo - { - public Guid Id { get; set; } - } - } -} \ No newline at end of file diff --git a/src/Provausio.Practices.Tests/EventSourcing/ByAssemblyQualifiedTypeNameStrategyTests.cs b/src/Provausio.Practices.Tests/EventSourcing/ByAssemblyQualifiedTypeNameStrategyTests.cs deleted file mode 100644 index 03b7f01..0000000 --- a/src/Provausio.Practices.Tests/EventSourcing/ByAssemblyQualifiedTypeNameStrategyTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Provausio.Practices.EventSourcing; -using Provausio.Practices.EventSourcing.Deserialization; -using Xunit; - -namespace Provausio.Practices.Tests.EventSourcing -{ - public class ByAssemblyQualifiedTypeNameStrategyTests - { - private const string TestString = "Hello World"; - private readonly byte[] _metaData; - private readonly byte[] _eventData; - - public ByAssemblyQualifiedTypeNameStrategyTests() - { - _eventData = new TestClass1 {Property1 = TestString}.GetData(); - _metaData = new EventMetadata { AssemblyQualifiedType = typeof(TestClass1).AssemblyQualifiedName }.GetData(); - } - - [Fact] - public void Deserialize_ValidData_Deserializes() - { - // arrange - var strat = new ByAssemblyQualifiedTypeNameStrategy(); - - // act - var result = (TestClass1) strat.DeserializeEvent(_eventData, _metaData); - - // assert - Assert.Equal(TestString, result.Property1); - } - - [Fact] - public void DeserializeEvent_MissingAssemblyName_Throws() - { - // arrange - var strat = new ByAssemblyQualifiedTypeNameStrategy(); - var metaData = new EventMetadata().GetData(); - - // act - - // assert - Assert.Throws( - () => strat.DeserializeEvent(_eventData, metaData)); - } - - private class TestClass1 : EventInfo - { - public string Property1 { get; set; } - } - } -} diff --git a/src/Provausio.Practices.Tests/EventSourcing/ByAttributeStrategyTests.cs b/src/Provausio.Practices.Tests/EventSourcing/ByAttributeStrategyTests.cs deleted file mode 100644 index ec67935..0000000 --- a/src/Provausio.Practices.Tests/EventSourcing/ByAttributeStrategyTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using Provausio.Practices.EventSourcing; -using Provausio.Practices.EventSourcing.Deserialization; -using Xunit; - -namespace Provausio.Practices.Tests.EventSourcing -{ - public class ByAttributeStrategyTests - { - private readonly byte[] _eventData; - private readonly byte[] _metaData; - private readonly Guid _testId = Guid.NewGuid(); - - public ByAttributeStrategyTests() - { - _eventData = new ValidEventClass { Id = _testId }.GetData(); - _metaData = new EventMetadata { EventNamespaces = "byattributetest" }.GetData(); - } - - //[Fact] - public void DeserializeEvent_ByPath_ValidEvent_Deserializes() - { - // arrange - var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - path = Path.GetDirectoryName(Assembly.GetAssembly(typeof(ValidEventClass)).Location); - var strat = new ByAttributeStrategy(path); - - // act - var result = (ValidEventClass) strat.DeserializeEvent(_eventData, _metaData); - - // assert - Assert.Equal(_testId, result.Id); - } - - //[Fact] - public void DeserializeEvent_ByTypeRef_ValidEvent_Deserializes() - { - // TODO: this test will randomly break with a null object ref... WHY?! - - // arrange - var strat = new ByAttributeStrategy(typeof(ValidEventClass)); - - // act - var result = (ValidEventClass)strat.DeserializeEvent(_eventData, _metaData); - - // assert - Assert.Equal(_testId, result.Id); - } - - [EventNamespace("byattributetest")] - public class ValidEventClass : EventInfo - { - public Guid Id { get; set; } - } - - private class InvalidClass - { - public string Value { get; set; } - } - } -} diff --git a/src/Provausio.Practices.Tests/EventSourcing/ByJsonTypeStrategyTests.cs b/src/Provausio.Practices.Tests/EventSourcing/ByJsonTypeStrategyTests.cs deleted file mode 100644 index 483b72c..0000000 --- a/src/Provausio.Practices.Tests/EventSourcing/ByJsonTypeStrategyTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Text; -using Newtonsoft.Json; -using Provausio.Practices.EventSourcing; -using Provausio.Practices.EventSourcing.Deserialization; -using Xunit; - -namespace Provausio.Practices.Tests.EventSourcing -{ - public class ByJsonTypeStrategyTests - { - private readonly byte[] _eventData; - private readonly byte[] _metaData; - private const string TestValue = "Hello World"; - - public ByJsonTypeStrategyTests() - { - var typeClass = new JsonTypeClass { Value = TestValue }; - _eventData = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject( - typeClass, - new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All})); - _metaData = new EventMetadata().GetData(); - } - - [Fact] - public void DeserializeEvent_TypedObject_Deserializes() - { - // arrange - var strat = new ByJsonTypeStrategy(); - - // act - var result = (JsonTypeClass) strat.DeserializeEvent(_eventData, _metaData); - - //assert - Assert.Equal(TestValue, result.Value); - } - - [Fact] - public void DeserializeEvent_UntypedObject_Throws() - { - // arrange - var strat = new ByJsonTypeStrategy(); - var untypedObject = JsonConvert.SerializeObject(new JsonTypeClass { Value = "irrelevant" }); - var asBytes = Encoding.UTF8.GetBytes(untypedObject); - - // act - - // assert - Assert.Throws(() => strat.DeserializeEvent(asBytes, _metaData)); - } - - private class JsonTypeClass : EventInfo - { - public string Value { get; set; } - } - } -} diff --git a/src/Provausio.Practices.Tests/EventSourcing/EntityTests.cs b/src/Provausio.Practices.Tests/EventSourcing/EntityTests.cs deleted file mode 100644 index ad38d8c..0000000 --- a/src/Provausio.Practices.Tests/EventSourcing/EntityTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using Provausio.Practices.EventSourcing; -using Xunit; - -namespace Provausio.Practices.Tests.EventSourcing -{ - public class EntityTests - { - [Fact] - public void Equals_DifferentIds_NotEqual() - { - // arrange - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - var e1 = new TestEntity(id1); - var e2 = new TestEntity(id2); - - // act - var areEqual = e1.Equals(e2); - - // assert - Assert.False(areEqual); - } - - [Fact] - public void Equals_SameIds_AreEqual() - { - // arrange - var id1 = Guid.NewGuid(); - var e1 = new TestEntity(id1); - var e2 = new TestEntity(id1); - - // act - var areEqual = e1.Equals(e2); - - // assert - Assert.True(areEqual); - } - - [Fact] - public void EqualsOperator_DifferentIds_NotEqual() - { - // arrange - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - var e1 = new TestEntity(id1); - var e2 = new TestEntity(id2); - - // act - var areEqual = e1 == e2; - - // assert - Assert.False(areEqual); - } - - [Fact] - public void EqualsOperator_SameIds_AreEqual() - { - // arrange - var id1 = Guid.NewGuid(); - var e1 = new TestEntity(id1); - var e2 = new TestEntity(id1); - - // act - var areEqual = e1 == e2; - - // assert - Assert.True(areEqual); - } - - [Fact] - public void EqualsOperator_NullRightComparison_NotEqual() - { - // arrange - var id1 = Guid.NewGuid(); - var e1 = new TestEntity(id1); - - // act - var areEqual = e1 == null; - - // assert - Assert.False(areEqual); - } - - [Fact] - public void EqualsOperator_NullLeftComparison_AreEqual() - { - // arrange - TestEntity e1 = null; - - // act - var areEqual = e1 == null; - - // assert - Assert.True(areEqual); - } - - [Fact] - public void InequalityOperator_NullLeft_IsNotEqual() - { - // arrange - TestEntity e1 = null; - TestEntity e2 = new TestEntity(Guid.NewGuid()); - - // act - var areNotEqual = e1 != e2; - - // assert - Assert.True(areNotEqual); - } - - [Fact] - public void InequalityOperator_NullRight_IsNotEqual() - { - // arrange - TestEntity e1 = null; - TestEntity e2 = new TestEntity(Guid.NewGuid()); - - // act - var areNotEqual = e2 != e1; - - // assert - Assert.True(areNotEqual); - } - - private class TestEntity : Entity - { - public TestEntity(Guid id) - { - Id = id; - } - } - } -} diff --git a/src/Provausio.Practices.Tests/EventSourcing/EventInfoTests.cs b/src/Provausio.Practices.Tests/EventSourcing/EventInfoTests.cs deleted file mode 100644 index 614e389..0000000 --- a/src/Provausio.Practices.Tests/EventSourcing/EventInfoTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using Newtonsoft.Json; -using Provausio.Practices.EventSourcing; -using Xunit; - -namespace Provausio.Practices.Tests.EventSourcing -{ - [ExcludeFromCodeCoverage] - public class EventInfoTests - { - private byte[] _v1data; - private byte[] _v2data; - private byte[] _v1metadata; - private byte[] _v2metadata; - - public EventInfoTests() - { - _v1data = Encoding.UTF8.GetBytes("{\"Property1\": \"Hello world!\"}"); - _v2data = Encoding.UTF8.GetBytes("{\"Id\": \"b7a90398-e98d-4603-9e88-4be76009bf24\"}"); - _v1metadata = - Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new EventMetadata {EventNamespaces = "test1"})); - _v2metadata = - Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new EventMetadata { EventNamespaces = "test2" })); - } - - [Fact] - public void Ctor_DefaultId_Throws() - { - // arrange - - // act - - // assert - Assert.Throws(() => new FakeEventInfo(new Guid(), "foo")); - } - - [Fact] - public void Ctor_NullReason_Throws() - { - // arrange - - // act - - // assert - Assert.Throws(() => new FakeEventInfo(Guid.NewGuid(), null)); - } - - [Fact] - public void Ctor_EmptyReason_Throws() - { - // arrange - - // act - - // assert - Assert.Throws(() => new FakeEventInfo(Guid.NewGuid(), string.Empty)); - } - - [Fact] - public void Ctor_ValidParameters_EventGetsUniqueId() - { - // arrange - - // act - var e1 = new FakeEventInfo(Guid.NewGuid(), "foo"); - var e2 = new FakeEventInfo(Guid.NewGuid(), "bar"); - - // assert - Assert.NotEqual(new Guid(), e1.EventId); - Assert.NotEqual(new Guid(), e2.EventId); - Assert.NotEqual(e1.EventId, e2.EventId); - } - - [Fact] - public void GetData_SerializesCorrectly() - { - // arrange - var id = Guid.NewGuid(); - var original = new FakeEventInfo(id, "foo"); - - // act - var data = original.GetData(); - - // assert - var asJson = Encoding.UTF8.GetString(data); - var asObject = JsonConvert.DeserializeObject(asJson); - Assert.Equal(original, asObject); - } - - [Fact] - public void Ctor_DefaultCtor_IsPermittedForDeserialization() - { - // arrange - - // act - var info = new FakeEventInfo(); - - // assert - Assert.NotNull(info); - } - - [Fact] - public void GetHashCode_DifferentObjects_DifferentHashCode() - { - // arrange - var e1 = new FakeEventInfo {EventId = Guid.NewGuid()}; - var e2 = new FakeEventInfo {EventId = Guid.NewGuid()}; - - // act - var areEqual = e1.GetHashCode() == e2.GetHashCode(); - - // assert - Assert.False(areEqual); - } - - private class FakeEventInfo : EventInfo - { - public FakeEventInfo() - { - } - - public FakeEventInfo(Guid entityId, string reason) - : base(entityId, reason) - { - } - } - - [EventNamespace("test1")] - private class Version1 : EventInfo - { - public string Property1 { get; set; } - } - - [EventNamespace("test2")] - private class Version2 : EventInfo - { - public Guid Id { get; set; } - } - - [EventNamespace("test3")] - private class Version3 - { - public string Property2 { get; set; } - } - } -} diff --git a/src/Provausio.Practices.Tests/EventSourcing/EventTypeCacheTests.cs b/src/Provausio.Practices.Tests/EventSourcing/EventTypeCacheTests.cs deleted file mode 100644 index bc5132c..0000000 --- a/src/Provausio.Practices.Tests/EventSourcing/EventTypeCacheTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using Provausio.Practices.EventSourcing; -using Xunit; - -namespace Provausio.Practices.Tests.EventSourcing -{ - public class EventTypeCacheTests - { - // these tests will randomly fail when run on appveyor. Not sure why. - //[Theory] - //[InlineData("doubledecorated1")] - //[InlineData("doubledecorated2")] - public void ResolveType_DoubleDecorated_Resolves(string nameSpace) - { - // arrange - var cache = EventTypeCache.GetInstance(typeof(DoubleDecoratedClass)); - - // act - var type = cache.ResolveType(nameSpace); - - // assert - Assert.Equal(typeof(DoubleDecoratedClass), type); - } - - [Fact] - public void ResolveType_MultipleRequestedNamespacesMatch_DoesNotThrow() - { - // arrange - var cache = EventTypeCache.GetInstance(typeof(DoubleDecoratedClass)); - - // act - var type = cache.ResolveType("doubledecorated1;doubledecorated2"); - - // assert - Assert.True(true); - } - - [EventNamespace("Type1")] - public class Type1 : EventInfo - { - - } - - [EventNamespace("Type2")] - public class Type2 : EventInfo - { - - } - - [EventNamespace("doubledecorated1")] - [EventNamespace("doubledecorated2")] - private class DoubleDecoratedClass : EventInfo - { - - } - } -} diff --git a/src/Provausio.Practices.Tests/EventSourcing/RepositoryTests.cs b/src/Provausio.Practices.Tests/EventSourcing/RepositoryTests.cs deleted file mode 100644 index 38d2a98..0000000 --- a/src/Provausio.Practices.Tests/EventSourcing/RepositoryTests.cs +++ /dev/null @@ -1,383 +0,0 @@ -//using System; -//using System.Collections; -//using System.Collections.Generic; -//using System.Diagnostics; -//using System.Linq; -//using System.Text; -//using System.Threading.Tasks; -//using DAS.Practices.EventSourcing; -//using Moq; -//using Xunit; - -//namespace DAS.Practices.Tests.EventSourcing -//{ -// public class RepositoryTests -// { -// private readonly Mock _eventStoreMock; - -// public RepositoryTests() -// { -// _eventStoreMock = new Mock(); -// } - -// [Fact] -// public void Ctor_NullStreamName_Throws() -// { -// // arrange -// var storeMock = new Mock(); - -// // act - -// // assert -// Assert.Throws(() => new FakeRepo(null, storeMock.Object)); -// } - -// [Fact] -// public void Ctor_NullEventStore_Throws() -// { -// // arrange - -// // act - -// // assert -// Assert.Throws(() => new FakeRepo("foo", null)); -// } - -// #region -- Save -- - -// [Fact] -// public async Task Save_NoEvents_DoesntConnectToStore() -// { -// // arrange -// _eventStoreMock.Setup(m => m.Connect()); -// var aggregate = new FakeAggregate(); -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// await repo.Save(aggregate); - -// // assert -// _eventStoreMock.Verify(m => m.Connect(), Times.Never); -// } - -// [Fact] -// public async Task Save_NoEvents_DoesntCallStore() -// { -// // arrange -// _eventStoreMock.Setup(m => m.AppendEventsAsync(It.IsAny(), It.IsAny())); -// var aggregate = new FakeAggregate(); -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// await repo.Save(aggregate); - -// // assert -// _eventStoreMock.Verify(m => m.AppendEventsAsync( -// It.IsAny(), -// It.IsAny()), Times.Never); -// } - -// [Fact] -// public async Task Save_HasUnsavedEvents_ConnectsToStore() -// { -// // arrange -// _eventStoreMock.Setup(m => m.Connect()); -// _eventStoreMock.Setup(m => m.Connect()).Returns(Task.CompletedTask); - -// var aggregate = new FakeAggregate(); -// aggregate.DoThing(); - -// _eventStoreMock.Setup(m => m.AppendEventsAsync(It.IsAny(), It.IsAny())) -// .Returns(Task.CompletedTask); - - -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// await repo.Save(aggregate); - -// // assert -// _eventStoreMock.Verify(m => m.Connect(), Times.Once); -// } - -// [Fact] -// public async Task Save_HasUnsavedEvents_AppendsToStore() -// { -// // arrange -// _eventStoreMock.Setup(m => m.Connect()).Returns(Task.CompletedTask); - -// var aggregate = new FakeAggregate(); -// aggregate.DoThing(); - -// _eventStoreMock.Setup(m => m.AppendEventsAsync(It.IsAny(), It.IsAny())) -// .Returns(Task.CompletedTask); - - -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// await repo.Save(aggregate); - -// // assert -// _eventStoreMock.Verify(m => m.AppendEventsAsync(It.IsAny(), It.IsAny()), Times.Once); -// } - -// [Fact] -// public async Task Save_HasUnsavedEvents_Disconnects() -// { -// // arrange -// _eventStoreMock.Setup(m => m.Disconnect()); -// _eventStoreMock.Setup(m => m.Connect()) -// .Returns(Task.CompletedTask); - -// var aggregate = new FakeAggregate(); -// aggregate.DoThing(); - -// _eventStoreMock.Setup(m => m.AppendEventsAsync(It.IsAny(), It.IsAny())) -// .Returns(Task.CompletedTask); - - -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// await repo.Save(aggregate); - -// // assert -// _eventStoreMock.Verify(m => m.Disconnect(), Times.Once); -// } - -// [Fact] -// public async Task Save_HasUnsavedEvents_MarksChangesAsCommitted() -// { -// // arrange -// _eventStoreMock.Setup(m => m.Connect()).Returns(Task.CompletedTask); - -// var aggregate = new FakeAggregate(); -// aggregate.DoThing(); - -// _eventStoreMock.Setup(m => m.AppendEventsAsync(It.IsAny(), It.IsAny())) -// .Returns(Task.CompletedTask); - -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// await repo.Save(aggregate); - -// // assert -// Assert.Equal(1, aggregate.ChangesCommittedCalls); -// } - -// #endregion - -// #region -- GetById -- - -// [Fact] -// public async Task GetById_ConnectsToStore() -// { -// // arrange -// _eventStoreMock.Setup(m => m.Connect()).Returns(Task.CompletedTask); -// _eventStoreMock -// .Setup(m => m.GetEvents(It.IsAny(), It.IsAny())) -// .Returns(Task.FromResult(new EventStreamResult { Events = new List() })); -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// await repo.GetById(It.IsAny(), true); - -// // assert -// _eventStoreMock.Verify(m => m.Connect(), Times.Once); -// } - -// [Fact] -// public async Task GetById_UseSnapshot_NullSnapshot_StartsAtPositionZero() -// { -// // arrange -// _eventStoreMock -// .Setup(m => m.GetEvents(It.IsAny(), It.IsAny())) -// .Returns(Task.FromResult(new EventStreamResult { Events = new List()})); - -// _eventStoreMock -// .Setup(m => m.GetSnapshot(It.IsAny())) -// .Returns(Task.FromResult((EventInfo) null)); - -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// var result = await repo.GetById(It.IsAny(), true); - -// // assert -// _eventStoreMock.Verify(m => m.GetEvents(It.IsAny(), 0)); -// } - -// [Fact] -// public async Task GetById_UseSnapshot_NotNullSnapshot_StartsAtNextPosition() -// { -// // arrange -// const int snapshotPosition = 99; -// var snapshot = new FakeSnapshot(snapshotPosition); - -// _eventStoreMock -// .Setup(m => m.GetEvents(It.IsAny(), It.IsAny())) -// .Returns(Task.FromResult(new EventStreamResult { Events = new List() })); - -// _eventStoreMock -// .Setup(m => m.GetSnapshot(It.IsAny())) -// .Returns(Task.FromResult((EventInfo) snapshot)); - -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// var result = await repo.GetById(It.IsAny(), true); - -// // assert -// _eventStoreMock.Verify(m => m.GetEvents(It.IsAny(), snapshotPosition + 1)); -// } - - -// [Fact] -// public async Task GetById_NoSnapshotNeeded_Disconnects() -// { -// // arrange -// const int snapshotPosition = 99; -// var snapshot = new FakeSnapshot(snapshotPosition); - -// _eventStoreMock -// .Setup(m => m.GetEvents(It.IsAny(), It.IsAny())) -// .Returns(Task.FromResult(new EventStreamResult { Events = new List() })); - -// _eventStoreMock -// .Setup(m => m.GetSnapshot(It.IsAny())) -// .Returns(Task.FromResult((EventInfo)snapshot)); - -// _eventStoreMock.Setup(m => m.Disconnect()); - -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// var result = await repo.GetById(It.IsAny(), true); - -// // assert -// _eventStoreMock.Verify(m => m.Disconnect(), Times.Once); -// } - -// [Fact] -// public async Task GetById_SnapshotRequired_SavesSnapshot() -// { -// // arrange -// const int snapshotPosition = 99; -// var snapshot = new FakeSnapshot(snapshotPosition); - -// _eventStoreMock -// .Setup(m => m.GetEvents(It.IsAny(), It.IsAny())) -// .Returns(Task.FromResult(new EventStreamResult { Events = new List(), NeedsSnapshot = true })); - -// _eventStoreMock -// .Setup(m => m.GetSnapshot(It.IsAny())) -// .Returns(Task.FromResult((EventInfo)snapshot)); - -// _eventStoreMock -// .Setup(m => m.AppendEventsAsync(It.IsAny(), It.IsAny())) -// .Returns(Task.CompletedTask); - -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// var result = await repo.GetById(It.IsAny(), true); - -// // assert -// _eventStoreMock.Verify(m => m.AppendEventsAsync(It.IsAny(), It.IsAny()), Times.Once); -// } - -// #endregion - -// [Fact] -// public void Dispose_DisconnectsEventStore() -// { -// // arrange -// _eventStoreMock.Setup(m => m.Disconnect()); -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// repo.Dispose(); - -// // assert -// _eventStoreMock.Verify(m => m.Disconnect(), Times.Once); -// } - -// [Fact] -// public void Dispose_DisposesEventStore() -// { -// // arrange -// _eventStoreMock.Setup(m => m.Dispose()); -// var repo = new FakeRepo("foo", _eventStoreMock.Object); - -// // act -// repo.Dispose(); - -// // assert -// _eventStoreMock.Verify(m => m.Dispose(), Times.Once); -// } - -// private class FakeAggregate : Aggregate -// { -// public int ChangesCommittedCalls { get; private set; } - -// public FakeAggregate() -// { -// Changes = new List(); -// } - -// public FakeAggregate(List changes) -// { -// Changes = changes; -// } - -// public void DoThing() -// { -// var eventInfo = new FakeEvent(); -// ApplyEvent(eventInfo); -// } - -// public void Apply(EventInfo e) -// { -// Debug.Write("Added event"); -// } - -// public override EventInfo GetSnapshot() -// { -// return new FakeSnapshot(Version); -// } - -// public override ICollection GetUncommittedEvents() -// { -// return Changes; -// } - -// public override void MarkChangesAsCommitted() -// { -// ChangesCommittedCalls++; -// } -// } - -// private class FakeRepo : Repository -// { -// public FakeRepo(string streamName, IStoreEvents eventStore) -// : base(streamName, eventStore) -// { -// } -// } - -// private class FakeEvent : EventInfo -// { -// } - -// private class FakeSnapshot : EventInfo -// { -// public FakeSnapshot(int version) -// { -// Version = version; -// } -// } -// } -//} - diff --git a/src/Provausio.Practices.Tests/EventSourcing/TestRoot.cs b/src/Provausio.Practices.Tests/EventSourcing/TestRoot.cs new file mode 100644 index 0000000..c1ff41d --- /dev/null +++ b/src/Provausio.Practices.Tests/EventSourcing/TestRoot.cs @@ -0,0 +1,110 @@ +using System; +using Provausio.Practices.EventSourcing.Aggregate; + +namespace Provausio.Practices.Tests.EventSourcing +{ + public class TestRoot : AggregateRoot + { + private TestEntity _entity; + + public string Prop1 { get; private set; } + public int Prop2 { get; private set; } + public bool Action1Occured { get; private set; } + public string EntityProp => _entity?.Prop1; + + public static TestRoot Create(string prop1, int prop2) + { + var test = new TestRoot(); + test.Raise(e => + { + e.Id = Guid.NewGuid(); + e.Prop1 = prop1; + e.Prop2 = prop2; + }); + + return test; + } + + public void Action1() + { + Raise(e => { Console.Out.WriteLine("Action 1 occured"); }); + } + + public void CreateEntity(string prop1) + { + Raise(c => { c.Prop1 = prop1; }); + } + + public void UpdateEntity(string prop1) + { + _entity.Update(prop1); + } + + protected override void RegisterHandlers() + { + RegisterHandler(Apply); + RegisterHandler(Apply); + RegisterHandler(Apply); + } + + private void Apply(CreationEvent e) + { + Id = e.RootId; + Prop1 = e.Prop1; + Prop2 = e.Prop2; + } + + private void Apply(Action1Occured e) + { + Action1Occured = true; + } + + private void Apply(EntityCreated e) + { + _entity = CreateEntity(); + _entity.Prop1 = e.Prop1; + _entity.Id = e.Id.ToString(); + } + } + + public class TestEntity : Entity + { + public string Prop1 { get; set; } + + public void Update(string prop1) + { + Root.Raise(updated => updated.Prop1 = prop1); + } + + private void Apply(EntityUpdated e) + { + Prop1 = e.Prop1; + } + + public override void RegisterHandlers(AggregateRoot root) + { + root.RegisterHandler(Apply); + } + } + + public class EntityCreated : AggregateEvent + { + public string Prop1 { get; set; } + } + + public class EntityUpdated : AggregateEvent + { + public string Prop1 { get; set; } + } + + public class CreationEvent : AggregateEvent + { + public string Prop1 { get; set; } + public int Prop2 { get; set; } + } + + public class Action1Occured : AggregateEvent + { + + } +} diff --git a/src/Provausio.Practices.Tests/Provausio.Practices.Tests.csproj b/src/Provausio.Practices.Tests/Provausio.Practices.Tests.csproj index f7fc5ce..c69ce31 100644 --- a/src/Provausio.Practices.Tests/Provausio.Practices.Tests.csproj +++ b/src/Provausio.Practices.Tests/Provausio.Practices.Tests.csproj @@ -17,4 +17,8 @@ + + + + diff --git a/src/Provausio.Practices/EventSourcing/Aggregate/AggregateEvent.cs b/src/Provausio.Practices/EventSourcing/Aggregate/AggregateEvent.cs new file mode 100644 index 0000000..8397efb --- /dev/null +++ b/src/Provausio.Practices/EventSourcing/Aggregate/AggregateEvent.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using Provausio.Common.Ext; + +namespace Provausio.Practices.EventSourcing.Aggregate +{ + public class AggregateEvent : IAggregateEvent + { + public Guid Id { get; set; } + + public DateTimeOffset TimeStamp { get; set; } + + public string RootId { get; set; } + + public long Version { get; set; } + + public string Reason { get; set; } + + public string Type => GetType().Name; + + public AggregateEventMetadata Metadata { get; set; } = new AggregateEventMetadata(); + + public static T Create() + where T : IAggregateEvent, new() + { + var instance = Activator.CreateInstance(); + instance.Id = Guid.NewGuid(); + return instance; + } + } + + public class AggregateEventMetadata + { + public string FullTypeName { get; set; } + + public string AssemblyQualifiedType { get; set; } + + public string EventNamespaces => string.Join(";", this.FindAttributes().Select(ns => ns.Namespace)); + } +} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/Aggregate/AggregateRoot.cs b/src/Provausio.Practices/EventSourcing/Aggregate/AggregateRoot.cs new file mode 100644 index 0000000..399f383 --- /dev/null +++ b/src/Provausio.Practices/EventSourcing/Aggregate/AggregateRoot.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Provausio.Practices.EventSourcing.Aggregate +{ + public abstract class AggregateRoot + { + private readonly List _uncommittedEvents = new List(); + private readonly Dictionary _eventHandlers = new Dictionary(); + + /// + /// The Aggregate ID + /// + public string Id { get; internal set; } + + /// + /// Version number of the aggregate. + /// + public long Version { get; internal set; } = -1; + + /// + /// A list of uncommitted events. + /// + public IReadOnlyCollection UncommittedEvents => + new ReadOnlyCollection(_uncommittedEvents); + + /// + /// Initializes a new instance of + /// + protected AggregateRoot() => RegisterHandlers(); // should be safe despite virtual call + + /// + /// Sets the ID of the aggregate root. Only use this when creating a new aggregate or + /// when beginning a replay. + /// + /// + public void SetRootId(string id) + { + Id = id; + } + + /// + /// Replays an event stream and brings the object up to state. + /// + /// A stream of events that will be replayed through the aggregate. + /// + public void LoadFromStream(IEnumerable events) + { + foreach (var e in events) + { + if (e.Version != 0 && e.RootId != Id) // version 0 is usually the one that sets the root id so let it pass + throw new InvalidOperationException("The event's rootId does not match the id of this aggregate root. Your data may be corrupted. Check stream."); + + if (e.Version <= Version && Version != -1) + throw new InvalidOperationException( + $"Incoming event version ({e.Version}) is <= root version ({Version}). This is probably due to corrupted data."); + + if(e.Version > Version + 1) + throw new InvalidOperationException("Incoming event version more than +1 of the current aggregate version. An event may have been missed somehow and the data may be corrupted."); + + Apply(e); + Version = e.Version; + } + } + + /// + /// Convenience method creates a properly constructed instance of a non-root entity. + /// + /// The event type that will be registered. + /// + protected T CreateEntity(Func idFactory = null) + where T : Entity, new() + { + if (idFactory == null) + idFactory = x => Guid.NewGuid().ToString(); + + var instance = Activator.CreateInstance(); + instance.SetRoot(this); + instance.RegisterHandlers(this); + instance.Id = idFactory(instance); + return instance; + } + + /// + /// Registers handlers. To implement this, call for each event type () + /// + protected abstract void RegisterHandlers(); + + /// + /// Raises an event and then invokes handlers. + /// + /// Constructs the event. + /// + public void Raise(Action constructor) + where T : IAggregateEvent, new() + { + var instance = AggregateEvent.Create(); + constructor(instance); + Raise(instance); + } + + private void Raise(T e) where T : IAggregateEvent + { + e.RootId = Id; + _uncommittedEvents.Add(e); + + Apply(e); + } + + private void Apply(T e) + where T : IAggregateEvent + { + if (!TryInvokeHandler(e, out var ex)) throw ex; + } + + private bool TryInvokeHandler(T e, out Exception ex) + { + try + { + var eventType = e.GetType(); + if (!_eventHandlers.ContainsKey(eventType)) + { + ex = new HandlerNotRegisteredException( + eventType, $"No handler is registered for type '{typeof(T)}'"); + + } + else + { + _eventHandlers[eventType].DynamicInvoke(e); + ex = null; + } + } + catch (Exception exception) + { + ex = exception; + } + + return ex == null; + } + + /// + /// Registers a handler. + /// + /// + /// + public void RegisterHandler(Action handler) + where T : IAggregateEvent => _eventHandlers.Add(typeof(T), handler); + + /// + /// Clears uncommitted events. + /// + public void ClearEvents() => _uncommittedEvents.Clear(); + } +} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/Aggregate/Entity.cs b/src/Provausio.Practices/EventSourcing/Aggregate/Entity.cs new file mode 100644 index 0000000..f9dae99 --- /dev/null +++ b/src/Provausio.Practices/EventSourcing/Aggregate/Entity.cs @@ -0,0 +1,24 @@ +using System; + +namespace Provausio.Practices.EventSourcing.Aggregate +{ + public abstract class Entity + { + public string Id { get; set; } + + public abstract void RegisterHandlers(AggregateRoot root); + + public void SetRoot(AggregateRoot root) + { + Root = root; + } + + protected AggregateRoot Root { get; private set; } + + protected void Raise(Action constructor) + where T : IAggregateEvent, new() + { + Root.Raise(constructor); + } + } +} diff --git a/src/Provausio.Practices/EventSourcing/Aggregate/HandlerNotRegisteredException.cs b/src/Provausio.Practices/EventSourcing/Aggregate/HandlerNotRegisteredException.cs new file mode 100644 index 0000000..048ae73 --- /dev/null +++ b/src/Provausio.Practices/EventSourcing/Aggregate/HandlerNotRegisteredException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Provausio.Practices.EventSourcing.Aggregate +{ + public class HandlerNotRegisteredException : Exception + { + public Type HandlerType { get; } + + public HandlerNotRegisteredException(Type handlerType, string message) + : base(message) + { + HandlerType = handlerType; + } + } +} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/Aggregate/IAggregateEvent.cs b/src/Provausio.Practices/EventSourcing/Aggregate/IAggregateEvent.cs new file mode 100644 index 0000000..86a2790 --- /dev/null +++ b/src/Provausio.Practices/EventSourcing/Aggregate/IAggregateEvent.cs @@ -0,0 +1,19 @@ +using System; + +namespace Provausio.Practices.EventSourcing.Aggregate +{ + public interface IAggregateEvent + { + Guid Id { get; set; } + + DateTimeOffset TimeStamp { get; set; } + + string RootId { get; set; } + + long Version { get; set; } + + string Type { get; } + + AggregateEventMetadata Metadata { get; set; } + } +} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/AggregateRoot.cs b/src/Provausio.Practices/EventSourcing/AggregateRoot.cs deleted file mode 100644 index 38969ca..0000000 --- a/src/Provausio.Practices/EventSourcing/AggregateRoot.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Provausio.Practices.EventSourcing -{ - public abstract class AggregateRoot : AggregateRoot>, IAggregateRoot - { - } - - public abstract class AggregateRoot : IAggregateRoot - where TSnapshotType : EventInfo - { - private readonly object _lock = new object(); - protected List> Changes = new List>(); - - public TIdType Id { get; set; } - - /// - /// - /// Gets the version. The version is the last event version that was applied to the aggregate's stream by the repository that fetched it. - /// So if new events were added after the fetch, this number will not be affected. - /// - /// - /// The version. - /// - public long Version { get; protected set; } = -1; // new - - /// - /// - /// Gets the uncommitted events. - /// - /// - public virtual ICollection GetUncommittedEvents() - { - return Changes; - } - - /// - /// - /// Clears the uncommitted events. - /// - public virtual void ClearUncommittedEvents() - { - Changes.Clear(); - } - - /// - /// - /// Gets the snapshot. - /// - /// - public EventInfo GetSnapshot() - { - var snapshot = BuildSnapshot(); - snapshot.EntityId = Id; - return snapshot; - } - - public TSnapshotType GetState() - { - return (TSnapshotType) GetSnapshot(); - } - - protected abstract TSnapshotType BuildSnapshot(); - - /// - /// - /// Marks the changes as committed. - /// - public virtual void MarkChangesAsCommitted() - { - ClearUncommittedEvents(); - } - - /// - /// - /// Loads from history. - /// - /// The history. - public void LoadFromHistory(IEnumerable> history) - { - foreach (var @event in history) - RaiseEvent((dynamic)@event, false); - } - - /// - /// Applies the event to the aggregate root. - /// - /// The event. - protected void RaiseEvent(EventInfo @event) - { - @event.EntityId = Id; - @event.Version = Version + 1; - RaiseEvent(@event, true); - } - - /// - /// Constructs and applies the event to the aggregate root. - /// - /// - /// - protected void RaiseEvent(Action modifier) - where T : EventInfo, new() - { - var e = new T(); - modifier(e); - RaiseEvent(e); - } - - private void RaiseEvent(EventInfo @event, bool isNew) - { - lock (_lock) - { - if (@event.Version <= Version) - throw new InvalidOperationException($"Unexpected version. Current version is {Version} but incoming event is {@event.Version}"); - - this.AsDynamic().Apply(@event); - - // only apply changes if the Apply() was successful - if (isNew) - { - Changes.Add(@event); - } - else - { - // in other words, we only want to set the version when we're - // reading from the stream but not when events are being applied - // by the aggregate. That way, when it is a new aggregate, the - // version is always -1 (new). - Version = @event.Version; - } - } - } - } -} diff --git a/src/Provausio.Practices/EventSourcing/Deserialization/AttributeAsAssemblyQualifiedNameStrategy.cs b/src/Provausio.Practices/EventSourcing/Deserialization/AttributeAsAssemblyQualifiedNameStrategy.cs deleted file mode 100644 index dea7962..0000000 --- a/src/Provausio.Practices/EventSourcing/Deserialization/AttributeAsAssemblyQualifiedNameStrategy.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using DAS.Infrastructure.Ext; - -namespace Provausio.Practices.EventSourcing.Deserialization -{ - /// - /// Will use attribute to match on AssemblyQualifiedType found in the event metadata - /// - /// - public class AttributeAsAssemblyQualifiedNameStrategy : ByAttributeStrategy - { - public AttributeAsAssemblyQualifiedNameStrategy(params Type[] typeRefs) - : base(typeRefs) { } - - public AttributeAsAssemblyQualifiedNameStrategy(string path) - : base(path) { } - - protected override EventInfo Deserialize(byte[] eventData, byte[] eventMetadata) - { - var metaData = eventMetadata.DeserializeJson(); - var eventNamespace = new EventNamespace(metaData.AssemblyQualifiedType); - - // use simplified assembly name to ignore revision/build - var simpleNamespace = eventNamespace.GetSimpleNamespace(); - var dtoType = FindType(simpleNamespace); - var deserialized = eventData.DeserializeJson>(dtoType); - return deserialized; - } - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/Deserialization/ByAssemblyQualifiedTypeNameStrategy.cs b/src/Provausio.Practices/EventSourcing/Deserialization/ByAssemblyQualifiedTypeNameStrategy.cs deleted file mode 100644 index 48d2780..0000000 --- a/src/Provausio.Practices/EventSourcing/Deserialization/ByAssemblyQualifiedTypeNameStrategy.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Text; -using Newtonsoft.Json; - -namespace Provausio.Practices.EventSourcing.Deserialization -{ - public class ByAssemblyQualifiedTypeNameStrategy : EventDeserializationStrategy - { - protected override EventInfo Deserialize(byte[] eventData, byte[] eventMetadata) - { - var metaData = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(eventMetadata)); - if (string.IsNullOrEmpty(metaData?.AssemblyQualifiedType)) - throw new InvalidOperationException("Invalid or missing event metadata."); - - var type = Type.GetType(metaData.AssemblyQualifiedType); - return (EventInfo)JsonConvert.DeserializeObject(Encoding.UTF8.GetString(eventData), type); - } - } - - public class ByFullTypeNameStrategy : EventDeserializationStrategy - { - protected override EventInfo Deserialize(byte[] eventData, byte[] eventMetadata) - { - var metaData = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(eventMetadata)); - if (string.IsNullOrEmpty(metaData?.AssemblyQualifiedType)) - throw new InvalidOperationException("Invalid or missing event metadata."); - - var type = Type.GetType(metaData.FullTypeName); - return (EventInfo)JsonConvert.DeserializeObject(Encoding.UTF8.GetString(eventData), type); - } - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/Deserialization/ByAttributeStrategy.cs b/src/Provausio.Practices/EventSourcing/Deserialization/ByAttributeStrategy.cs deleted file mode 100644 index b5417f3..0000000 --- a/src/Provausio.Practices/EventSourcing/Deserialization/ByAttributeStrategy.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using DAS.Infrastructure.Ext; -using Provausio.Practices.Validation.Assertion; - -namespace Provausio.Practices.EventSourcing.Deserialization -{ - public class ByAttributeStrategy : EventDeserializationStrategy - { - private readonly EventTypeCache _eventTypeCache; - - public ByAttributeStrategy(string path) - { - _eventTypeCache = EventTypeCache.GetInstance(path); - } - - public ByAttributeStrategy(params Type[] typeRefs) - { - _eventTypeCache = EventTypeCache.GetInstance(typeRefs); - } - - protected override EventInfo Deserialize(byte[] eventData, byte[] eventMetaData) - { - var metaData = eventMetaData.DeserializeJson(); - var dtoType = FindType(metaData.EventNamespaces); - - var deserialized = eventData.DeserializeJson>(dtoType); - return deserialized; - } - - protected Type FindType(string nameSpace) - { - Ensure.IsNotNullOrEmpty(nameSpace, nameof(nameSpace)); - return _eventTypeCache.ResolveType(nameSpace); - } - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/Deserialization/ByJsonTypeStrategy.cs b/src/Provausio.Practices/EventSourcing/Deserialization/ByJsonTypeStrategy.cs deleted file mode 100644 index cd4b584..0000000 --- a/src/Provausio.Practices/EventSourcing/Deserialization/ByJsonTypeStrategy.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text; -using Newtonsoft.Json; - -namespace Provausio.Practices.EventSourcing.Deserialization -{ - public class ByJsonTypeStrategy : EventDeserializationStrategy - { - protected override EventInfo Deserialize(byte[] eventData, byte[] eventMetadata) - { - return (EventInfo)JsonConvert.DeserializeObject( - Encoding.UTF8.GetString(eventData), - new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }); - } - } -} diff --git a/src/Provausio.Practices/EventSourcing/Deserialization/EventDeserializationFactory.cs b/src/Provausio.Practices/EventSourcing/Deserialization/EventDeserializationFactory.cs deleted file mode 100644 index c78352b..0000000 --- a/src/Provausio.Practices/EventSourcing/Deserialization/EventDeserializationFactory.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using DAS.Infrastructure.Logging; - -namespace Provausio.Practices.EventSourcing.Deserialization -{ - public class EventDeserializationFactory : IEventDeserializationFactory - { - private readonly List> _deserializationStrategies = new List>(); - - private EventDeserializationFactory( - ByAttributeStrategy byAttributeStrategy, - AttributeAsAssemblyQualifiedNameStrategy attributeAsAssemblyQualifiedNameStrategy) - { - // in order of likeliness - _deserializationStrategies.Add(byAttributeStrategy); - _deserializationStrategies.Add(new ByJsonTypeStrategy()); - _deserializationStrategies.Add(new ByFullTypeNameStrategy()); - _deserializationStrategies.Add(new ByAssemblyQualifiedTypeNameStrategy()); - _deserializationStrategies.Add(attributeAsAssemblyQualifiedNameStrategy); - } - - public EventDeserializationFactory(string eventLibraryPath) - : this(new ByAttributeStrategy(eventLibraryPath), - new AttributeAsAssemblyQualifiedNameStrategy(eventLibraryPath)) { } - - public EventDeserializationFactory(params Type[] typeRefs) - : this(new ByAttributeStrategy(typeRefs), - new AttributeAsAssemblyQualifiedNameStrategy(typeRefs)) { } - - public bool TryDeserialize(byte[] eventData, byte[] eventMetaData, out EventInfo e) - { - e = null; - foreach (var strategy in _deserializationStrategies) - { - try - { - e = strategy.DeserializeEvent(eventData, eventMetaData); - if (e != null) - { - //Logger.Debug($"Deserialization succeeded using {strategy} ({e.GetType()})", this); - return true; - } - } - catch { } - } - - Logger.Error("All deserialization strategies failed.", this); - return false; - } - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/Deserialization/EventDeserializationStrategy.cs b/src/Provausio.Practices/EventSourcing/Deserialization/EventDeserializationStrategy.cs deleted file mode 100644 index ca072b2..0000000 --- a/src/Provausio.Practices/EventSourcing/Deserialization/EventDeserializationStrategy.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Provausio.Practices.Validation.Assertion; - -namespace Provausio.Practices.EventSourcing.Deserialization -{ - public abstract class EventDeserializationStrategy : IEventDeserializationStrategy - { - protected abstract EventInfo Deserialize(byte[] eventData, byte[] eventMetadata); - - public EventInfo DeserializeEvent(byte[] eventData, byte[] eventMetaData) - { - Validate(eventData, eventMetaData); - return Deserialize(eventData, eventMetaData); - } - - protected void Validate(byte[] eventData, byte[] metaData) - { - Ensure.IsNotNull(eventData, nameof(eventData)); - Ensure.IsNotNull(metaData, nameof(metaData)); - } - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/Deserialization/EventNamespace.cs b/src/Provausio.Practices/EventSourcing/Deserialization/EventNamespace.cs deleted file mode 100644 index ce4c2ca..0000000 --- a/src/Provausio.Practices/EventSourcing/Deserialization/EventNamespace.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using Provausio.Practices.Validation.Assertion; - -namespace Provausio.Practices.EventSourcing.Deserialization -{ - public class EventNamespace - { - public string AssemblyQualifiedName { get; } - - public string SimplifiedName { get; } - - public EventNamespace(string assemblyQualifiedName) - { - AssemblyQualifiedName = Ensure.IsNotNullOrEmpty(assemblyQualifiedName, nameof(assemblyQualifiedName)); - SimplifiedName = GetSimpleNamespace(); - } - - public string GetSimpleNamespace() - { - var parts = AssemblyQualifiedName.Split(','); - if (parts.Length < 3) - throw new FormatException($"Unexpected format for assembly qualified name: {AssemblyQualifiedName}"); - - var className = parts[0].Trim(); - var assemblyName = parts[1].Trim(); - var version = parts[2].Trim(); - - var v = new Version(version.Split('=')[1]); - - return $"{className}, {assemblyName}, Version={v.Major}.{v.Minor}"; - } - - public override bool Equals(object obj) - { - return obj != null && Equals(obj as EventNamespace); - } - - protected bool Equals(EventNamespace other) - { - return string.Equals(AssemblyQualifiedName, other.AssemblyQualifiedName) && string.Equals(SimplifiedName, other.SimplifiedName); - } - - public override int GetHashCode() - { - unchecked - { - return ((AssemblyQualifiedName?.GetHashCode() ?? 0) * 397) ^ (SimplifiedName?.GetHashCode() ?? 0); - } - } - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/Deserialization/IEventDeserializationFactory.cs b/src/Provausio.Practices/EventSourcing/Deserialization/IEventDeserializationFactory.cs deleted file mode 100644 index aeb3988..0000000 --- a/src/Provausio.Practices/EventSourcing/Deserialization/IEventDeserializationFactory.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Provausio.Practices.EventSourcing.Deserialization -{ - public interface IEventDeserializationFactory - { - bool TryDeserialize(byte[] eventData, byte[] eventMetadata, out EventInfo e); - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/Deserialization/IEventDeserializationStrategy.cs b/src/Provausio.Practices/EventSourcing/Deserialization/IEventDeserializationStrategy.cs deleted file mode 100644 index fe9c13b..0000000 --- a/src/Provausio.Practices/EventSourcing/Deserialization/IEventDeserializationStrategy.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Provausio.Practices.EventSourcing.Deserialization -{ - public interface IEventDeserializationStrategy - { - EventInfo DeserializeEvent(byte[] eventData, byte[] eventMetaData); - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/DynamicWrapper.cs b/src/Provausio.Practices/EventSourcing/DynamicWrapper.cs deleted file mode 100644 index 1f85d88..0000000 --- a/src/Provausio.Practices/EventSourcing/DynamicWrapper.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Dynamic; -using System.Linq; -using System.Reflection; - -namespace Provausio.Practices.EventSourcing -{ - public class PrivateReflectionDynamicObject : DynamicObject - { - - private static readonly IDictionary> PropertiesOnType = new ConcurrentDictionary>(); - - // Simple abstraction to make field and property access consistent - private interface IProperty - { - string Name { get; } - object GetValue(object obj, object[] index); - void SetValue(object obj, object val, object[] index); - } - - // IProperty implementation over a PropertyInfo - private class Property : IProperty - { - internal PropertyInfo PropertyInfo { private get; set; } - - string IProperty.Name => PropertyInfo.Name; - - object IProperty.GetValue(object obj, object[] index) - { - return PropertyInfo.GetValue(obj, index); - } - - void IProperty.SetValue(object obj, object val, object[] index) - { - PropertyInfo.SetValue(obj, val, index); - } - } - - // IProperty implementation over a FieldInfo - private class Field : IProperty - { - internal FieldInfo FieldInfo { get; set; } - - string IProperty.Name => FieldInfo.Name; - - - object IProperty.GetValue(object obj, object[] index) - { - return FieldInfo.GetValue(obj); - } - - void IProperty.SetValue(object obj, object val, object[] index) - { - FieldInfo.SetValue(obj, val); - } - } - - - private object RealObject { get; set; } - private const BindingFlags BindingFlags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic; - - internal static object WrapObjectIfNeeded(object o) - { - // Don't wrap primitive types, which don't have many interesting internal APIs - if (o == null || o.GetType().IsPrimitive || o is string) - return o; - - return new PrivateReflectionDynamicObject { RealObject = o }; - } - - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - var prop = GetProperty(binder.Name); - - // Get the property value - result = prop.GetValue(RealObject, index: null); - - // Wrap the sub object if necessary. This allows nested anonymous objects to work. - result = WrapObjectIfNeeded(result); - - return true; - } - - public override bool TrySetMember(SetMemberBinder binder, object value) - { - var prop = GetProperty(binder.Name); - - // Set the property value - prop.SetValue(RealObject, value, index: null); - - return true; - } - - public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) - { - // The indexed property is always named "Item" in C# - var prop = GetIndexProperty(); - result = prop.GetValue(RealObject, indexes); - - // Wrap the sub object if necessary. This allows nested anonymous objects to work. - result = WrapObjectIfNeeded(result); - - return true; - } - - public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) - { - // The indexed property is always named "Item" in C# - var prop = GetIndexProperty(); - prop.SetValue(RealObject, value, indexes); - return true; - } - - // Called when a method is called - public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) - { - result = InvokeMemberOnType(RealObject.GetType(), RealObject, binder.Name, args); - - // Wrap the sub object if necessary. This allows nested anonymous objects to work. - result = WrapObjectIfNeeded(result); - - return true; - } - - public override bool TryConvert(ConvertBinder binder, out object result) - { - result = Convert.ChangeType(RealObject, binder.Type); - return true; - } - - public override string ToString() - { - return RealObject.ToString(); - } - - private IProperty GetIndexProperty() - { - // The index property is always named "Item" in C# - return GetProperty("Item"); - } - - private IProperty GetProperty(string propertyName) - { - // Get the list of properties and fields for this type - var typeProperties = GetTypeProperties(RealObject.GetType()); - - // Look for the one we want - IProperty property; - if (typeProperties.TryGetValue(propertyName, out property)) - { - return property; - } - - // The property doesn't exist - - // Get a list of supported properties and fields and show them as part of the exception message - // For fields, skip the auto property backing fields (which name start with <) - var propNames = typeProperties.Keys.Where(name => name[0] != '<').OrderBy(name => name); - throw new ArgumentException( - $"The property {propertyName} doesn't exist on type {RealObject.GetType()}. Supported properties are: {string.Join(", ", propNames)}"); - } - - private static IDictionary GetTypeProperties(Type type) - { - // First, check if we already have it cached - IDictionary typeProperties; - if (PropertiesOnType.TryGetValue(type, out typeProperties)) - { - return typeProperties; - } - - // Not cache, so we need to build it - - typeProperties = new ConcurrentDictionary(); - - // First, add all the properties - foreach (var prop in type.GetProperties(BindingFlags).Where(p => p.DeclaringType == type)) - { - typeProperties[prop.Name] = new Property() { PropertyInfo = prop }; - } - - // Now, add all the fields - foreach (var field in type.GetFields(BindingFlags).Where(p => p.DeclaringType == type)) - { - typeProperties[field.Name] = new Field() { FieldInfo = field }; - } - - // Finally, recurse on the base class to add its fields - if (type.BaseType != null) - { - foreach (var prop in GetTypeProperties(type.BaseType).Values) - { - typeProperties[prop.Name] = prop; - } - } - - // Cache it for next time - PropertiesOnType[type] = typeProperties; - - return typeProperties; - } - - private static object InvokeMemberOnType(Type type, object target, string name, object[] args) - { - try - { - // Try to incoke the method - return type.InvokeMember( - name, - BindingFlags.InvokeMethod | BindingFlags, - null, - target, - args); - } - catch (MissingMethodException) - { - // If we couldn't find the method, try on the base class - return type.BaseType != null - ? InvokeMemberOnType(type.BaseType, target, name, args) - : null; - //quick greg hack to allow methods to not exist! - } - } - } - - - public static class PrivateReflectionDynamicObjectExtensions - { - public static dynamic AsDynamic(this object o) - { - return PrivateReflectionDynamicObject.WrapObjectIfNeeded(o); - } - } -} diff --git a/src/Provausio.Practices/EventSourcing/EventInfo.cs b/src/Provausio.Practices/EventSourcing/EventInfo.cs deleted file mode 100644 index 4cd1f06..0000000 --- a/src/Provausio.Practices/EventSourcing/EventInfo.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Linq; -using DAS.Infrastructure.Ext; -using Provausio.Practices.Validation.Assertion; - -namespace Provausio.Practices.EventSourcing -{ - public abstract class EventInfo : JsonEventData - { - /// - /// Gets the type of the event. - /// - /// - /// The type of the event. - /// - public string EventType => GetType().Name; - - /// - /// Gets the metadata. - /// - /// - /// The metadata. - /// - public EventMetadata Metadata { get; set; } - - /// - /// Gets or sets the entity identifier. - /// - /// - /// The entity identifier. - /// - public T EntityId { get; set; } - - /// - /// Gets or sets the event identifier. Used for idempotent operations. - /// - /// - /// The event identifier. - /// - public Guid EventId { get; set; } - - /// - /// Gets or sets the version (the event sequence number). - /// - /// - /// The version. - /// - public long Version { get; set; } - - /// - /// Gets or sets the reason for the event. - /// - /// - /// The reason. - /// - public string Reason { get; set; } - - /// - /// Initializes a new instance of the class. - /// - protected EventInfo() - { - EventId = Guid.NewGuid(); - - var nameSpaces = this.FindAttributes(); - var namespaceNames = string.Join(";", nameSpaces.Select(ns => ns.Namespace)); - - Metadata = new EventMetadata - { - FullTypeName = GetType().FullName, - AssemblyQualifiedType = GetType().AssemblyQualifiedName, - EventNamespaces = namespaceNames - }; - } - - /// - /// - /// Initializes a new instance of the class. - /// - /// The entity identifier. - /// The reason. - protected EventInfo(T entityId, string reason) - : this() - { - EntityId = Ensure.IsNotDefault(entityId, nameof(entityId)); - Reason = Ensure.IsNotNullOrEmpty(reason, nameof(reason)); - } - - public override bool Equals(object obj) - { - return Equals(obj as EventInfo); - } - - protected bool Equals(EventInfo other) - { - return other != null && EventId.Equals(other.EventId); - } - - public override int GetHashCode() - { - // needed to allow setter for deserialization :/ - return EventId.GetHashCode(); - } - } -} diff --git a/src/Provausio.Practices/EventSourcing/EventMetadata.cs b/src/Provausio.Practices/EventSourcing/EventMetadata.cs deleted file mode 100644 index d3dcb2f..0000000 --- a/src/Provausio.Practices/EventSourcing/EventMetadata.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Provausio.Practices.EventSourcing -{ - public class EventMetadata : JsonEventData - { - /// - /// The full type name. - /// - public string FullTypeName { get; set; } - - /// - /// The assembly qualified type name. - /// - public string AssemblyQualifiedType { get; set; } - - /// - /// Declared namespaces - /// - public string EventNamespaces { get; set; } - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/EventStreamResult.cs b/src/Provausio.Practices/EventSourcing/EventStreamResult.cs deleted file mode 100644 index c722432..0000000 --- a/src/Provausio.Practices/EventSourcing/EventStreamResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; - -namespace Provausio.Practices.EventSourcing -{ - public class EventStreamResult - { - /// - /// Gets or sets the events. - /// - /// - /// The events. - /// - public List> Events { get; set; } - - /// - /// Gets or sets a value indicating whether or not a snapshot should be taken as a followup action. - /// - /// - /// true if [needs snapshot]; otherwise, false. - /// - public bool NeedsSnapshot { get; set; } - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/EventTypeCache.cs b/src/Provausio.Practices/EventSourcing/EventTypeCache.cs deleted file mode 100644 index ae2c3af..0000000 --- a/src/Provausio.Practices/EventSourcing/EventTypeCache.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using DAS.Infrastructure.Logging; -using Provausio.Practices.Validation.Assertion; - -namespace Provausio.Practices.EventSourcing -{ - public class EventTypeCache - { - private static readonly object Lock = new object(); - private static EventTypeCache _instance; - private List _decoratedTypes = new List(); - - private EventTypeCache() { } - - public static EventTypeCache GetInstance(string path) - { - // double-check lock (seems redundant, but it's not) - if (_instance == null) - { - lock (Lock) - { - if (_instance == null) - { - Logger.Debug($"Event type cache is uninitialized. Initializing now from {path}...", null); - _instance = new EventTypeCache(); - var allTypes = GetAssemblyTypes(path); - _instance.SetDecoratedTypes(allTypes); - } - } - } - return _instance; - } - - /// - /// Gets the instance. - /// - /// List of types that live in the library you wanna scan. - /// - public static EventTypeCache GetInstance(params Type[] typeRefs) - { - // double-check lock (seems redundant, but it's not) - if (_instance == null) - { - lock (Lock) - { - if (_instance == null) - { - _instance = new EventTypeCache(); - var allTypes = GetAssemblyTypes(typeRefs); - _instance.SetDecoratedTypes(allTypes); - } - } - } - return _instance; - } - - /// - /// Attempts to find the object type that matches the requested namespace. - /// - /// The namespaces associated with the object. Semicolon (;) delimited. NOTE: "namespace" refers to the value of - /// if set to true [throw on not found]. If false, method will return null. - /// - /// - /// - public Type ResolveType(string nameSpaces, bool throwOnNotFound = true) - { - Ensure.IsNotNullOrEmpty(nameSpaces, nameof(nameSpaces)); - - var namespacesThatCanMatch = nameSpaces.Split(';'); - - var matchingTypes = new List(); - foreach (var type in _decoratedTypes) - { - // we're just checking each type to see if at least one of - // the namespaces match the attribute value - try - { - var attributes = type - .GetCustomAttributes(typeof(EventNamespaceAttribute), false) - .FirstOrDefault( - t => namespacesThatCanMatch.Contains( - ((EventNamespaceAttribute) t).Namespace)); - - if (attributes != null) - matchingTypes.Add(type); - } - catch (InvalidOperationException ex) - { - var x = type - .GetCustomAttributes(typeof(EventNamespaceAttribute), false) - .Where(member => namespacesThatCanMatch.Contains(((EventNamespaceAttribute) member).Namespace)) - .Select(o => o.GetType().FullName); - - Logger.Fatal($"Failed to gather EventNamespaceAttribute for {nameSpaces}. The following types were found to host the specified namespaces: {string.Join(", ", x)}", this, ex); - throw; - } - } - - if (matchingTypes.Count == 1) - return matchingTypes.First(); - - if (matchingTypes.Count > 1) - throw new InvalidOperationException($"More than 1 object was found with the namespace '{nameSpaces}'"); - - if (throwOnNotFound) - throw new Exception($"No objects were found with the event namespace '{nameSpaces}'"); - - return null; - } - - private static IEnumerable GetAssemblyTypes(params Type[] typeRefs) - { - var types = new List(); - foreach (var type in typeRefs) - { - var eventInfoTypes = GetTypes(type.Assembly); - types.AddRange(eventInfoTypes); - } - - return types; - } - - private static IEnumerable GetTypes(Assembly assy) - { - Logger.Debug($"Scanning types in {assy.FullName}", null); - var eventInfoTypes = assy - .GetTypes() - .Where(t => !t.IsAbstract && IsDerivedOfGenericType(t, typeof(EventInfo<>))); - - return eventInfoTypes; - } - - private static bool IsDerivedOfGenericType(Type type, Type genericType) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == genericType) - return true; - - return type.BaseType != null && IsDerivedOfGenericType(type.BaseType, genericType); - } - - private static IEnumerable GetAssemblyTypes(string path) - { - try - { - var paths = Directory.GetFiles(path, "*.dll"); - - Logger.Debug($"Attempting to load {string.Join(",", paths)}", null); - - var types = new List(); - foreach (var filePath in paths) - { - var assy = Assembly.LoadFile(filePath); - var eventInfoTypes = GetTypes(assy); - types.AddRange(eventInfoTypes); - } - - Logger.Debug($"Found {types.Count} types in {paths.Length} assemblies at {path}.", null); - return types; - } - catch (ReflectionTypeLoadException ex) - { - var message = ex.LoaderExceptions.Select(e => e.Message); - Logger.Fatal($"Failed to load assembly: \r\n{message}", null, ex); - throw; - } - catch (Exception ex) - { - Logger.Fatal(ex.Message, null, ex); - throw; - } - } - - private void SetDecoratedTypes(IEnumerable types) - { - var typeList = types?.ToList(); - if(typeList == null || !typeList.Any()) - { - _decoratedTypes = new List(); - return; - } - - _decoratedTypes = typeList - .Where(t => t.GetCustomAttributes(typeof(EventNamespaceAttribute), false).Length > 0 && IsDerivedOfGenericType(t, typeof(EventInfo<>))) - .ToList(); - - var typeNames = string.Join(",", _decoratedTypes.Select(t => t.Name)); - Logger.Debug($"Cached {_decoratedTypes.Count} decorated event types: {typeNames}", this); - } - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/IAggregateRoot.cs b/src/Provausio.Practices/EventSourcing/IAggregateRoot.cs deleted file mode 100644 index 0cab966..0000000 --- a/src/Provausio.Practices/EventSourcing/IAggregateRoot.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Collections; -using System.Collections.Generic; - -namespace Provausio.Practices.EventSourcing -{ - /// - /// Entities contain an identity. All instances of that possess the same ID are considered to be equal. - /// - - public interface IEntity - { - T Id { get; set; } - } - - public abstract class Entity : IEntity - { - public T Id { get; set; } - - protected bool Equals(Entity other) - { - return Id.Equals(other.Id); - } - - public bool Equals(IEntity other) - { - return Id.Equals(other.Id); - } - - public override bool Equals(object obj) - { - if (obj is null) return false; - if (ReferenceEquals(this, obj)) return true; - return obj.GetType() == GetType() && Equals((Entity)obj); - } - - public static bool operator ==(Entity left, Entity right) - { - if (ReferenceEquals(left, right)) - return true; - - if ((object)left == null || (object)right == null) - { - return false; - } - - return left.Equals(right); - } - - public static bool operator !=(Entity left, Entity right) - { - if (ReferenceEquals(left, right)) - return false; - - if ((object)left == null || (object)right == null) - { - return true; - } - - return left.Equals(right); - } - - public override int GetHashCode() - { - // allowing this to facilitate factory create methods. - // ReSharper disable once NonReadonlyMemberInGetHashCode - return Id.GetHashCode(); - } - } - - /// - /// - /// Extension of . Introduced as a non-breaking change to add a more precise GetState method. - /// - /// - /// - public interface IAggregateRoot : IAggregateRoot> - { - } - - public interface IAggregateRoot : IEntity - where TSnapshotType : EventInfo - { - /// - /// Gets the version. - /// - /// - /// The version. - /// - long Version { get; } - - /// - /// Gets the uncommitted events. - /// - /// - ICollection GetUncommittedEvents(); - - /// - /// Clears the uncommitted events. - /// - void ClearUncommittedEvents(); - - /// - /// Marks the changes as committed. - /// - void MarkChangesAsCommitted(); - - /// - /// Loads from history. - /// - /// The history. - void LoadFromHistory(IEnumerable> history); - - /// - /// Gets the snapshot. - /// - /// - EventInfo GetSnapshot(); - - /// - /// Gets the snapshot. - /// - /// - /// - TSnapshotType GetState(); - } -} diff --git a/src/Provausio.Practices/EventSourcing/IProjection.cs b/src/Provausio.Practices/EventSourcing/IProjection.cs deleted file mode 100644 index 4b2c468..0000000 --- a/src/Provausio.Practices/EventSourcing/IProjection.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Threading.Tasks; - -namespace Provausio.Practices.EventSourcing -{ - public interface IProjection - { - /// - /// If true, this projection will not actually start when Start() is called. Used for testing purposes. - /// - bool BypassStart { get; } - - /// - /// Gets the name of the projection. - /// - /// - /// The name. - /// - string Name { get; } - - /// - /// Starts this instance. - /// - Task StartAsync(); - - /// - /// Stops this instance. - /// - Task StopAsync(); - } -} diff --git a/src/Provausio.Practices/EventSourcing/IRepository.cs b/src/Provausio.Practices/EventSourcing/IRepository.cs deleted file mode 100644 index 5c45aa3..0000000 --- a/src/Provausio.Practices/EventSourcing/IRepository.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Threading.Tasks; - -namespace Provausio.Practices.EventSourcing -{ - public interface IRepository : IRepository> - where TEntityType : IAggregateRoot> - { - } - - public interface IRepository - where TSnapshotType : EventInfo - where TEntityType : IAggregateRoot - { - /// - /// Saves the specified entity. - /// - /// The entity. - /// - Task Save(TEntityType entity); - - /// - /// Saves the specified entity and then gets the updated version. - /// - /// - /// - Task SaveAndUpdate(TEntityType entity); - - /// - /// Gets the by identifier. - /// - /// The identifier. - /// if set to true [use snapshot]. - /// - Task GetById(TIdType id, bool useSnapshot); - } -} diff --git a/src/Provausio.Practices/EventSourcing/IStoreEvents.cs b/src/Provausio.Practices/EventSourcing/IStoreEvents.cs deleted file mode 100644 index 2fe8253..0000000 --- a/src/Provausio.Practices/EventSourcing/IStoreEvents.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Provausio.Practices.EventSourcing -{ - public interface IStoreEvents : IDisposable - { - Task AppendEventsAsync(string streamName, params EventInfo[] infos); - - Task> GetEvents(string streamname, int startingPosition); - - Task> GetSnapshot(string streamName); - - Task Connect(); - - void Disconnect(); - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/ISubscriptionAdapter.cs b/src/Provausio.Practices/EventSourcing/ISubscriptionAdapter.cs deleted file mode 100644 index af611ad..0000000 --- a/src/Provausio.Practices/EventSourcing/ISubscriptionAdapter.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Provausio.Practices.EventSourcing -{ - public interface ISubscriptionAdapter - { - bool EnableSubscriptionLogging { get; set; } - - /// - /// Starts a catch-up subscription. - /// - /// Name of the stream. - /// Last index of the stream. - /// Delegate that will process an event. - /// The size of the queue when running live subscription. - /// The number of events that will be processed at a time when catchup is running. - /// - /// Whether or not a link-to should be resolved. - void CatchUpSubscription( - string streamName, - long lastSeenIndex, - Func, long, Task> processAction, - int liveQueueSize = 10000, - int readBatchSize = 500, - bool enableVerboseClientLogging = false, - bool resolveLinkTos = true); - - /// - /// Starts a persistent subscription. - /// - /// Name of the group. - /// Name of the stream. - /// Delegate that will process an event. - void PersistentSubscription( - string groupName, - string streamName, - Func, long, Task> processAction); - - /// - /// Stops this instance. - /// - void Stop(); - } -} diff --git a/src/Provausio.Practices/EventSourcing/ISubscriptionProvider.cs b/src/Provausio.Practices/EventSourcing/ISubscriptionProvider.cs deleted file mode 100644 index ea20244..0000000 --- a/src/Provausio.Practices/EventSourcing/ISubscriptionProvider.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Provausio.Practices.EventSourcing -{ - public interface ISubscriptionProvider - { - - } -} diff --git a/src/Provausio.Practices/EventSourcing/JsonEventData.cs b/src/Provausio.Practices/EventSourcing/JsonEventData.cs deleted file mode 100644 index 8a2f77d..0000000 --- a/src/Provausio.Practices/EventSourcing/JsonEventData.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Text; -using Newtonsoft.Json; - -namespace Provausio.Practices.EventSourcing -{ - public class JsonEventData - { - public byte[] GetData() - { - var asJson = JsonConvert.SerializeObject(this, new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All}); - return Encoding.UTF8.GetBytes(asJson); - } - } -} \ No newline at end of file diff --git a/src/Provausio.Practices/EventSourcing/Projection.cs b/src/Provausio.Practices/EventSourcing/Projection.cs deleted file mode 100644 index f164c55..0000000 --- a/src/Provausio.Practices/EventSourcing/Projection.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Threading.Tasks; -using DAS.Infrastructure.Logging; -using Microsoft.CSharp.RuntimeBinder; -using Provausio.Practices.Validation.Assertion; - -namespace Provausio.Practices.EventSourcing -{ - public abstract class Projection : IProjection - where TClientType : class - { - protected const string IndexCacheName = "ProjectionIndexCache"; - - private readonly TClientType _client; - protected readonly string ClassName; - - /// - /// - /// If true, this projection will not actually start when Start() is called. Used for testing purposes. - /// - public bool BypassStart { get; set; } - - /// - /// - /// Gets the name of the projection. This is used to identify it in the cache. - /// - /// - /// The name. - /// - public string Name { get; } - - /// - /// Gets the last recorded index. - /// - /// - /// The last index. - /// - public long LastIndex { get; private set; } - - protected Projection(string name, TClientType client) - { - _client = Ensure.IsNotNull(client, nameof(client)); - - Name = Ensure.IsNotNullOrEmpty(name, nameof(name)); - ClassName = GetType().Name; - } - - /// - /// Performs the provide save action and updates the index. - /// - /// The save action. - /// Index of the latest. - /// - public async Task SaveAsync(Func saveAction, long latestIndex) - { - try - { - await OnSaveAsync(saveAction, latestIndex, _client).ConfigureAwait(false); - - Logger.Verbose($"{ClassName}::{latestIndex} - Done! Updating index cache...", this); - - await UpdateIndexAsync(latestIndex).ConfigureAwait(false); - - Logger.Verbose($"{ClassName}::{latestIndex} - Done!", this); - LastIndex = latestIndex; - } - catch (Exception ex) - { - Logger.Fatal($"{ClassName}::{latestIndex} - Save action failed.", this, ex); - throw; - } - } - - /// - /// Overriding this method allows the child class to control wrap - /// the save action passed into SaveAsync (e.g. add retry logic, etc.) - /// while still honoring things like the throttle. - /// - /// The same save action that was given to SaveAsync(...) - /// - /// The instance of that was supplied to the ctor - /// - protected virtual async Task OnSaveAsync(Func saveAction, long latestIndex, TClientType client) - { - Logger.Verbose($"{ClassName}::{latestIndex} - Saving...", this); - await saveAction(_client).ConfigureAwait(false); - } - - protected async Task InitializeAsync() - { - Logger.Verbose($"Running projection initialization... \"{GetType().Name}\"...", this); - Logger.Verbose("Fetching last known index...", this); - LastIndex = await GetLastIndexAsync().ConfigureAwait(false); - Logger.Verbose($"Found Index {LastIndex}", this); - Logger.Verbose("Initialization completed...", this); - } - - protected virtual async Task ProcessEvent(EventInfo e, long index) - { - if (LastIndex >= index) - { - Logger.Verbose("Index is less than last recorded {@index}", this, new { LastIndex, ReceivedIndex = index }); - return; - } - - try - { - var t = this.AsDynamic().Process(e, index) as Task; - if (t != null) await t; - - Logger.Verbose($"{ClassName}::Dynamically invoked {{@Data}}", this, new - { - EventType = e.GetType().Name, - TargetObject = GetType().Name - }); - } - catch (RuntimeBinderException ex) - { - Logger.Fatal($"{ClassName}::ProcessEvent failed! Process(EventInfo, long). Does Process() return Task?", this, ex); - } - catch (Exception ex) - { - Logger.Fatal($"{ClassName}::Could not dynamically call child process.", this, ex, e); - throw; - } - } - - /// - /// Starts this instance. - /// - public abstract Task StartAsync(); - - /// - /// Stops this instance. - /// - public abstract Task StopAsync(); - - /// - /// Retrieves the last known index. - /// - /// - protected abstract Task GetLastIndexAsync(); - - /// - /// Updates the last known index with the cache. - /// - /// - protected abstract Task UpdateIndexAsync(long latestIndex); - } -} diff --git a/src/Provausio.Practices/EventSourcing/ProjectionLoader.cs b/src/Provausio.Practices/EventSourcing/ProjectionLoader.cs deleted file mode 100644 index 4d36758..0000000 --- a/src/Provausio.Practices/EventSourcing/ProjectionLoader.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using DAS.Infrastructure.Logging; -using Microsoft.Extensions.DependencyInjection; - -namespace Provausio.Practices.EventSourcing -{ - public static class ProjectionLoader - { - /// - /// Scans the assembly that contains the specified type and then starts the projections. - /// - /// - /// - /// - public static async Task ScanForAndStartProjections( - IServiceProvider provider, - Type targetAssemblyType) - { - var projectionTypes = - Assembly.GetAssembly(targetAssemblyType) - .GetTypes() - .Where(type => type.GetInterfaces().Contains(typeof(IProjection)) && !type.IsAbstract) - .ToArray(); - - await StartProjections(provider, projectionTypes).ConfigureAwait(false); - } - - /// - /// Starts the specified projections. - /// - /// - /// - /// - public static async Task StartProjections( - IServiceProvider serviceProvider, - params Type[] projectionTypes) - { - Logger.Information($"Found {projectionTypes.Length} projections. Attempting to start...", null); - - foreach (var projectionType in projectionTypes) - { - var projection = (IProjection) ActivatorUtilities.CreateInstance(serviceProvider, projectionType); - await projection.StartAsync().ConfigureAwait(false); - Logger.Information($"Started {projectionType.FullName}!", null); - } - } - } -} diff --git a/src/Provausio.Practices/Properties/PropertyInfo.cs b/src/Provausio.Practices/Properties/PropertyInfo.cs new file mode 100644 index 0000000..8ad4a88 --- /dev/null +++ b/src/Provausio.Practices/Properties/PropertyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Provausio.Practices.Tests")] \ No newline at end of file diff --git a/src/Provausio.Practices/Provausio.Practices.csproj b/src/Provausio.Practices/Provausio.Practices.csproj index f3a51db..1c8a1d3 100644 --- a/src/Provausio.Practices/Provausio.Practices.csproj +++ b/src/Provausio.Practices/Provausio.Practices.csproj @@ -26,9 +26,13 @@ - + + + + +