6

Firstly I have seen IEqualityComparer for anonymous type and the answers there do not answer my question, for the obvious reason that I need an IEqualityComparer not and IComparer for use with Linq's Distinct() method. I have checked the other answers too and these fall short of a solution...

The Problem

I have some code to manipulate and pull records in from a DataTable

var glext = m_dtGLExt.AsEnumerable();
var cflist =
    (from c in glext
     orderby c.Field<string>(m_strpcCCType), 
             c.Field<string>(m_strpcCC), 
             c.Field<string>(m_strpcCCDesc),
             c.Field<string>(m_strpcCostItem)
     select new
     {
        CCType = c.Field<string>(m_strpcCCType),
        CC = c.Field<string>(m_strpcCC),
        CCDesc = c.Field<string>(m_strpcCCDesc),
        CostItem = c.Field<string>(m_strpcCostItem)
     }).Distinct();

but I need the distinct method to be case insensitive. What is throwing me here is the use of anonymous types.

Attempted Solution 1

If I had SomeClass which had concrete objects I could obviously do

public class SumObject
{
    public string CCType { get; set; }
    public string CC { get; set; }
    public string CCDesc { get; set; }
    public string CostItem { get; set; }
}

I could obviously do this

List<SumObject> lso = new List<SumObject>() 
{
    new SumObject() { CCType = "1-OCC", CC = "300401", CCDesc = "Rooney", CostItem = "I477" },
    new SumObject() { CCType = "1-OCC", CC = "300401", CCDesc = "Zidane", CostItem = "I677" },
    new SumObject() { CCType = "1-OCC", CC = "300401", CCDesc = "Falcao", CostItem = "I470" },
};
var e = lso.Distinct(new SumObjectComparer()); // Great :]

where

class SumObjectComparer : IEqualityComparer<SumObject>
{
    public bool Equals(SumObject x, SumObject y)
    {
        if (Object.ReferenceEquals(x, y)) 
            return true;
        if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
            return false;
        return x.CCType.CompareNoCase(y.CCType) == 0 && 
               x.CC.CompareNoCase(y.CC) == 0 && 
               x.CCDesc.CompareNoCase(y.CCDesc) == 0 && 
               x.CostItem.CompareNoCase(y.CostItem) == 0;
    }

    public int GetHashCode(SumObject o)
    {
        if (Object.ReferenceEquals(o, null)) 
            return 0;
        int hashCCType = String.IsNullOrEmpty(o.CCType) ? 
            0 : o.CCType.ToLower().GetHashCode();
        int hashCC = String.IsNullOrEmpty(o.CC) ? 
            0 : o.CC.ToLower().GetHashCode();
        int hashCCDesc = String.IsNullOrEmpty(o.CCDesc) ? 
            0 : o.CCDesc.ToLower().GetHashCode();
        int hashCostItem = String.IsNullOrEmpty(o.CostItem) ? 
            0 : o.CostItem.ToLower().GetHashCode();
        return hashCCType ^ hashCC ^ hashCCDesc ^ hashCostItem;
    }
}

However, the use of anonymous types in the above Linq query are throwing me.

Attempted Solution 2

To attempt another solution to this (and because I have the same issue elsewhere) I generated the following generic comparer class

public class GenericEqualityComparer<T> : IEqualityComparer<T>
{
    Func<T, T, bool> compareFunction;
    Func<T, int> hashFunction;

    public GenericEqualityComparer(Func<T, T, bool> compareFunction, Func<T, int> hashFunction)
    {
        this.compareFunction = compareFunction;
        this.hashFunction = hashFunction;
    }

    public bool Equals(T x, T y) { return compareFunction(x, y); }
    public int GetHashCode(T obj) { return hashFunction(obj); }
}

so that I could attempt to do

var comparer = new GenericEqualityComparer<dynamic>(
    (x, y) => { /* My equality stuff */ }, 
    o => { /* My hash stuff */ });

but this casts the returned value as IEnumerable<dynamic> which in turn effects my forthcoming use of cflist, so that in a following query the join fails.

 var cf = 
    (from o in cflist
     join od in glext
     on new { o.CCType, o.CC, o.CCDesc, o.CostItem } equals new
     {
        CCType = od.Field<string>(m_strpcCCType),
        CC = od.Field<string>(m_strpcCC),
        CCDesc = od.Field<string>(m_strpcCCDesc),
        CostItem = od.Field<string>(m_strpcCostItem)
     }
     into c
     select new { ... }

I don't want to get into ugly casting to and from IEnumerable<T>s due to the heavy use of this code...

Question

Is there a way I can create my an IEquailityComparer for my anonymous types?

Thanks for your time.

Community
  • 1
  • 1
MoonKnight
  • 23,214
  • 40
  • 145
  • 277
  • Your first solution would have worked if you replaced your anonymous `select new` object with a `select new SumObject` object. What's the big deal with using an anonymous object, if you have already created a named class for it? – Sergey Kalinichenko Sep 19 '14 at 13:00
  • I have'nt and can't, that is the point. If I could it would be easy. I also have several other places where these distinct operation work with linq select queries and anonymous types so I would like a more general solution... – MoonKnight Sep 19 '14 at 13:20
  • It will be slow, but I guess you will have to use reflection to write an IEqualityComparer and compare all fields and properties. – Manuel Schweigert Sep 19 '14 at 13:35

2 Answers2

12

Is there a way I can create my an IEquailityComparer for my anonymous types?

Sure. You just need to use type inference. For example, you could have something like:

public static class InferredEqualityComparer
{
    public static IEqualityComparer<T> Create<T>(
        IEnumerable<T> example,
        Func<T, T, bool> equalityCheck,
        Func<T, int> hashCodeProvider)
    {
        return new EqualityComparerImpl<T>(equalityCheck, hashCodeProvider);
    }

    private sealed class EqualityComparerImpl<T> : IEqualityComparer<T>
    {
        // Implement in the obvious way, remembering the delegates and
        // calling them appropriately.
    }
}

Then:

var glext = m_dtGLExt.AsEnumerable();
var query = from c in glext
            orderby ...
            select new { ... };
var comparer = InferredEqualityComparer.Create(query,
    (x, y) => { ... },
    o => { ... }
);
var distinct = query.Distinct(comparer);

Basically the first parameter to the method is just used for type inference, so that the compiler can work out what type to use for the lambda expression parameters.

You could create the comparer ahead of time by creating a sample of the anonymous type:

var sample = new[] { new { ... } };
var comparer = InferredExqualityComparer.Create(sample, ...);
var distinct = (... query here ... ).Distinct(comparer);

but then any time you change the query you've got to change the sample too.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 2
    Dammit Skeet I was about to post something just like this. – tom.dietrich Sep 19 '14 at 13:00
  • Hi Jon, thanks for the reply, but there may be some miss-understanding here. I don't have the `lso` object you have referenced, I was merely using this as an example of what I could do using `Distinct()` with a concrete type. I don't see how I can use the above solution with the LINQ query in the first code snippet of the question... – MoonKnight Sep 19 '14 at 13:24
  • @Killercam: Edited. Basically you need to get the "non-distinct" version first, then create the comparer, then call `Distinct`. – Jon Skeet Sep 19 '14 at 13:26
  • Thanks very much for your time Jon, it is always most appreciated. – MoonKnight Sep 19 '14 at 14:29
1

This post may get what you want. Although for .NET 2.0 it also works for newer versions (see the bottom of this post for how to achieve this). In contrast to Jon Skeets solution we won´t use a factory-method like create. But this is only syntactic sugar I think.

Community
  • 1
  • 1
MakePeaceGreatAgain
  • 35,491
  • 6
  • 60
  • 111