Comparing similar types that don't share any interface I have two types that don't share any interface but have similar properties and that I need to compare for equality.

One of them is comming from a 3rd party library and one of them is mine. I obviously cannot add any interface to the foreign type and honestly I don't want to create any wrapper for both of them in order to just use it once for some linq. I prefer to have something that lasts longer and can be reused for other projects.

I used these to types for testing but they might of course have many other properties - and as a matter of fact my two actual types have exactly the same properties as this example:


class PersonLib1
{
  public string FirstName { get; set; }

  public string LastName { get; set; }
}

class PersonLib2
{
  public string FirstName { get; set; }

  public string LastName { get; set; }
}





API

I solved this problem by implementing the IEqualityComparer&lt;object&gt; as a DuckEqualityComparer - its name comes from the idea of Duck Typing - this is, since both types share some common or similar properties they must be the same.

My comparer allows the user to specify the exact types and their comparison logic. The order of the types is not relevant and it tries both combinations.

I used additional blocks of {} inside the Equals method to be able to reuse the short variable names.

class DuckEqualityComparer&lt;Tx, Ty&gt; : IEqualityComparer&lt;object&gt;
{
    private readonly Func&lt;Tx, Ty, bool&gt; _equals;
    private readonly Func&lt;Tx, int&gt; _getHashCodeX;
    private readonly Func&lt;Ty, int&gt; _getHashCodeY;

    private DuckEqualityComparer(Func&lt;Tx, Ty, bool&gt; equals, Func&lt;Tx, int&gt; getHashCodeX, Func&lt;Ty, int&gt; getHashCodeY)
    {
        _equals = equals;
        _getHashCodeX = getHashCodeX;
        _getHashCodeY = getHashCodeY;
    }

    public new bool Equals(object x, object y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (ReferenceEquals(x, null)) return false;
        if (ReferenceEquals(y, null)) return false;
        {
            if (x is Tx tx &amp;&amp; y is Ty ty &amp;&amp; Equals(tx, ty)) return true;
        }
        {
            if (x is Ty ty &amp;&amp; y is Tx tx &amp;&amp; Equals(tx, ty)) return true;
        }
        return false;
    }

    private bool Equals(Tx x, Ty y)
    {
        if (ReferenceEquals(x, null)) return false;
        if (ReferenceEquals(y, null)) return false;
        return _equals(x, y);
    }

    public int GetHashCode(object obj)
    {
        switch (obj)
        {
            case Tx x: return _getHashCodeX(x);
            case Ty y: return _getHashCodeY(y);
            default: return 0;
        }
    }

    public static IEqualityComparer&lt;object&gt; Create(Func&lt;Tx, Ty, bool&gt; equals, Func&lt;Tx, int&gt; getHashCodeX, Func&lt;Ty, int&gt; getHashCodeY)
    {

        return new DuckEqualityComparer&lt;Tx, Ty&gt;(equals, getHashCodeX, getHashCodeY);
    }
}


Why didn't I use tuples? Because they don't allow me to use other then the default comparer on the property level.

Is this an acceptable solution or is there anything that could be done better?



Example

This is how I use it:

var p1 = new PersonLib1 { FirstName = "John", LastName = "Doe" };
var p2 = new PersonLib2 { FirstName = "JOHN", LastName = "Doe" };
var p3 = new PersonLib2 { FirstName = "Joh", LastName = "Doe" };

var stringComparer = StringComparer.OrdinalIgnoreCase;
var personComparer = DuckEqualityComparer&lt;PersonLib1, PersonLib2&gt;.Create(
    equals: (x, y) =&gt;
        stringComparer.Equals(x.FirstName, y.FirstName) &amp;&amp;
        stringComparer.Equals(x.LastName, y.LastName),
    getHashCodeX: x =&gt; 
        stringComparer.GetHashCode(x.FirstName) + 
        stringComparer.GetHashCode(x.LastName),
    getHashCodeY: y =&gt; 
        stringComparer.GetHashCode(y.FirstName) + 
        stringComparer.GetHashCode(y.LastName)
);

personComparer.Equals(p1, p2).Dump();
personComparer.Equals(p2, p1).Dump();

personComparer.Equals(p1, p3).Dump();
personComparer.Equals(p3, p1).Dump();
You can shorten the syntax a bit and increase readability by using the is or == operators instead of ReferenceEquals:

public new bool Equals(object x, object y)
{
    if (ReferenceEquals(x, y)) return true;
    if (x is null || y is null) return false;
    {
        if (x is Tx tx &amp;&amp; y is Ty ty &amp;&amp; Equals(tx, ty)) return true;
    }
    {
        if (x is Ty ty &amp;&amp; y is Tx tx &amp;&amp; Equals(tx, ty)) return true;
    }
    return false;
}


The extra braces to make the variables local to their body is clever, however I cant exactly say I like it, it's very easy to consider the first braces to be part of the if statement above them and it can be made much more readable too if you extract it to a method:

public new bool Equals(object x, object y)
{
    if (ReferenceEquals(x, y)) return true;
    if (x is null || y is null) return false;
    return CompareObjects(x, y) || CompareObjects(y, x);
}

protected bool CompareObjects(object x, object y)
{
    return x is Tx tx &amp;&amp; y is Ty ty &amp;&amp; Equals(tx, ty);
}


You can also define 2 generic type arguments for the CompareObjects methods, to make it more explicit, that you're swapping the types.


private bool Equals(Tx x, Ty y)



Feels weird and out of place in my opinion, you're checking if the values of x and y are null, but you've already done so in public new bool Equals(object x, object y) and since it's a private method, that doesn't make much sense, removing this check makes it a simple call to _equals, which makes the whole method redundant, you are better off making it protected virtual, or just delete it all together

protected virtual bool Equals(Tx x, Ty y)
{
    return x != null &amp;&amp; y != null &amp;&amp; _equals(x, y);
}


You also lack null checks for the delegates in the Create method, but I assume you've omitted those for simplicity/