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):
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.