7

I have a class WorkflowItemSearchCriteria whose instances represent a set of values and references that will be used to return a filtered result set of workflow items based on the given search criteria.

class WorkflowItemSearchCriteria
{
    string ID { get; set; }
    Client TargetClient { get; set; }
    Person Supervisor { get; set; }
    Person WorkingAttorney { get; set; }
    SearchDateKind? TargetSearchDateKind { get; set; }
    DateTime? FromDate { get; set; }
    DateTime? ToDate { get; set; }

    public override bool Equals(object obj)
    {
        if (!(obj is WorkflowItemSearchCriteria)) return false;
        var target = (WorkflowItemSearchCriteria) obj;
        if (!target.ID.Equals(this.ID)) return false;
        if (target.TargetClient != this.TargetClient) return false;
        if (target.Supervisor != this.Supervisor) return false;
        if (target.WorkingAttorney != this.WorkingAttorney) return false;
        if (!target.TargetSearchDateKind.Equals(this.TargetSearchDateKind)) return false;
        if (!target.FromDate.Equals(this.FromDate)) return false;
        if (!target.ToDate.Equals(this.ToDate)) return false;
        return true;
    }

    public override int GetHashCode()
    {
        // According to MSDN, two objects that are equal must return the same hash code,
        // but the same hash code does not mean two objects are equal.  If two value 
        // objects are equal, then all property and field values must be equal,
        // so it's sufficient to return the hash code from just ONE field or property
        // because it will equal the hash code from another object that is equal to it.
        return ID.GetHashCode();
    }

    enum SearchDateKind
    {
        DateCreated,
        DateCompleted
    }
}

At some point, I want to compare two instances of this class for value-wise equality, so I'm overriding Equals as shown. I've been reading sources such as this to help me along, but I still feel that doing it the way I have results in brittle code because future developers would have to remember to update the Equals overload if there were ever properties added to WorkflowItemSearchCriteria.

Is there a better approach to this?

  • And future programmers would also have to make sure not to update Equals if there were ever a property added to WorkflowItemSearchCriteria which does should not impact equality. – Brian Dec 29 '15 at 15:56
  • 1
    There are some workarounds, but the simplest solution is to separate the properties into two sections in the code, those that affect equality and those that don't. Add appropriate comments. – user949300 Dec 29 '15 at 17:07
  • 1
    In a better universe, there'd be a way to mark fields that matter for equality (e.g. with annotations) and generate the comparison code automatically. To avoid the issue altogether, though, one can make the class sealed and force the future extensions use composition, not inheritance. In fact, the current class can be probably separated into several logical pieces, each with its own trivial Equals(); this would simplify implementing a fluid interface to construct this big object. – 9000 Dec 29 '15 at 18:37
  • @9000 That "better universe" may eventually be record types in C#7 – Ben Aaronson Dec 29 '15 at 21:35
  • @9000 Well, you could do that, using attributes. Say, [MattersForEquality]. And then use reflection to scan the type for members with this attribute and act accordingly. – GregRos Jan 4 '16 at 19:17
2

One approach to this problem is to add a unit test which checks for proper equality. The unit test could use reflection to enumerate every property, constructing objects with only one unequal property and verifying that equality handles it properly.

A tool like Microsoft Pex can go a step further, exhaustively (algorithmically, not via brute force) checking such equality. However, resolving this problem with Pex is probably both difficult and overkill.

An alternative approach is to use some sort of static analysis tool which solves this problem for you, but I'm unsure if any reasonable tooling exists for this scenario.

1

I'm a little lacking on sleep at the moment, so I'm going to give a rough answer.

You can annotate equality-relevant members with a custom attribute of your own, such as [EqualityMember], and in the Equals method you can perform the comparisons using reflection. You would have to scan the type for fields/other members decorated with the attribute using GetCustomAttribute<EqualityMember>() != null.

You could even go crazier, and have the attribute specify how to approach the equality. What kind of comparison to use. Attributes are really neat.

Since type members aren't going to change, you can cache a lot of the reflection work and reuse it. I doubt performance will be a problem, but you could even construct an expression tree and compile it, though that sounds like a headache.

I've just realized, I've actually done this before. But I don't remember when or why...

0

You can also include Fody Equals, which injects the Equals logic (including IEquatable<T> and overloading operators == and !=) at compile time.

You only need to add the Equals attribute:

[Equals]
class WorkflowItemSearchCriteria
{
    //...
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.