Search code examples
c#.netequalityanonymous-types

Why does Object.Equals() return false for identical anonymous types when they're instantiated from different assemblies?


I have some code that maps strongly-typed business objects into anonymous types, which are then serialized into JSON and exposed via an API.

After restructuring my solution into separate projects, some of my tests started to fail. I've done a bit of digging and it turns out that Object.Equals behaves differently on anonymous types that are returned by code from a different assembly - and I'm not sure why, or what I can do to work around it.

There's full repro code at https://github.com/dylanbeattie/AnonymousTypeEquality but the bit that's actually breaking is below. This code is in the Tests project:

[TestFixture]
public class Tests {
    [Test]
    public void BothInline() {
        var a = new { name = "test", value = 123 };
        var b = new { name = "test", value = 123 };
        Assert.That(Object.Equals(a,b)); // passes
    }

    [Test]
    public void FromLocalMethod() {
        var a = new { name = "test", value = 123 };
        var b = MakeObject("test", 123);
        Assert.That(Object.Equals(a, b)); // passes
    }

    [Test]
    public void FromOtherNamespace() {
        var a = new { name = "test", value = 123 };
        var b = OtherNamespaceClass.MakeObject("test", 123);
        Assert.That(Object.Equals(a, b)); // passes
    }


    [Test]
    public void FromOtherClass() {
        var a = new { name = "test", value = 123 };
        var b = OtherClass.MakeObject("test", 123);

        /* This is the test that fails, and I cannot work out why */
        Assert.That(Object.Equals(a, b));
    }

    private object MakeObject(string name, int value) {
        return new { name, value };
    }
}

and then there is a separate class library in the solution containing only this:

namespace OtherClasses {
  public static class OtherClass {
    public static object MakeObject(string name, int value) {
      return new { name, value };
    }
  }  
}

According to MSDN, "two instances of the same anonymous type are equal only if all their properties are equal." (my emphasis) - so what controls whether two instances are of the same anonymous type for comparison purposes? My two instances have equal hash codes, and both appear to be <>f__AnonymousType0`2[System.String,System.Int32] - but I'm guessing that equality for anonymous types must take the fully qualified type name into account and therefore moving code into a different assembly can break things. Anyone got a definitive source / link on exactly how this is implemented?


Solution

  • If you disassemble your assemblies using a tool like Reflector, you'll see that your anonymous type is represented by a class in each assembly that looks like this (after unmangling compiler-generated identifiers):

    internal sealed class AnonymousType<TName, TValue>
    {
        private readonly TName _name;
        private readonly TValue _value;
    
        public TName name => this._name;
        public TValue value => this._value;
    
        public AnonymousType(TName name, TValue value)
        {
            this._name = name;
            this._value = value;
        }
    
        public override bool Equals(object value)
        {
            var that = value as AnonymousType<TName, TValue>;
            return that != null &&
                EqualityComparer<TName>.Default.Equals(this._name, that._name) &&
                EqualityComparer<TValue>.Default.Equals(this._value, that._value);
        }
    
        public override int GetHashCode()
        {
            // ...
        }
    }
    

    The first line of the Equals method checks whether value is an instance of AnonymousType<TName, TValue>, referring specifically to the class defined in the current assembly. Thus, anonymous types from different assemblies will never compare equal even if they have the same structure.

    You may want to change your tests to compare the serialized JSON of objects rather than the objects themselves.