Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing IEqualityComparer<T> for comparing arbitrary properties of any class (including anonymous)

I am writing this neat class implementing IEqualityComparer, so that I can just pass any anonymous type to it (or actually any type with properties) and it will automatically compare the types by comparing the property values of the types.

public class CompareProperty<T> : IEqualityComparer<T>
    {
        private Type type;
        private PropertyInfo propInfo;
        private string _fieldName;

        public string fieldName
        {
            get;
            set;
        }

        public CompareProperty(string fieldName)
        {
            this.fieldName = fieldName;
        }

        public bool Equals<T>(T x, T y)
        {
            if (this.type == null)
            {
                type = x.GetType();
                propInfo = type.GetProperty(fieldName);
            }
            object objX = propInfo.GetValue(x, null);
            object objY = propInfo.GetValue(y, null);
            return objX.ToString() == objY.ToString();
        }
    }

I thought this was a nice little helper function I could use many times.

In order to use this, I have to do:

var t = typeof(CompareProperty<>);
var g = t.MakeGenericType(infoType.GetType());
var c = g.GetConstructor(new Type[] {String.Empty.GetType()});
var obj = c.Invoke(new object[] {"somePropertyName"});

Fair enough, but what do I do with the obj variable it returns?

someEnumerable.Distinct(obj);

The overload of the distinct extension function does not accept this, because it does not see a IEqualityComparer type, it only sees an object, of course.

someEnumerable.Distinct((t) obj);
someEnumerable.Distinct(obj as t);

This also doesn't work. Type/Namespace not found (red underline).

How do I get this straight?

like image 987
cdbeelala89 Avatar asked Jul 05 '12 13:07

cdbeelala89


2 Answers

I'll first provide a solution for non-anonymous types and afterwards extend it to work for anonymous types as well. Hopefully, it will help you to understand what people were trying to say in comments to your question.

My "universal" IEqualityComparer<> looks like this:

public class PropertyComparer<T> : IEqualityComparer<T>
{
    private readonly PropertyInfo propertyToCompare;

    public PropertyComparer(string propertyName)
    {
        propertyToCompare = typeof(T).GetProperty(propertyName);
    }
    public bool Equals(T x, T y)
    {
        object xValue = propertyToCompare.GetValue(x, null);
        object yValue = propertyToCompare.GetValue(y, null);
        return xValue.Equals(yValue);
    }

    public int GetHashCode(T obj)
    {
        object objValue = propertyToCompare.GetValue(obj, null);
        return objValue.GetHashCode();
    }
}

Say that we want to use it with non-anonymous type first, like Person:

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string Surname { get; set; }
 }

The usage would be quite straightforward:

IEnumerable<Person> people = ... ; // some database call here
var distinctPeople = people.Distinct(new PropertyComparer<Person>("FirstName"));

As you can see, to use the PropertyComparer<T>, we need to specify the type (the T) instances of which are going to be compared against each other. What would the T be when dealing with anonymous types? Since they are generated at runtime, you cannot use the comparer by directly creating its instance, simply because you do not know the T at compile time. Instead, you need to use type-inference to let the C# compiler infer T from context on its own. Neat way to do this is to write the following extension method:

public static class LinqExtensions
{
    public static IEnumerable<T> WithDistinctProperty<T>(this IEnumerable<T> source, string propertyName)
    {
        return source.Distinct(new PropertyComparer<T>(propertyName));
    }
}

Now it will also work with anonymous types:

var distinctPeople = people
        .Select(x => new { x.FirstName, x.Surname })
        .WithDistinctProperty("FirstName");

All of a sudden, there is no need to specify the exact type the query is dealing with anywhere, since C# compiler is smart enough to infer it from the context (which, in this case, is being provided from the type of source parameter in the extension method).

Hope this will help you.

like image 195
Nikola Anusev Avatar answered Oct 20 '22 04:10

Nikola Anusev


Just add own validations.

class PropertyComparer<T, TResult> : IEqualityComparer<T>
{
    private Func<T, TResult> _getProperty;

    public PropertyComparer(Func<T, TResult> predicate)
    {
        this._getProperty = predicate;
    }

    public bool Equals(T x, T y)
    {
        return this._getProperty(x).Equals(_getProperty(y));
    }

    public int GetHashCode(T obj)
    {
        return this._getProperty(obj).GetHashCode();
    }
}

Class with extension method:

public static class IEnumerableExtensions
{
    public static IEnumerable<TSource> DistinctBy<TSource, TResult>
        (this IEnumerable<TSource> source, Func<TSource, TResult> predicate)
    {
        return source.Distinct(new PropertyComparer<TSource, TResult>(predicate));
    }
}

Usage:

someEnumerable.DistinctBy(i => i.SomeProperty);
like image 23
Kuhelbecker Avatar answered Oct 20 '22 06:10

Kuhelbecker