1

I have found a very strange behavior in C# (.net core 3.1) for comparison of anonymous objects that I cannot explain.

As far as I understand calling Equals for anonymous objects uses structural equality comparison (check e.g. here). Example:

public static class Foo
{
    public static object GetEmptyObject() => new { };
}

static async Task Main(string[] args)
{   
    var emptyObject = new { };

    emptyObject.Equals(new { }); // True
    emptyObject.Equals(Foo.GetEmptyObject()); // True
}

That looks correct. But the situation gets totally different if I move 'Foo' to another assembly!

emptyObject.Equals(Foo.GetEmptyObject()); // False

Exactly same code returns a different result if the anonymous object is from another assembly.

Is this a bug in C#, implementation detail or something that I do not understand alltogether?

P.S. Same thing happens if I evaluate the expression in quick watch (true in runtime, false in quick watch):

enter image description here

Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
  • Does this answer your question? [Assert.Equal anonymous objects across assemblies fails](https://stackoverflow.com/questions/22098096/assert-equal-anonymous-objects-across-assemblies-fails) – Pavel Anikhouski Mar 17 '20 at 13:14
  • 2
    In addition to the link above check remarks here https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/anonymous-types#remarks – Nkosi Mar 17 '20 at 13:16
  • @PavelAnikhouski it seems like it's the same thing, yes. But my question is a bit more generic, and it has a way better answer with details, so perhaps it can live on it's own – Ilya Chernomordik Mar 17 '20 at 13:25

1 Answers1

4

It's an implementation detail that you don't understand.

If you use an anonymous type, the compiler has to generate a new type (with an unspeakable name, such as <>f__AnonymousType0<<A>j__TPar>), and it generates this type in the assembly which uses it.

It will use that same generated type for all usages of anonymous types with the same structure within that assembly. However, each assembly will have its own anonymous type definitions: there's no way to share them across assemblies. Of course the way around this, as you discovered, is to pass them around as object.

This restriction is one of the main reasons why there's no way of exposing anonymous types: you can't return them from methods, have them as fields etc. It would cause all sorts of issues if you could pass them around between assemblies.

You can see that at work in SharpLab, where:

var x = new { A = 1 };

causes this type to be generated in the same assembly:

internal sealed class <>f__AnonymousType0<<A>j__TPar>
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <A>j__TPar <A>i__Field;

    public <A>j__TPar A
    {
        get
        {
            return <A>i__Field;
        }
    }

    [DebuggerHidden]
    public <>f__AnonymousType0(<A>j__TPar A)
    {
        <A>i__Field = A;
    }

    [DebuggerHidden]
    public override bool Equals(object value)
    {
        global::<>f__AnonymousType0<<A>j__TPar> anon = value as global::<>f__AnonymousType0<<A>j__TPar>;
        if (anon != null)
        {
            return EqualityComparer<<A>j__TPar>.Default.Equals(<A>i__Field, anon.<A>i__Field);
        }
        return false;
    }

    [DebuggerHidden]
    public override int GetHashCode()
    {
        return -1711670909 * -1521134295 + EqualityComparer<<A>j__TPar>.Default.GetHashCode(<A>i__Field);
    }

    [DebuggerHidden]
    public override string ToString()
    {
        object[] obj = new object[1];
        <A>j__TPar val = <A>i__Field;
        obj[0] = ((val != null) ? val.ToString() : null);
        return string.Format(null, "{{ A = {0} }}", obj);
    }
}

ValueTuple had the same challenges around wanting to define types anonymously but still pass them between assemblies, and solved it a different way: by defining ValueTuple<..> in the BCL, and using compiler magic to pretend that their properties have names other than Item1, Item2, etc.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • It seems that the answer lies in the details of Equals implementation as it really does compare if it is exactly the same type (i.e. from same assembly). It does not really do a comparison of the "fields" or binary representation in a way as I thought. So it just returns false if it's any other type, and anonymous type from another library is another type. – Ilya Chernomordik Mar 17 '20 at 13:22
  • @IlyaChernomordik It would have to use reflection to do that, and no, anonymous types don't use reflection – canton7 Mar 17 '20 at 13:23
  • Reflection or some magic to do comparison of memory dump in a way (don't even know if it's possible). I.e. if it's only int in both types it would really have to be exactly the same 4 bytes in memory – Ilya Chernomordik Mar 17 '20 at 13:24
  • @IlyaChernomordik Struct equality does something similar, but only if the struct type is blittable, otherwise it falls back to reflection. [Link](https://source.dot.net/#System.Private.CoreLib/src/System/ValueType.cs,23). Note that that code works for two objects of the same type, which wouldn't be the case here: you'd have to match up members using their name and type. And no, the intention of anonymous types is that they're kept local to methods and are not passed between assemblies, so I assume the designers thought that it wasn't worth the performance penalty of falling back to reflection – canton7 Mar 17 '20 at 13:25
  • Yes, that what I expect anonymous types would get as well – Ilya Chernomordik Mar 17 '20 at 13:27
  • 1
    Thanks for the great answer anyway, as usual your answers explain everything in details :) – Ilya Chernomordik Mar 17 '20 at 13:27