As a .NET developer, you're likely familiar with the Equals method in .NET. It's used to determine whether two instances are equivalent. Alongside this, there's GetHashCode(), which generates a hash code often used for dictionary keys.
While the default .NET implementation is generally reasonable and works in most scenarios, there are cases where implementing the IEquatable<T> interface can be beneficial. However, it involves writing a lot of boilerplate code. In this series of articles, we will explore how to write an aspect that automatically generates this boilerplate during compilation.
As is often the case, equality comparison seems like a simple problem on the surface, but it's an intriguing rabbit hole to explore when it comes to customization and optimization. We'll start simple and add complexity progressively.
This series contains the following articles:
| Article | Description |
|---|---|
| Why and when to use a custom equality contract? | This article discusses when to depart from the default .NET equality implementations. |
| Step 1: Getting started - Basic implementation | This article walks you through a basic implementation including all fields and properties in the comparison, while omitting type inheritance. |
| Step 2: Supporting type inheritance | This article adds type inheritance to the mix. |
| Step 3: Hand-picking equality members | This article describes how to add an API so users can hand-pick fields and properties that must be part of the comparison. |
| Step 4: Customization and optimizations | This article shows how to let the user choose a different IEqualityComparer instance and how to optimize the member evaluation order. |
At the end of this series, you'll be able to create aspects that generate code like the following examples:
1namespace Metalama.Samples.Comparison4;
2
3public partial class Entity
4{
5 [StringEqualityMember( StringComparison.InvariantCultureIgnoreCase )]
6 public required string EntityType { get; init; }
7
8 [EqualityMember]
9 public int Id { get; init; }
10
11 public Guid ObjectId { get; } = Guid.NewGuid();
12}
1using System;
2using System.Collections.Generic;
3
4namespace Metalama.Samples.Comparison4;
5
6public partial class Entity
7: IEquatable<Entity>
8{
9 [StringEqualityMember( StringComparison.InvariantCultureIgnoreCase )]
10 public required string EntityType { get; init; }
11
12 [EqualityMember]
13 public int Id { get; init; }
14
15 public Guid ObjectId { get; } = Guid.NewGuid();
16public virtual bool Equals(Entity other)
17 {
18 if (other == null)
19 {
20 return (bool)false;
21 }
22
23 if (object.ReferenceEquals(this, other))
24 {
25 return (bool)true;
26 }
27
28 if (!EqualityComparer<int>.Default.Equals(this.Id, other.Id))
29 {
30 return (bool)false;
31 }
32
33 if (!StringComparer.InvariantCultureIgnoreCase.Equals(this.EntityType, other.EntityType))
34 {
35 return (bool)false;
36 }
37
38 return true;
39 }
40 public override bool Equals(object? other)
41 {
42 if (object.ReferenceEquals(this, other))
43 {
44 return (bool)true;
45 }
46
47 return other is Entity typed && Equals(typed);
48 }
49 public override int GetHashCode()
50 {
51 var hashCode = default(HashCode);
52 hashCode.Add(this.Id, EqualityComparer<int>.Default);
53 hashCode.Add(this.EntityType, StringComparer.InvariantCultureIgnoreCase);
54 return hashCode.ToHashCode();
55 }
56 public static bool operator ==(Entity a, Entity b)
57 {
58 return a == null && b == null || a != null && a.Equals(b);
59 }
60
61 public static bool operator !=(Entity a, Entity b)
62 {
63 return a == null ^ b == null || a != null && !a.Equals(b);
64 }
65}
1namespace Metalama.Samples.Comparison4;
2
3public class VersionedEntity : Entity
4{
5 [EqualityMember]
6 public int Version { get; init; }
7}
1using System;
2using System.Collections.Generic;
3
4namespace Metalama.Samples.Comparison4;
5
6public class VersionedEntity :Entity
7,IEquatable<VersionedEntity>
8{
9 [EqualityMember]
10 public int Version { get; init; }
11public virtual bool Equals(VersionedEntity other)
12 {
13 if (!base.Equals((Entity)other))
14 {
15 return (bool)false;
16 }
17
18 if (!EqualityComparer<int>.Default.Equals(this.Version, other.Version))
19 {
20 return (bool)false;
21 }
22
23 return true;
24 }
25 public override bool Equals(object? other)
26 {
27 if (object.ReferenceEquals(this, other))
28 {
29 return (bool)true;
30 }
31
32 return other is VersionedEntity typed && Equals(typed);
33 }
34 public override sealed bool Equals(Entity other)
35 {
36 if (object.ReferenceEquals(this, other))
37 {
38 return (bool)true;
39 }
40
41 return other is VersionedEntity typed && Equals(typed);
42 }
43 public override int GetHashCode()
44 {
45 var hashCode = default(HashCode);
46 hashCode.Add(base.GetHashCode());
47 hashCode.Add(this.Version, EqualityComparer<int>.Default);
48 return hashCode.ToHashCode();
49 }
50 public static bool operator ==(VersionedEntity a, VersionedEntity b)
51 {
52 return a == null && b == null || a != null && a.Equals(b);
53 }
54
55 public static bool operator !=(VersionedEntity a, VersionedEntity b)
56 {
57 return a == null ^ b == null || a != null && !a.Equals(b);
58 }
59}
1namespace Metalama.Samples.Comparison4;
2
3public sealed partial class Person
4{
5 [StringEqualityMember( StringComparison.InvariantCultureIgnoreCase, true )]
6 public string? Name { get; init; }
7
8 [DateEqualityMember]
9 public DateTime DateOfBirth { get; init; }
10}
1using System;
2
3namespace Metalama.Samples.Comparison4;
4
5public sealed partial class Person
6: IEquatable<Person>
7{
8 [StringEqualityMember( StringComparison.InvariantCultureIgnoreCase, true )]
9 public string? Name { get; init; }
10
11 [DateEqualityMember]
12 public DateTime DateOfBirth { get; init; }
13public bool Equals(Person other)
14 {
15 if (other == null)
16 {
17 return (bool)false;
18 }
19
20 if (object.ReferenceEquals(this, other))
21 {
22 return (bool)true;
23 }
24
25 if (!DateComparer.Instance.Equals(this.DateOfBirth, other.DateOfBirth))
26 {
27 return (bool)false;
28 }
29
30 if (!TrimmingStringEqualityComparer.InvariantCultureIgnoreCase.Equals(this.Name, other.Name))
31 {
32 return (bool)false;
33 }
34
35 return true;
36 }
37 public override bool Equals(object? other)
38 {
39 if (object.ReferenceEquals(this, other))
40 {
41 return (bool)true;
42 }
43
44 return other is Person typed && Equals(typed);
45 }
46 public override int GetHashCode()
47 {
48 var hashCode = default(HashCode);
49 hashCode.Add(this.DateOfBirth, DateComparer.Instance);
50 hashCode.Add(this.Name, TrimmingStringEqualityComparer.InvariantCultureIgnoreCase);
51 return hashCode.ToHashCode();
52 }
53 public static bool operator ==(Person a, Person b)
54 {
55 return a == null && b == null || a != null && a.Equals(b);
56 }
57
58 public static bool operator !=(Person a, Person b)
59 {
60 return a == null ^ b == null || a != null && !a.Equals(b);
61 }
62}