0

I have classes that contain other child classes, so I have implemented IEquatable<T> to perform a custom Equals method recursively down the chain. That is working fine, but I am thinking if other developers need to add new public properties to these classes, we would want to enforce that they also add them to the Equals method. I was wondering if there is an easy way to do this without reflection? Could I somehow leverage custom attributes? I'd basically like to add it to the unit test suite so that the build would fail.

  • Short of writing your own analyser (which would be iffy at best), no. My suggestion, is put a big (totally outrageous) comment in your code, and write unit tests – TheGeneral Mar 17 '20 at 00:37
  • Use a static constructor to generate an equals `Func` method? – Jeremy Lakeman Mar 17 '20 at 01:19
  • @MichaelRandall yeah I considered a comment of that nature, but it doesn't really enforce anything. Plus my team typically avoids comments whenever possible. But thanks for the response! – Djangoboots Mar 17 '20 at 02:14
  • @JeremyLakeman -- hmm this is an intriguing answer. I don't know what that might look like yet, but it opens up an avenue for investigation -- thank you! If you have time, can you expand on that idea a little more? – Djangoboots Mar 17 '20 at 02:14
  • There is no easy way to do this, static or otherwise, it would have to be compile static analysis, the reason why there is nothing is because the compiler cant guess what you want to be included in the check. The only option would be to write a code analyser, which it quite doable these days, yet in larger code bases would find false positives (yet might work for you). The way we tend to deal with these situations is comments, and unit tests unfortunately. on a side note, i nearly spat out my coffee when you said your team doesn't like to write comments, all i could image was the wild-west :) – TheGeneral Mar 17 '20 at 02:31
  • 1
    @MichaelRandall Hahah -- yeah I totally understand! It's not that we DON'T write comments (we definitely have xml docs on our public methods or more complicated logic), it's more that they are only used if appropriate naming and clear code still don't get the message across. But in this case, it may be the way to go. :) – Djangoboots Mar 17 '20 at 02:37
  • You could use the reflection this case. Basically you could find the properties in a class using refelction and equate the public ones value – Kalyan Mar 17 '20 at 03:23

1 Answers1

1

If you want to force that your equality methods are built in a standard way, perhaps you could compile them at runtime using reflection;

    private static Expression Equality(Type propType, MemberExpression thisProp, MemberExpression otherProp)
    {
        var equatable = typeof(IEquatable<>).MakeGenericType(propType);
        var equal = Expression.Equal(thisProp, otherProp);

        if (!equatable.IsAssignableFrom(propType))
            return equal;

        // a == b || (a!=null && a.Equals(b))
        return Expression.OrElse(
            equal, 
            Expression.AndAlso(
                Expression.NotEqual(thisProp, Expression.Constant(null, propType)),
                Expression.Call(thisProp, equatable.GetMethod("Equals"), otherProp)
            )
        );
    }

    private static Delegate GenerateEquatable(Type type)
    {
        var thisParm = Expression.Parameter(type, "a");
        var otherParm = Expression.Parameter(type, "b");
        return Expression.Lambda(
            type.GetProperties()
                .Where(prop => prop.CanRead)
                .Select(prop => Equality(
                    prop.PropertyType,
                    Expression.MakeMemberAccess(thisParm, prop),
                    Expression.MakeMemberAccess(otherParm, prop)))
                .Aggregate((a, b) => Expression.AndAlso(a, b)),
            thisParm, otherParm).Compile();
    }

    public static Func<T, T, bool> GenerateEquatable<T>() where T:IEquatable<T> =>
        (Func<T, T, bool>)GenerateEquatable(typeof(T));

    public class Foo : IEquatable<Foo>{
        private static Func<Foo, Foo, bool> _equals = GenerateEquatable<Foo>();
        public bool Equals([AllowNull] Foo other) => other != null && _equals(this, other);
    }

Jeremy Lakeman
  • 9,515
  • 25
  • 29