Object Equality in .NET Domain Models

I am looking for recommendations on best practices for implementing equality in a domain model. As I can see, there are three (3) types of equality:

  • Reference equality - this means that both objects are stored in the same physical memory space.

  • Equality of identity - this means that both objects have the same meaning. For example, two Order objects with the same order number represent the same object. This is especially important when storing values ​​in lists, hash tables, etc. and the object needs a unique identifier for the search.

  • Equality value - both objects have all properties equally.

By convention, .NET provides two (2) ways to check for equality: Equals and ==. So, how do we match the three types (3) with the two (2) methods?

Of course, I left Object.ReferenceEquals, which MS added in recognition of the fact that most people redefined Equals because relational equality was not their desired behavior. So maybe we can transfer the first type (?).

Given the behavior of GetHashCode and Equals in the context of a hash table, can we say that Equals should always ensure equality of identity? If so, how do we give callers the opportunity to check the value of Equality?

And, do not most developers assume that Equals and == will produce the same result? Since == checks reference equality, does this mean that we must also overload == when we override Equals?

Your thoughts?

UPDATE

I don’t know all the details, but they told me (in a personal conversation with a colleague) that WPF has strict requirements that data-related objects use reference equality for Equals or data binding does not work correctly.

In addition, looking at typical Assert classes, there is even more confusing semantics. AreEqual (a, b) usually uses the Equals method, which implies Identity or Value Equality, while AreSame (a, b) uses ReferenceEquals for reference equality.

+4
source share
3 answers

I thought I was offering my resume from the above posts, as well as external conversations as an answer, rather than confusing the topic by updating the original post. I will leave this topic open and let readers vote on which answer they consider the best before choosing.

Here are the key points I got from these discussions:

  • Entities, by their very definition, in domain models have an identity.

  • Aggregate roots (in accordance with certain definitions) that contain other objects; therefore, the population also has an identity.

  • Although entities are mutable, their identification should not be.

  • Microsoft manuals indicate that when using GetHashCode () for two objects, Equals should return true for these objects.

  • When storing an object in a hash table, GetHashCode should return a value that represents the identifier of this object.

  • Equality of identity does not mean referential equality or equivalence of value. Primary equality means equal equality. But, referential equality matters Identity and Value Equality.

In truth, I realized that this could just be a syntax / semantics issue. We need a third way to define equality. We have two:

Equally . In a domain model, two objects are equal when they have the same identifier. I believe that this should be so as to satisfy No. 4 and No. 5 above. We use an entity identifier to generate the hash code returned from GetHashCode, so the same values ​​should be used to determine equality.

Same . Based on existing usage (as part of debugging and testing), when two objects / entities are the same, they refer to the same instance (referential equality).

??? . How do we indicate Value Equality in the code?

In all my conversations, I have found that we use qualifiers to formulate these terms in some way; using names like "IdentityEquals" and "IsSameXYZ", so "Equals" means Value Equality or "IsEquivalentTo" and "ExactlyEquals" to mean Value Equality, so "Equals" means Identity Equality.

While I appreciate the flexibility, the more I go along this path, the more I understand that the two developers do not see it the same way. And that causes problems.

And I can tell you that every developer with whom I spoke, with one, indicated that they expect that "==" will behave exactly like Equals. However, Microsoft recommends that you do not overload "==" even if we override Equals. It would be nice if the core == operator just delegated Equals.

So, on the bottom line, I will redefine Equals to ensure Identity Equality, provide the SameAs method for reference equality (just a convenient wrapper on ReferenceEquals) and overload == in our base class to use Equals to make them consistent. Then I will use comparators to "compare" the values ​​of two "equal" objects.

More thoughts?

0
source

For reference equality, I use object.ReferenceEquals , as you said, although you can also just reference objects and compare them (as long as they are reference types).

With respect to 2 and 3, it really depends on what the developer wants if they want to define equality as identity or equality of value. I usually like it when my Equals () is equal to the value of equality, and then provides external mappings for equality of identity.

Most of the methods that compare elements give you the opportunity to go through a user mapping, and that is where I usually pass any custom comparisons mapper (like an identifier), but that's me.

And, as I said, this is my typical use, I also created object models where I consider only a subset of the properties to represent the identity, and the rest are not compared.

You can always create a very simple ProjectionComparer that accepts any type and creates a projection-based resolver, simplifies the transfer of user mappings for identification, etc. at the right point and leaves the Equals () method only for the value.

In addition, as a rule, I personally do not overload == unless I write a value type that needs typical comparison operators, because there is so much confusion with operator overloading and how overloads are not redefined.

But then again, this is just my opinion :-)

UPDATE Here is my projection comparator, you can find many other implementations, of course, but this one works well for me, it implements both EqualityComparer<TCompare> (supports bool Equals(T, T) and int GetHashCode(T) and IComparer<T> , which supports Compare(T, T) ):

 public sealed class ProjectionComparer<TCompare, TProjected> : EqualityComparer<TCompare>, IComparer<TCompare> { private readonly Func<TCompare, TProjected> _projection; // construct with the projection public ProjectionComparer(Func<TCompare, TProjected> projection) { if (projection == null) { throw new ArgumentNullException("projection"); } _projection = projection; } // Compares objects, if either object is null, use standard null rules // for compare, then compare projection of each if both not null. public int Compare(TCompare left, TCompare right) { // if both same object or both null, return zero automatically if (ReferenceEquals(left, right)) { return 0; } // can only happen if left null and right not null if (left == null) { return -1; } // can only happen if right null and left non-null if (right == null) { return 1; } // otherwise compare the projections return Comparer<TProjected>.Default.Compare(_projection(left), _projection(right)); } // Equals method that checks for null objects and then checks projection public override bool Equals(TCompare left, TCompare right) { // why bother to extract if they refer to same object... if (ReferenceEquals(left, right)) { return true; } // if either is null, no sense checking either (both are null is handled by ReferenceEquals()) if (left == null || right == null) { return false; } return Equals(_projection(left), _projection(right)); } // GetHashCode method that gets hash code of the projection result public override int GetHashCode(TCompare obj) { // unlike Equals, GetHashCode() should never be called on a null object if (obj == null) { throw new ArgumentNullException("obj"); } var key = _projection(obj); // I decided since obj is non-null, i'd return zero if key was null. return key == null ? 0 : key.GetHashCode(); } // Factory method to generate the comparer for the projection using type public static ProjectionComparer<TCompare, TProjected> Create<TCompare, TProjected>(Func<TCompare, TProjected> projection) { return new ProjectionComparer<TCompare, TProjected>(projection); } } 

This will allow you to do things like:

 List<Employee> emp = ...; // sort by ID emp.Sort(ProjectionComparer.Create((Employee e) => e.ID)); // sort by name emp.Sort(ProjectionComparer.Create((Employee e) => e.Name)); 
+1
source

As usual, I design my domain models, around == and ReferenceEquals() , reference equality is executed. And Equals() fulfilling equality of value. The reason I don't use any of them for identity equality is three times:

Not everything has an identifier , so there may be confusion about how Equals () and == actually work when an object is used without identification. Think, for example, of a cache containing multiple objects, or temporary / auxiliary objects. What about aggregated objects that can be based on several different domain objects? What identity could he compare?

Equality of identity is a subset of the meaning of equality , from my experience when identity equality is involved, equality of equality is not much behind, and usually the value of identity also includes equality of identity. After all, if the identities do not match, are the meanings really the same?

What does equality of identity really mean, say , ask yourself this question: "What does equality of identity mean without context?" Is a user with Id 1 equal to a comment with Id 1? Of course, I hope that this is not so, since both entities are very different.

So, why use any of the built-in equality methods ( == and Equals() ) for something that is an exception, not a rule? Instead, I tend to implement a base class that provides my identity and implements identity equality, depending on how general identity equality is in my current domain.

For instance; in an area where identity equality is very unusual, I would create a custom EqualityComparer<T> to make identity equality when and where necessary, taking into account the context if identity equality is not a common problem in my current domain.

However, in an area where identity equality is very common, I would choose a method in my base identifier class IdentityEquals() that takes care of identity equality at a basic level.

Thus, I disclose only equality of identity, where it is relevant and logical. Without any potential confusion as to how any of my equality checks can work. Be it Equals() , == , or IdentityEquals / EqualityComparer<T> (depending on how common an identity identity is within my domain).

Also, as a note, I recommend reading Microsoft's recommendations for overloading equality .

In particular:

By default, the == operator checks the reference equality to determine if two references point to the same object, so link types do not need to implement the == operator to get this functionality. When the type is immutable, which means the data contained in the instance cannot be changed, overloading the == operator to compare the value of equality instead of reference equality can be useful because, since immutable objects, they can be considered the same as long as they have such same value. The override operator == in immutable types is not recommended.

EDIT:

Regarding Assert.AreEqual and Assert.AreSame your domain defines what equality means; be it reference, personality or value. Thus, your Equals definition in your domain also extends to the Assert.AreEqual definition. If you say that Equals checks for equality of identity, then the logical extension Assert.AreEqual checks for equality of identity.

Assert.AreSame checks if both objects are the same object. Identical and equal are two different concepts. The only way to check if the object referenced by A is the same as the object referenced by B is a reference equality. Semantically and syntactically, both names make sense.

+1
source

Source: https://habr.com/ru/post/1379366/


All Articles