From 43a11f7b0914eff374cb7135830d6f56bdf40fef Mon Sep 17 00:00:00 2001 From: Gabriel Lima Gomes Date: Sat, 18 Sep 2021 22:39:24 -0300 Subject: [PATCH] complexity reduced + tests --- src/CoreDomain.Tests/AggregateBaseTest.cs | 41 +++++++ src/CoreDomain.Tests/Common/Generator.cs | 58 ++++++++++ src/CoreDomain.Tests/DomainEventBaseTest.cs | 32 ++++++ src/CoreDomain.Tests/EntityTest.cs | 102 ++++++++++++++---- .../Models/MyCustomAggregate.cs | 24 +++++ src/CoreDomain.Tests/Models/MyCustomEntity.cs | 9 ++ src/CoreDomain.Tests/Models/MyCustomEvent.cs | 23 ++++ .../Models/MyCustomValueObject.cs | 24 +++++ src/CoreDomain.Tests/ValueObjectTest.cs | 33 ++++++ src/CoreDomain/Interfaces/IAggregateEvent.cs | 13 +++ src/CoreDomain/Interfaces/IDomainEvent.cs | 9 +- src/CoreDomain/Models/AggregateBase.cs | 22 +++- src/CoreDomain/Models/DomainEventBase.cs | 22 +--- src/CoreDomain/Models/ValueObject.cs | 54 ++++++---- 14 files changed, 398 insertions(+), 68 deletions(-) create mode 100644 src/CoreDomain.Tests/AggregateBaseTest.cs create mode 100644 src/CoreDomain.Tests/Common/Generator.cs create mode 100644 src/CoreDomain.Tests/DomainEventBaseTest.cs create mode 100644 src/CoreDomain.Tests/Models/MyCustomAggregate.cs create mode 100644 src/CoreDomain.Tests/Models/MyCustomEntity.cs create mode 100644 src/CoreDomain.Tests/Models/MyCustomEvent.cs create mode 100644 src/CoreDomain.Tests/Models/MyCustomValueObject.cs create mode 100644 src/CoreDomain.Tests/ValueObjectTest.cs create mode 100644 src/CoreDomain/Interfaces/IAggregateEvent.cs diff --git a/src/CoreDomain.Tests/AggregateBaseTest.cs b/src/CoreDomain.Tests/AggregateBaseTest.cs new file mode 100644 index 0000000..6d25eaf --- /dev/null +++ b/src/CoreDomain.Tests/AggregateBaseTest.cs @@ -0,0 +1,41 @@ +using CoreDomain.Tests.Common; +using CoreDomain.Tests.Models; +using System; +using System.Linq; +using Xunit; + +namespace CoreDomain.Tests +{ + public class AggregateBaseTest + { + [Fact] + public void InstanceTest() + { + MyCustomAggregate customAggregate = Generator.GetAggregateInstace(); + + Assert.NotNull(customAggregate.ValueObject); + Assert.NotNull(customAggregate.Entity); + Assert.NotNull(customAggregate); + Assert.True(customAggregate.Id != Guid.Empty); + Assert.True(customAggregate.Version > -1); + Assert.NotNull(customAggregate.GetUncommittedEvents()); + Assert.True(customAggregate.GetUncommittedEvents().Any()); + } + + [Fact] + public void AddEventTest() + { + MyCustomAggregate customAggregate = Generator.GetAggregateInstace(); + + customAggregate.ApplyEvent(); + + Assert.NotNull(customAggregate.ValueObject); + Assert.NotNull(customAggregate.Entity); + Assert.NotNull(customAggregate); + Assert.True(customAggregate.Id != Guid.Empty); + Assert.True(customAggregate.Version > -1); + Assert.NotNull(customAggregate.GetUncommittedEvents()); + Assert.True(customAggregate.GetUncommittedEvents().Count() == 2); + } + } +} diff --git a/src/CoreDomain.Tests/Common/Generator.cs b/src/CoreDomain.Tests/Common/Generator.cs new file mode 100644 index 0000000..a8bea70 --- /dev/null +++ b/src/CoreDomain.Tests/Common/Generator.cs @@ -0,0 +1,58 @@ +using CoreDomain.Tests.Models; +using System; +using System.Linq; + +namespace CoreDomain.Tests.Common +{ + public static class Generator + { + public static int GetRandomInt() + { + return new Random().Next(); + } + + public static long GetRandomLong() + { + return new Random().Next() * new Random().Next(); + } + + public static string GetRandomString(int length) + { + Random random = new Random(); + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + } + + public static Guid GetRandomGuid() + { + return Guid.NewGuid(); + } + + public static MyCustomAggregate GetAggregateInstace() + { + MyCustomValueObject customValueObject = new MyCustomValueObject(GetRandomInt(), GetRandomString(10)); + MyCustomEntity customEntity = new MyCustomEntity(); + MyCustomAggregate customAggregate = new MyCustomAggregate(customEntity, customValueObject); + + return customAggregate; + } + } + + public static class Generator + { + static readonly Type type = typeof(T); + + public static MyCustomEntity GetCustomEntityInstance(params object[] args) + { + if (type.Equals(typeof(int)) + || type.Equals(typeof(long)) + || type.Equals(typeof(string)) + || type.Equals(typeof(Guid))) + return (MyCustomEntity)Activator.CreateInstance(typeof(MyCustomEntity), args); + + throw new InvalidCastException($"Type {typeof(T).Name} is not supported"); + } + } +} diff --git a/src/CoreDomain.Tests/DomainEventBaseTest.cs b/src/CoreDomain.Tests/DomainEventBaseTest.cs new file mode 100644 index 0000000..ac71445 --- /dev/null +++ b/src/CoreDomain.Tests/DomainEventBaseTest.cs @@ -0,0 +1,32 @@ +using CoreDomain.Tests.Common; +using CoreDomain.Tests.Models; +using System; +using Xunit; + +namespace CoreDomain.Tests +{ + public class DomainEventBaseTest + { + [Fact] + public void InstanceTest() + { + MyCustomEvent customEvent = new MyCustomEvent(Generator.GetRandomGuid(), 1); + + Assert.NotNull(customEvent); + Assert.True(customEvent.AggregateId != Guid.Empty); + Assert.True(customEvent.EventId != Guid.Empty); + Assert.True(customEvent.AggregateVersion > -1); + Assert.True(customEvent.CreatedAt != DateTime.MinValue); + } + + [Fact] + public void NonEqualityTest() + { + MyCustomEvent customEvent1 = new MyCustomEvent(Generator.GetRandomGuid(), 1); + MyCustomEvent customEvent2 = new MyCustomEvent(Generator.GetRandomGuid(), 1); + + Assert.False(customEvent1.Equals(customEvent2)); + Assert.NotEqual(customEvent1, customEvent2); + } + } +} diff --git a/src/CoreDomain.Tests/EntityTest.cs b/src/CoreDomain.Tests/EntityTest.cs index 295d4d0..974363d 100644 --- a/src/CoreDomain.Tests/EntityTest.cs +++ b/src/CoreDomain.Tests/EntityTest.cs @@ -1,49 +1,107 @@ +using CoreDomain.Tests.Common; +using System; using Xunit; -namespace CoreDomain.Tests +namespace CoreDomain.Tests.Models { public class EntityTest { [Fact] - public void EqualityTest() + public void EqualityTest_Int() { - MyIntClass myClass1 = new MyIntClass(); - MyIntClass myClass2 = new MyIntClass(); + int id = Generator.GetRandomInt(); - Assert.True(myClass1 == myClass2); - Assert.False(myClass1 != myClass2); - Assert.True(myClass1.Equals(myClass2)); - Assert.Equal(myClass1, myClass2); + MyCustomEntity myClass1 = Generator.GetCustomEntityInstance(id); + MyCustomEntity myClass2 = Generator.GetCustomEntityInstance(id); + + ValidateEquality(myClass1, myClass2); + } + + [Fact] + public void NonEqualityTest_Int() + { + MyCustomEntity myClass1 = Generator.GetCustomEntityInstance(Generator.GetRandomInt()); + MyCustomEntity myClass2 = Generator.GetCustomEntityInstance(Generator.GetRandomInt()); + + ValidateNonEquality(myClass1, myClass2); } [Fact] - public void EqualityTestWithId() + public void EqualityTest_Long() { - MyIntClass myClass1 = new MyIntClass(1); - MyIntClass myClass2 = new MyIntClass(1); + long id = Generator.GetRandomLong(); + + MyCustomEntity myClass1 = Generator.GetCustomEntityInstance(id); + MyCustomEntity myClass2 = Generator.GetCustomEntityInstance(id); + + ValidateEquality(myClass1, myClass2); + } + + [Fact] + public void NonEqualityTest_Long() + { + MyCustomEntity myClass1 = Generator.GetCustomEntityInstance(Generator.GetRandomLong()); + MyCustomEntity myClass2 = Generator.GetCustomEntityInstance(Generator.GetRandomLong()); + + ValidateNonEquality(myClass1, myClass2); + } + + [Fact] + public void EqualityTest_String() + { + string id = Generator.GetRandomString(20); + + MyCustomEntity myClass1 = Generator.GetCustomEntityInstance(id); + MyCustomEntity myClass2 = Generator.GetCustomEntityInstance(id); + + ValidateEquality(myClass1, myClass2); + } + [Fact] + public void NonEqualityTest_String() + { + MyCustomEntity myClass1 = Generator.GetCustomEntityInstance(Generator.GetRandomString(20)); + MyCustomEntity myClass2 = Generator.GetCustomEntityInstance(Generator.GetRandomString(20)); + + ValidateNonEquality(myClass1, myClass2); + } + + [Fact] + public void EqualityTest_Guid() + { + Guid id = Generator.GetRandomGuid(); + + MyCustomEntity myClass1 = Generator.GetCustomEntityInstance(id); + MyCustomEntity myClass2 = Generator.GetCustomEntityInstance(id); + + ValidateEquality(myClass1, myClass2); + } + + [Fact] + public void NonEqualityTest_Guid() + { + MyCustomEntity myClass1 = Generator.GetCustomEntityInstance(Generator.GetRandomGuid()); + MyCustomEntity myClass2 = Generator.GetCustomEntityInstance(Generator.GetRandomGuid()); + + ValidateNonEquality(myClass1, myClass2); + } + + private static void ValidateEquality(MyCustomEntity myClass1, MyCustomEntity myClass2) + { Assert.True(myClass1 == myClass2); Assert.False(myClass1 != myClass2); Assert.True(myClass1.Equals(myClass2)); Assert.Equal(myClass1, myClass2); + Assert.Equal(myClass1.Id, myClass2.Id); } - [Fact] - public void EqualityTestWithDifferentId() + private static void ValidateNonEquality(MyCustomEntity myClass1, MyCustomEntity myClass2) { - MyIntClass myClass1 = new MyIntClass(1); - MyIntClass myClass2 = new MyIntClass(2); - Assert.False(myClass1 == myClass2); Assert.True(myClass1 != myClass2); Assert.False(myClass1.Equals(myClass2)); Assert.NotEqual(myClass1, myClass2); + Assert.NotEqual(myClass1.Id, myClass2.Id); } } - public class MyIntClass : EntityBase - { - public MyIntClass() { } - - public MyIntClass(int id) => Id = id; - } } diff --git a/src/CoreDomain.Tests/Models/MyCustomAggregate.cs b/src/CoreDomain.Tests/Models/MyCustomAggregate.cs new file mode 100644 index 0000000..a902129 --- /dev/null +++ b/src/CoreDomain.Tests/Models/MyCustomAggregate.cs @@ -0,0 +1,24 @@ +using System; + +namespace CoreDomain.Tests.Models +{ + public class MyCustomAggregate : AggregateBase + { + public MyCustomEntity Entity { get; set; } + public MyCustomValueObject ValueObject { get; set; } + + public MyCustomAggregate(MyCustomEntity entity, MyCustomValueObject valueObject) + { + Id = Guid.NewGuid(); + Entity = entity ?? throw new ArgumentNullException(nameof(entity)); + ValueObject = valueObject ?? throw new ArgumentNullException(nameof(valueObject)); + + RaiseEvent(new MyCustomEvent(Id, Version)); + } + + public void ApplyEvent() + { + RaiseEvent(new MyCustomEvent()); + } + } +} diff --git a/src/CoreDomain.Tests/Models/MyCustomEntity.cs b/src/CoreDomain.Tests/Models/MyCustomEntity.cs new file mode 100644 index 0000000..602a468 --- /dev/null +++ b/src/CoreDomain.Tests/Models/MyCustomEntity.cs @@ -0,0 +1,9 @@ +namespace CoreDomain.Tests.Models +{ + public class MyCustomEntity : EntityBase + { + public MyCustomEntity() { } + + public MyCustomEntity(T id) => Id = id; + } +} diff --git a/src/CoreDomain.Tests/Models/MyCustomEvent.cs b/src/CoreDomain.Tests/Models/MyCustomEvent.cs new file mode 100644 index 0000000..ff63287 --- /dev/null +++ b/src/CoreDomain.Tests/Models/MyCustomEvent.cs @@ -0,0 +1,23 @@ +using CoreDomain.Interfaces; +using System; + +namespace CoreDomain.Tests.Models +{ + public class MyCustomEvent : DomainEventBase + { + public MyCustomEvent() : base() + { + + } + + public MyCustomEvent(Guid aggregateId, long version) : base(aggregateId, version) + { + + } + + public override IDomainEvent WithAggregate(Guid aggregateId, long aggregateVersion) + { + return new MyCustomEvent(aggregateId, aggregateVersion); + } + } +} diff --git a/src/CoreDomain.Tests/Models/MyCustomValueObject.cs b/src/CoreDomain.Tests/Models/MyCustomValueObject.cs new file mode 100644 index 0000000..464c303 --- /dev/null +++ b/src/CoreDomain.Tests/Models/MyCustomValueObject.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace CoreDomain.Tests.Models +{ + public class MyCustomValueObject : ValueObject + { + public MyCustomValueObject(int foo, string bar) + { + Foo = foo; + Bar = bar ?? throw new ArgumentNullException(nameof(bar)); + } + + public int Foo { get; set; } + + public string Bar { get; set; } + + protected override IEnumerable GetEqualityComponents() + { + yield return Foo; + yield return Bar; + } + } +} diff --git a/src/CoreDomain.Tests/ValueObjectTest.cs b/src/CoreDomain.Tests/ValueObjectTest.cs new file mode 100644 index 0000000..71f55b3 --- /dev/null +++ b/src/CoreDomain.Tests/ValueObjectTest.cs @@ -0,0 +1,33 @@ +using CoreDomain.Tests.Models; +using Xunit; + +namespace CoreDomain.Tests +{ + public class ValueObjectTest + { + [Theory] + [InlineData(1, "test")] + public void EqualityTest(int foo, string bar) + { + MyCustomValueObject valueObject1 = new MyCustomValueObject(foo, bar); + MyCustomValueObject valueObject2 = new MyCustomValueObject(foo, bar); + + Assert.True(valueObject1 == valueObject2); + Assert.False(valueObject1 != valueObject2); + Assert.True(valueObject1.Equals(valueObject2)); + Assert.Equal(valueObject1, valueObject2); + } + + [Fact] + public void NonEqualityTest() + { + MyCustomValueObject valueObject1 = new MyCustomValueObject(1, "test"); + MyCustomValueObject valueObject2 = new MyCustomValueObject(2, "test"); + + Assert.True(valueObject1 != valueObject2); + Assert.False(valueObject1 == valueObject2); + Assert.False(valueObject1.Equals(valueObject2)); + Assert.NotEqual(valueObject1, valueObject2); + } + } +} diff --git a/src/CoreDomain/Interfaces/IAggregateEvent.cs b/src/CoreDomain/Interfaces/IAggregateEvent.cs new file mode 100644 index 0000000..5d37c8f --- /dev/null +++ b/src/CoreDomain/Interfaces/IAggregateEvent.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace CoreDomain.Interfaces +{ + public interface IAggregateEvent + { + long Version { get; } + IReadOnlyCollection> UncommittedEvents { get; } + + IEnumerable> GetUncommittedEvents(); + void ClearUncommittedEvents(); + } +} diff --git a/src/CoreDomain/Interfaces/IDomainEvent.cs b/src/CoreDomain/Interfaces/IDomainEvent.cs index b092ca0..bedf749 100644 --- a/src/CoreDomain/Interfaces/IDomainEvent.cs +++ b/src/CoreDomain/Interfaces/IDomainEvent.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace CoreDomain.Interfaces { @@ -7,11 +6,7 @@ public interface IDomainEvent { Guid EventId { get; } TKey AggregateId { get; } - long Version { get; } - ICollection> UncommittedEvents { get; } - - void ApplyEvent(IDomainEvent @event, long version); - IEnumerable> GetUncommittedEvents(); - void ClearUncommittedEvents(); + long AggregateVersion { get; } + public DateTime CreatedAt { get; } } } diff --git a/src/CoreDomain/Models/AggregateBase.cs b/src/CoreDomain/Models/AggregateBase.cs index bc44d0f..3dc4f4f 100644 --- a/src/CoreDomain/Models/AggregateBase.cs +++ b/src/CoreDomain/Models/AggregateBase.cs @@ -1,11 +1,23 @@ using CoreDomain.Interfaces; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; namespace CoreDomain { - public abstract class AggregateBase : DomainEventBase, IAggregate + public abstract class AggregateBase : IAggregate, IAggregateEvent { public TKey Id { get; protected set; } + public const long NewAggregateVersion = 0; + public long Version { get; private set; } = NewAggregateVersion; + + private readonly ICollection> _uncommittedEvents = new Collection>(); + IReadOnlyCollection> IAggregateEvent.UncommittedEvents => _uncommittedEvents as IReadOnlyCollection>; + + public void ClearUncommittedEvents() => _uncommittedEvents.Clear(); + public IEnumerable> GetUncommittedEvents() => _uncommittedEvents.AsEnumerable(); + protected void RaiseEvent(TEvent @event) where TEvent : DomainEventBase { @@ -13,9 +25,11 @@ protected void RaiseEvent(TEvent @event) Equals(Id, default(TKey)) ? @event.AggregateId : Id, Version); - ((IDomainEvent)this).ApplyEvent(eventWithAggregate, Version + 1); - - UncommittedEvents.Add(@event); + if (!_uncommittedEvents.Any(x => Equals(x.EventId, @event.EventId))) + { + _uncommittedEvents.Add(@event); + Version++; + } } } } diff --git a/src/CoreDomain/Models/DomainEventBase.cs b/src/CoreDomain/Models/DomainEventBase.cs index 1ce7b26..c246010 100644 --- a/src/CoreDomain/Models/DomainEventBase.cs +++ b/src/CoreDomain/Models/DomainEventBase.cs @@ -1,22 +1,22 @@ using CoreDomain.Interfaces; using System; using System.Collections.Generic; -using System.Linq; namespace CoreDomain { public abstract class DomainEventBase : IDomainEvent, IEquatable> { - public const long NewVersion = -1; + public const long NewVersion = 0; public Guid EventId { get; } public TKey AggregateId { get; } - public long Version { get; private set; } = NewVersion; - public ICollection> UncommittedEvents { get; } + public long AggregateVersion { get; private set; } = NewVersion; + public DateTime CreatedAt { get; } protected DomainEventBase() { EventId = Guid.NewGuid(); + CreatedAt = DateTime.Now; } protected DomainEventBase(TKey aggregateId) : this() @@ -26,21 +26,9 @@ protected DomainEventBase(TKey aggregateId) : this() protected DomainEventBase(TKey aggregateId, long aggregateVersion) : this(aggregateId) { - Version = aggregateVersion; + AggregateVersion = aggregateVersion; } - public void ApplyEvent(IDomainEvent @event, long version) - { - if (!UncommittedEvents.Any(x => Equals(x.EventId, @event.EventId))) - { - ((dynamic)this).Apply((dynamic)@event); - Version = version; - } - } - - public void ClearUncommittedEvents() => UncommittedEvents.Clear(); - public IEnumerable> GetUncommittedEvents() => UncommittedEvents.AsEnumerable(); - public abstract IDomainEvent WithAggregate(TKey aggregateId, long aggregateVersion); public override bool Equals(object obj) diff --git a/src/CoreDomain/Models/ValueObject.cs b/src/CoreDomain/Models/ValueObject.cs index 369d8d8..eae328e 100644 --- a/src/CoreDomain/Models/ValueObject.cs +++ b/src/CoreDomain/Models/ValueObject.cs @@ -1,45 +1,63 @@ -namespace CoreDomain +using System.Collections.Generic; +using System.Linq; + +namespace CoreDomain { /// https://enterprisecraftsmanship.com/posts/value-object-better-implementation/ - public abstract class ValueObject - where T : ValueObject + /// https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/implement-value-objects + public abstract class ValueObject { public override bool Equals(object obj) { - var valueObject = obj as T; - - if (valueObject == null) + if (obj == null) return false; if (GetType() != obj.GetType()) return false; - return EqualsCore(valueObject); + var other = (ValueObject)obj; + + return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); } - protected abstract bool EqualsCore(T other); + /// + /// Needs to implement using a yield return statement to return each element one at a time + /// + /// + /// yield return Foo; + /// yield return Bar; + /// + /// + protected abstract IEnumerable GetEqualityComponents(); public override int GetHashCode() { - return GetHashCodeCore(); + return GetEqualityComponents() + .Select(x => x != null ? x.GetHashCode() : 0) + .Aggregate((x, y) => x ^ y); } - protected abstract int GetHashCodeCore(); + public static bool operator ==(ValueObject a, ValueObject b) + { + return EqualOperator(a, b); + } - public static bool operator ==(ValueObject a, ValueObject b) + public static bool operator !=(ValueObject a, ValueObject b) { - if (a is null && b is null) - return true; + return NotEqualOperator(a, b); + } - if (a is null || b is null) + protected static bool EqualOperator(ValueObject left, ValueObject right) + { + if (left is null ^ right is null) return false; - - return a.Equals(b); + + return left is null || left.Equals(right); } - public static bool operator !=(ValueObject a, ValueObject b) + protected static bool NotEqualOperator(ValueObject left, ValueObject right) { - return !(a == b); + return !EqualOperator(left, right); } } }