Reputation: 54130
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?
Upvotes: 11
Views: 1375
Reputation: 55389
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.
Upvotes: 6
Reputation: 13306
Anonymous types get get compiled to a hidden Type inside the assembly they live in, which is reused for efficiency purposes if the definition matches. This means that similar ATs in different assemblies will be of different Types, and their .Equals will do a Type check.
Here's one of my fave things to do with anon types:
void Main()
{
var json = "{ \"name\": \"Dylan\"}";
var x = Deserialize(json, new { name = null as string});
Console.WriteLine(x.name);
}
T Deserialize<T>(string json, T template)
{
return (T) JsonConvert.DeserializeObject(json, typeof(T));
}
It would be interesting to put the Deserialize method in another assembly...
Upvotes: 2
Reputation: 63732
Anonymous types are inherently scoped. Your example breaks that scoping, so the types are different. In current C# compilers, anonymous types cannot transcend assemblies (or modules, to be more exact). Even if two anonymous types from two different assemblies have the same properties, they are two different types (and they are internal
, so beware of the security implications). The second you downcast an anonymous type to object
, you know you're doing it wrong.
TL; DR: You're abusing anonymous types. Don't be surprised it bites you.
Upvotes: 9