Skip to content

Commit e2891d1

Browse files
committed
Entity + Aggregate Root equality
1 parent ae72584 commit e2891d1

File tree

11 files changed

+552
-15
lines changed

11 files changed

+552
-15
lines changed

Src/Xer.DomainDriven/AggregateRoot.cs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Xer.DomainDriven
66
{
7-
public abstract class AggregateRoot : Entity, IAggregateRoot
7+
public abstract class AggregateRoot : Entity, IAggregateRoot, IEquatable<IAggregateRoot>
88
{
99
#region Declarations
1010

@@ -60,6 +60,67 @@ void IAggregateRoot.MarkDomainEventsAsCommitted()
6060

6161
#endregion IAggregateRoot implementation
6262

63+
#region Methods
64+
65+
/// <summary>
66+
/// Determine if object is equal by identity.
67+
/// </summary>
68+
/// <param name="other">Other object.</param>
69+
/// <returns>True, if aggregate roots are equal by identity. Otherwise, false.</returns>
70+
public override bool Equals(object other)
71+
{
72+
return Equals(other as IAggregateRoot);
73+
}
74+
75+
/// <summary>
76+
/// Determine if aggregate root is equal by identity.
77+
/// </summary>
78+
/// <param name="other">Other aggregate root.</param>
79+
/// <returns>True, if aggregate roots are equal by identity. Otherwise, false.</returns>
80+
public virtual bool Equals(IAggregateRoot other)
81+
{
82+
return base.Equals(other);
83+
}
84+
85+
/// <summary>
86+
/// Equality operator.
87+
/// </summary>
88+
/// <param name="aggregateRoot1">First aggregate root.</param>
89+
/// <param name="aggregateRoot2">Second aggregate root.</param>
90+
/// <returns>True, if aggregate roots are equal by identity. Otherwise, false.</returns>
91+
public static bool operator ==(AggregateRoot aggregateRoot1, AggregateRoot aggregateRoot2)
92+
{
93+
if (ReferenceEquals(aggregateRoot1, null) && ReferenceEquals(aggregateRoot2, null))
94+
return true;
95+
96+
if (!ReferenceEquals(aggregateRoot1, null))
97+
return aggregateRoot1.Equals(aggregateRoot2);
98+
99+
return false;
100+
}
101+
102+
/// <summary>
103+
/// Ineuality operator.
104+
/// </summary>
105+
/// <param name="aggregateRoot1">First aggregate root.</param>
106+
/// <param name="aggregateRoot2">Second aggregate root.</param>
107+
/// <returns>True, if aggregate roots are not equal by identity. Otherwise, false.</returns>
108+
public static bool operator !=(AggregateRoot aggregateRoot1, AggregateRoot aggregateRoot2)
109+
{
110+
return !(aggregateRoot1 == aggregateRoot2);
111+
}
112+
113+
/// <summary>
114+
/// Generate hash code from ID.
115+
/// </summary>
116+
/// <returns>Hash code generated from ID.</returns>
117+
public override int GetHashCode()
118+
{
119+
return base.GetHashCode();
120+
}
121+
122+
#endregion Methods
123+
63124
#region Protected Methods
64125

65126
/// <summary>

Src/Xer.DomainDriven/Entity.cs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Xer.DomainDriven
44
{
5-
public abstract class Entity : IEntity
5+
public abstract class Entity : IEntity, IEquatable<IEntity>
66
{
77
/// <summary>
88
/// Unique ID.
@@ -45,5 +45,73 @@ public Entity(Guid entityId, DateTime created, DateTime updated)
4545
Created = created;
4646
Updated = updated;
4747
}
48+
49+
/// <summary>
50+
/// Determine if object is equal by identity.
51+
/// </summary>
52+
/// <param name="other">Other object.</param>
53+
/// <returns>True, if entities are equal by identity. Otherwise, false.</returns>
54+
public override bool Equals(object other)
55+
{
56+
return Equals(other as IEntity);
57+
}
58+
59+
/// <summary>
60+
/// Determine if entity is equal by identity.
61+
/// </summary>
62+
/// <param name="other">Other entity.</param>
63+
/// <returns>True, if entities are equal by identity. Otherwise, false.</returns>
64+
public virtual bool Equals(IEntity other)
65+
{
66+
if (ReferenceEquals(other, null))
67+
return false;
68+
69+
if (ReferenceEquals(this, other))
70+
return true;
71+
72+
if (this.GetType() != other.GetType())
73+
return false;
74+
75+
return Id == other.Id;
76+
}
77+
78+
/// <summary>
79+
/// Equality operator.
80+
/// </summary>
81+
/// <param name="entity1">First entity.</param>
82+
/// <param name="entity2">Second entity.</param>
83+
/// <returns>True, if entities are equal by identity. Otherwise, false.</returns>
84+
public static bool operator ==(Entity e1, Entity e2)
85+
{
86+
if (ReferenceEquals(e1, null) && ReferenceEquals(e2, null))
87+
return true;
88+
89+
if (!ReferenceEquals(e1, null))
90+
{
91+
return e1.Equals(e2);
92+
}
93+
94+
return false;
95+
}
96+
97+
/// <summary>
98+
/// Inequality operator.
99+
/// </summary>
100+
/// <param name="entity1">First entity.</param>
101+
/// <param name="entity2">Second entity.</param>
102+
/// <returns>True, if entities are not equal by identity. Otherwise, false.</returns>
103+
public static bool operator !=(Entity entity1, Entity entity2)
104+
{
105+
return !(entity1 == entity2);
106+
}
107+
108+
/// <summary>
109+
/// Generate hash code from ID.
110+
/// </summary>
111+
/// <returns>Hash code generated from ID.</returns>
112+
public override int GetHashCode()
113+
{
114+
return Id.GetHashCode();
115+
}
48116
}
49117
}

Src/Xer.DomainDriven/Repositories/InMemoryAggregateRootRepository.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,7 @@ public InMemoryAggregateRootRepository(bool throwIfAggregateRootIsNotFound)
6969
/// <returns>Asynchronous task.</returns>
7070
public Task SaveAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default(CancellationToken))
7171
{
72-
if (_aggregateRoots.Contains(aggregateRoot))
73-
{
74-
_aggregateRoots.Remove(aggregateRoot);
75-
}
76-
72+
_aggregateRoots.RemoveAll(a => a.Id == aggregateRoot.Id);
7773
_aggregateRoots.Add(aggregateRoot);
7874

7975
return CompletedTask;

Src/Xer.DomainDriven/Repositories/PublishingRepository.cs renamed to Src/Xer.DomainDriven/Repositories/PublishingAggregateRootRepository.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
namespace Xer.DomainDriven.Repositories
66
{
7-
public class PublishingRepository<TAggregateRoot> : IAggregateRootRepository<TAggregateRoot>
8-
where TAggregateRoot : IAggregateRoot
7+
public class PublishingAggregateRootRepository<TAggregateRoot> : IAggregateRootRepository<TAggregateRoot>
8+
where TAggregateRoot : IAggregateRoot
99
{
1010
private readonly IAggregateRootRepository<TAggregateRoot> _inner;
1111
private readonly IDomainEventPublisher _domainEventPublisher;
@@ -15,7 +15,7 @@ public class PublishingRepository<TAggregateRoot> : IAggregateRootRepository<TAg
1515
/// </summary>
1616
/// <param name="inner">Aggregate root repository to decorate.</param>
1717
/// <param name="domainEventPublisher">Domain event publisher.</param>
18-
public PublishingRepository(IAggregateRootRepository<TAggregateRoot> inner,
18+
public PublishingAggregateRootRepository(IAggregateRootRepository<TAggregateRoot> inner,
1919
IDomainEventPublisher domainEventPublisher)
2020
{
2121
_inner = inner;

Src/Xer.DomainDriven/ValueObject.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ public bool Equals(TSelf other)
4343
if (other == null)
4444
return false;
4545

46+
if (ReferenceEquals(this, other))
47+
return true;
48+
49+
if (this.GetType() != other.GetType())
50+
return false;
51+
4652
return ValueEquals(other);
4753
}
4854

@@ -59,7 +65,7 @@ public bool Equals(TSelf other)
5965
return true;
6066
}
6167

62-
if (!ReferenceEquals(obj1, null) && !ReferenceEquals(obj2, null))
68+
if (!ReferenceEquals(obj1, null))
6369
{
6470
return obj1.Equals(obj2);
6571
}

Tests/Xer.DomainDriven.Tests/AggregateRootTests.cs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using FluentAssertions;
34
using Xer.DomainDriven.Exceptions;
45
using Xer.DomainDriven.Tests.Entities;
@@ -8,6 +9,134 @@ namespace Xer.DomainDriven.Tests
89
{
910
public class AggregateRootTests
1011
{
12+
#region Equality
13+
14+
public class Equality
15+
{
16+
[Fact]
17+
public void EqualsShouldBeTrueIfSameId()
18+
{
19+
var id = Guid.NewGuid();
20+
var aggregateRoot1 = new TestAggregateRoot(id);
21+
var aggregateRoot2 = new TestAggregateRoot(id);
22+
23+
// Same ID, should be equal.
24+
aggregateRoot1.Equals(aggregateRoot2).Should().BeTrue();
25+
}
26+
27+
[Fact]
28+
public void EqualsShouldBeTrueIfSameReference()
29+
{
30+
var aggregateRoot1 = new TestAggregateRoot(Guid.NewGuid());
31+
var sameReference = aggregateRoot1;
32+
33+
// Same ID, should be equal.
34+
aggregateRoot1.Equals(sameReference).Should().BeTrue();
35+
}
36+
37+
[Fact]
38+
public void ObjectEqualsShouldBeTrueIfSameId()
39+
{
40+
var id = Guid.NewGuid();
41+
var aggregateRoot1 = new TestAggregateRoot(id);
42+
var aggregateRoot2 = new TestAggregateRoot(id);
43+
44+
// Same ID, should be equal.
45+
aggregateRoot1.Equals((object)aggregateRoot2).Should().BeTrue();
46+
}
47+
48+
[Fact]
49+
public void ObjectEqualsShouldBeTrueIfSameReference()
50+
{
51+
var aggregateRoot1 = new TestAggregateRoot(Guid.NewGuid());
52+
var sameReference = aggregateRoot1;
53+
54+
// Same ID, should be equal.
55+
aggregateRoot1.Equals((object)sameReference).Should().BeTrue();
56+
}
57+
58+
[Fact]
59+
public void EqualityOperatorShouldBeTrueIfSameId()
60+
{
61+
var id = Guid.NewGuid();
62+
var aggregateRoot1 = new TestAggregateRoot(id);
63+
var aggregateRoot2 = new TestAggregateRoot(id);
64+
65+
// Same ID, should be equal.
66+
(aggregateRoot1 == aggregateRoot2).Should().BeTrue();
67+
}
68+
69+
[Fact]
70+
public void EqualityOperatorShouldBeTrueIfSameReference()
71+
{
72+
var aggregateRoot1 = new TestAggregateRoot(Guid.NewGuid());
73+
var sameReference = aggregateRoot1;
74+
75+
// Same ID, should be equal.
76+
(aggregateRoot1 == sameReference).Should().BeTrue();
77+
}
78+
79+
[Fact]
80+
public void ShouldNotBeEqualIfSameIdButDifferentType()
81+
{
82+
var id = Guid.NewGuid();
83+
var aggregateRoot1 = new TestAggregateRoot(id);
84+
var aggregateRoot2 = new NoApplierAggregateRoot(id);
85+
86+
// Same ID, should be equal.
87+
aggregateRoot1.Should().NotBe(aggregateRoot2);
88+
}
89+
}
90+
91+
#endregion Equality
92+
93+
#region GetHashCodeMethod
94+
95+
public class GetHashCodeMethod
96+
{
97+
[Fact]
98+
public void ShouldReturnTheSameValueForSameInstance()
99+
{
100+
var aggregateRoot = new TestAggregateRoot(Guid.NewGuid());
101+
int hashCode1 = aggregateRoot.GetHashCode();
102+
int hashCode2 = aggregateRoot.GetHashCode();
103+
104+
hashCode1.Should().Be(hashCode2);
105+
}
106+
107+
[Fact]
108+
public void ShouldBeSearcheableInHashSet()
109+
{
110+
var id = Guid.NewGuid();
111+
var aggregateRoot1 = new TestAggregateRoot(id);
112+
var aggregateRoot2 = new TestAggregateRoot(id);
113+
114+
var hashSet = new HashSet<TestAggregateRoot>();
115+
hashSet.Add(aggregateRoot1);
116+
117+
// Should be searcheable because aggregate roots are equal by ID.
118+
hashSet.Contains(aggregateRoot1).Should().BeTrue();
119+
hashSet.Contains(aggregateRoot2).Should().BeTrue();
120+
}
121+
122+
[Fact]
123+
public void ShouldBeSearcheableInDictionary()
124+
{
125+
var id = Guid.NewGuid();
126+
var aggregateRoot1 = new TestAggregateRoot(id);
127+
var aggregateRoot2 = new TestAggregateRoot(id);
128+
129+
var dictionary = new Dictionary<TestAggregateRoot, TestAggregateRoot>(1);
130+
dictionary[aggregateRoot1] = aggregateRoot1;
131+
132+
// Should be searcheable because aggregate roots are equal by ID.
133+
dictionary[aggregateRoot1].Should().Be(aggregateRoot1);
134+
dictionary[aggregateRoot2].Should().Be(aggregateRoot1);
135+
}
136+
}
137+
138+
#endregion GetHashCodeMethod
139+
11140
#region ApplyDomainEventMethod
12141

13142
public class ApplyDomainEventMethod
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
3+
namespace Xer.DomainDriven.Tests.Entities
4+
{
5+
public class TestEntity : Entity
6+
{
7+
public TestEntity(Guid entityId) : base(entityId)
8+
{
9+
}
10+
11+
public TestEntity(Guid entityId, DateTime created, DateTime updated) : base(entityId, created, updated)
12+
{
13+
}
14+
}
15+
16+
public class TestEntitySecond : Entity
17+
{
18+
public TestEntitySecond(Guid entityId) : base(entityId)
19+
{
20+
}
21+
22+
public TestEntitySecond(Guid entityId, DateTime created, DateTime updated) : base(entityId, created, updated)
23+
{
24+
}
25+
}
26+
}

Tests/Xer.DomainDriven.Tests/Entities/TestVaueObject.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,12 @@ protected override HashCode GenerateHashCode()
2222
return HashCode.From(Number, Data);
2323
}
2424
}
25+
26+
public class TestValueObjectSecond : TestValueObject
27+
{
28+
public TestValueObjectSecond(string data, int number)
29+
: base(data, number)
30+
{
31+
}
32+
}
2533
}

0 commit comments

Comments
 (0)