diff --git a/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/Change.cs b/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/Change.cs index a061853cd..27002a9f9 100644 --- a/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/Change.cs +++ b/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/Change.cs @@ -1,6 +1,8 @@ -namespace FilterLists.Directory.Domain.Aggregates.Changes; +using FilterLists.SharedKernel.Domain.SeedWork; -public abstract class Change +namespace FilterLists.Directory.Domain.Aggregates.Changes; + +public abstract class Change : Entity { public string? Reason { get; protected init; } public DateTime SubmittedAt { get; protected init; } = DateTime.UtcNow; diff --git a/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterLists/FilterList.cs b/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterLists/FilterList.cs index aeb9cd221..95bf2bdb1 100644 --- a/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterLists/FilterList.cs +++ b/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterLists/FilterList.cs @@ -1,9 +1,10 @@ using FilterLists.Directory.Domain.Aggregates.Changes; using FilterLists.Directory.Domain.Aggregates.Licenses; +using FilterLists.SharedKernel.Domain.SeedWork; namespace FilterLists.Directory.Domain.Aggregates.FilterLists; -public class FilterList : IRequireChangeApproval +public class FilterList : Entity, IRequireChangeApproval { protected FilterList() { } diff --git a/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterLists/FilterListViewUrl.cs b/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterLists/FilterListViewUrl.cs index f82a90d43..718714880 100644 --- a/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterLists/FilterListViewUrl.cs +++ b/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterLists/FilterListViewUrl.cs @@ -1,6 +1,8 @@ -namespace FilterLists.Directory.Domain.Aggregates.FilterLists; +using FilterLists.SharedKernel.Domain.SeedWork; -public class FilterListViewUrl +namespace FilterLists.Directory.Domain.Aggregates.FilterLists; + +public class FilterListViewUrl : Entity { protected FilterListViewUrl() { } diff --git a/services/Directory/FilterLists.Directory.Domain/Aggregates/Licenses/License.cs b/services/Directory/FilterLists.Directory.Domain/Aggregates/Licenses/License.cs index fe375efce..0707c83f0 100644 --- a/services/Directory/FilterLists.Directory.Domain/Aggregates/Licenses/License.cs +++ b/services/Directory/FilterLists.Directory.Domain/Aggregates/Licenses/License.cs @@ -1,6 +1,8 @@ -namespace FilterLists.Directory.Domain.Aggregates.Licenses; +using FilterLists.SharedKernel.Domain.SeedWork; -public class License +namespace FilterLists.Directory.Domain.Aggregates.Licenses; + +public class License : Entity { protected License() { } diff --git a/services/SharedKernel/FilterLists.SharedKernel.Domain.SeedWork/Entity.cs b/services/SharedKernel/FilterLists.SharedKernel.Domain.SeedWork/Entity.cs new file mode 100644 index 000000000..be5d5c066 --- /dev/null +++ b/services/SharedKernel/FilterLists.SharedKernel.Domain.SeedWork/Entity.cs @@ -0,0 +1,73 @@ +namespace FilterLists.SharedKernel.Domain.SeedWork; + +/// +/// https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/CSharpFunctionalExtensions/Entity/Entity.cs +/// +public abstract class Entity +{ + public virtual TId Id { get; protected set; } = default!; + + protected Entity() + { + } + + protected Entity(TId id) + { + Id = id; + } + + public override bool Equals(object? obj) + { + if (obj is not Entity other) + return false; + + if (ReferenceEquals(this, other)) + return true; + + if (ValueObject.GetUnproxiedType(this) != ValueObject.GetUnproxiedType(other)) + return false; + + if (IsTransient() || other.IsTransient()) + return false; + + return Id is not null && Id.Equals(other.Id); + } + + private bool IsTransient() + { + return Id is null || Id.Equals(default(TId)); + } + + public static bool operator ==(Entity? a, Entity? b) + { + if (a is null && b is null) + return true; + + if (a is null || b is null) + return false; + + return a.Equals(b); + } + + public static bool operator !=(Entity? a, Entity? b) + { + return !(a == b); + } + + public override int GetHashCode() + { + return (ValueObject.GetUnproxiedType(this)?.ToString() + Id).GetHashCode(); + } +} + +public abstract class Entity : Entity +{ + protected Entity() + { + } + + protected Entity(long id) + : base(id) + { + } +} diff --git a/services/SharedKernel/FilterLists.SharedKernel.Domain.SeedWork/ValueObject.cs b/services/SharedKernel/FilterLists.SharedKernel.Domain.SeedWork/ValueObject.cs index 7f7a4c4b7..a94d98be2 100644 --- a/services/SharedKernel/FilterLists.SharedKernel.Domain.SeedWork/ValueObject.cs +++ b/services/SharedKernel/FilterLists.SharedKernel.Domain.SeedWork/ValueObject.cs @@ -1,23 +1,22 @@ namespace FilterLists.SharedKernel.Domain.SeedWork; /// -/// https://enterprisecraftsmanship.com/posts/value-object-better-implementation/ +/// https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/CSharpFunctionalExtensions/ValueObject/ValueObject.cs /// -public abstract class ValueObject +[Serializable] +public abstract class ValueObject : IComparable, IComparable { - protected abstract IEnumerable GetEqualityComponents(); + private int? _cachedHashCode; + + protected abstract IEnumerable GetEqualityComponents(); public override bool Equals(object? obj) { if (obj == null) - { return false; - } - if (GetType() != obj.GetType()) - { + if (GetUnproxiedType(this) != GetUnproxiedType(obj)) return false; - } var valueObject = (ValueObject)obj; @@ -26,27 +25,66 @@ public override bool Equals(object? obj) public override int GetHashCode() { - return GetEqualityComponents() - .Aggregate(1, (current, obj) => - { - unchecked + _cachedHashCode ??= GetEqualityComponents() + .Aggregate( + 1, + (current, obj) => { - return (current * 23) + (obj?.GetHashCode() ?? 0); - } - }); + unchecked + { + return current * 23 + (obj?.GetHashCode() ?? 0); + } + }); + + return _cachedHashCode.Value; + } + + public virtual int CompareTo(object? obj) + { + var thisType = GetUnproxiedType(this); + var otherType = GetUnproxiedType(obj); + + if (thisType != otherType) + return string.CompareOrdinal(thisType?.ToString(), otherType?.ToString()); + + var other = (ValueObject?)obj; + + var components = GetEqualityComponents().ToArray(); + var otherComponents = other?.GetEqualityComponents().ToArray() ?? Array.Empty(); + + return components.Select((t, i) => CompareComponents(t, otherComponents[i])) + .FirstOrDefault(comparison => comparison != 0); + } + + private static int CompareComponents(object? object1, object? object2) + { + if (object1 is null && object2 is null) + return 0; + + if (object1 is null) + return -1; + + if (object2 is null) + return 1; + + if (object1 is IComparable comparable1 && object2 is IComparable comparable2) + return comparable1.CompareTo(comparable2); + + return object1.Equals(object2) ? 0 : -1; + } + + public virtual int CompareTo(ValueObject? other) + { + return CompareTo(other as object); } public static bool operator ==(ValueObject? a, ValueObject? b) { - if (ReferenceEquals(a, null) && ReferenceEquals(b, null)) - { + if (a is null && b is null) return true; - } - if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) - { + if (a is null || b is null) return false; - } return a.Equals(b); } @@ -55,4 +93,18 @@ public override int GetHashCode() { return !(a == b); } + + internal static Type? GetUnproxiedType(object? obj) + { + const string EFCoreProxyPrefix = "Castle.Proxies."; + const string NHibernateProxyPostfix = "Proxy"; + + var type = obj?.GetType(); + string? typeString = type?.ToString(); + + if (typeString is not null && (typeString.Contains(EFCoreProxyPrefix) || typeString.EndsWith(NHibernateProxyPostfix))) + return type?.BaseType; + + return type; + } }