Dynamic equality in C#

Viewed 166

Let's say that I have the following class:

public class TsvDataModel : IEquatable<TsvDataModel>
{
    public int ElementId { get; set; }

    public string Location { get; set; }
    public string RoomKey { get; set; 

    public decimal Area { get; set; }

    public bool Equals(TsvDataModel other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return ElementUid == other.ElementUid && Location == other.Location && RoomKey == other.RoomKey &&
               SubType == other.SubType;
    }

    // current equality comparison implementation
    public override bool Equals(object obj) => ReferenceEquals(this, obj) || (obj is TsvDataModel other && Equals(other));

    public override int GetHashCode() => HashCode.Combine(ElementUid, Location, RoomKey, SubType);
}

What I want is to create dynamic equality based on user-provided properties i.e. sometimes I want to compare only by ElementIds and sometimes to include/exclude other properties. I don't want to create all possible combinations. I tried using reflection to get properties via dynamic type and I can get value and type, but comparison is not working.

I'd prefer to pass in the properties as a list of property names (strings.)

I would like to use either IEqualityComparer or IEquitable (I'm using this object as HashSet<T>).

1 Answers

Here's a DynamicEqualityComparer:

public class DynamicEqualityComparer<T> : IEqualityComparer<T>
{
    private readonly IEnumerable<Func<T, object>> _propertySelectors;

    public DynamicEqualityComparer(params Func<T, object>[] propertySelectors)
    {
        _propertySelectors = propertySelectors;
    }

    public bool Equals(T x, T y)
    {
        return _propertySelectors.All(propertySelector =>
            object.Equals(propertySelector(x), propertySelector(y)));

    }

    public int GetHashCode(T obj)
    {
        var hashCode = new HashCode();
        foreach (var propertySelector in _propertySelectors)
        {
            hashCode.Add(propertySelector((T)obj));
        }

        return hashCode.ToHashCode();
    }

    public static DynamicEqualityComparer<T> FromPropertyNames(IEnumerable<string> propertyNames)
    {
        IEnumerable<Func<T, object>> selectors = propertyNames.Select(propertyName =>
        {
            var property = typeof(T).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
            if (property == null)
            {
                throw new Exception($"Unable to find a property named {propertyName} in type {typeof(T)}");
            }
            return new Func<T, object>(obj => property.GetValue(obj));
        });
        return new DynamicEqualityComparer<T>(selectors.ToArray());
    }
}

Usage:

var comparer = new DynamicEqualityComparer<TsvDataModel>(
    model => model.Location, 
    model => model.RoomKey,
    model => model.Area);

var hashset = new HashSet<TsvDataModel>(comparer);

or

var comparer = DynamicEqualityComparer<TsvDataModel>
    .FromPropertyNames(new[] {"Location", "RoomKey", "Area"});

var hashset = new HashSet<TsvDataModel>(comparer);

And for good measure, here are a few unit tests:

[Fact]
public void Recognizes_Equal_Models()
{
    var modelA = new TsvDataModel { ElementId = 2, Location = "x", RoomKey = "y", Area = 1.1m };
    var modelB = new TsvDataModel { ElementId = 3, Location = "x", RoomKey = "y", Area = 1.1m };

    var comparer = DynamicEqualityComparer<TsvDataModel>
        .FromPropertyNames(new[] { "Location", "RoomKey", "Area" });

    var hashset = new HashSet<TsvDataModel>(comparer);
    hashset.Add(modelA);
    hashset.Add(modelB);

    // ElementId is ignored. Location, RoomKey, and Area are the same.
    // So the comparer indicates that they are "equal."
    Assert.Equal(1, hashset.Count);
}

[Fact]
public void Recognizes_Unequal_Models()
{
    var modelA = new TsvDataModel { ElementId = 2, Location = "xy", RoomKey = "y", Area = 1.1m };
    var modelB = new TsvDataModel { ElementId = 3, Location = "x", RoomKey = "y", Area = 1.1m };

    var comparer = DynamicEqualityComparer<TsvDataModel>
        .FromPropertyNames(new[] { "Location", "RoomKey", "Area" });

    var hashset = new HashSet<TsvDataModel>(comparer);
    hashset.Add(modelA);
    hashset.Add(modelB);

    // ElementId is ignored. RoomKey and Area are the same but Location is different.
    // So the comparer indicates that they are not "equal."
    Assert.Equal(2, hashset.Count);
}
Related