When an anonymous type is defined in one assembly, it won’t match an anonymous type defined in another assembly. This causes problems when you’re unit testing and trying to mock a method that has an anonymous type parameter.
For example, let’s say you’re trying to unit test the following method:
public IEnumerable<Employee> GetEmployees(string title)
{
return Repository.Query("SELECT * FROM Employees WHERE Title=@Title", new { title });
}
Code language: C# (cs)
To unit test this, you want to mock out the Repository.Query() method. Intuitively, you may try to pass in an anonymous type to try to match the method call:
mockRepo.Setup(t => t.Query(expectedSql, new { title = "Programmer" }))
.Returns(employees);
Code language: C# (cs)
This won’t work though, because the anonymous types are defined in different assemblies and they won’t match.
In this article, I’ll show different options for solving this problem, and explain why you shouldn’t use GetHashCode() to solve this.
Note: This is using Moq in the examples, but would apply to other mocking frameworks too, since the problem would happen anytime you’re trying to match anonymous types defined in different assemblies.
Table of Contents
Serialize to JSON and compare the strings
One way to match an anonymous type parameter is to use JSON serialization. You can serialize the anonymous type and the actual parameter and then compare the JSON strings. It’s a good idea to put this in an extension method, such as the following:
using System.Text.Json;
public static class AnonymousTypeExtensions
{
private readonly static JsonSerializerOptions options = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public static bool JsonMatches(this object o, object that)
{
return JsonSerializer.Serialize(o, options) == JsonSerializer.Serialize(that, options);
}
}
Code language: C# (cs)
The mock setup would then use this extension method like this:
mockRepo.Setup(t => t.Query(expectedSql,
It.Is<object>(o => o.JsonMatches(new { title = "Programmer" }))))
.Returns(employees);
Code language: C# (cs)
Using the JsonNamingPolicy.CamelCase setting makes it serialize all properties with the same casing. If two anonymous types have property names with different casing, this’ll match them.
Other options
There are other options for matching anonymous types.
Option 1 – Don’t check the anonymous type properties/values
If you aren’t concerned about precisely matching the anonymous type parameter, then you can keep it simple and match any object:
mockRepo.Setup(t => t.Query(expectedSql,
It.IsAny<object>()))
.Returns(employees);
Code language: C# (cs)
This option prevents your tests from being fragile. Anytime tests know too much about the internal workings of the code they are testing, they’re fragile and can break easily.
Option 2 – Use reflection manually
You can use reflection to get the properties from the anonymous type and compare the property values:
mockRepo.Setup(t => t.Query(expectedSql,
It.Is<object>(o => (string)o.GetType().GetProperty("title").GetValue(o) == "Programmer")))
.Returns(employees);
Code language: C# (cs)
This gives you complete flexibility over what is compared to determine if the parameters match, but it can be tedious if there are several properties to match.
This is the best approach if you only want to match based on a few of the properties. If you want to match based on all properties, stick with the JSON serialization approach shown up above.
Don’t use GetHashCode() – it doesn’t always work
Using GetHashCode() sometimes works:
var expectedParam = new { title = "Programmer" };
mockRepo.Setup(t => t.Query(expectedSql,
It.Is<object>(o => o.GetHashCode() == expectedParam.GetHashCode())))
.Returns(employees);
Code language: C# (cs)
It doesn’t always work though, which is why I wouldn’t recommend using this approach when trying to match anonymous types.
Problem 1 – It doesn’t always match anonymous types
Here’s an example of where using GetHashCode() fails to match the anonymous type parameter. Let’s say you’re testing the following method with the highlighted anonymous type:
public IEnumerable<Employee> GetEmployees(string title)
{
return Repository.Query("SELECT * FROM Employees WHERE Title=@Title",
new { title, languages = new[] { "C#", "Java" } });
}
Code language: C# (cs)
The following attempt to match the anonymous type with GetHashCode() will fail:
var expectedParam = new { title = "Programmer", languages = new[] { "C#", "Java" }};
mockRepo.Setup(t => t.Query(expectedSql,
It.Is<object>(o => o.GetHashCode() == expectedParam.GetHashCode())))
.Returns(employees);
Code language: C# (cs)
Apparently it doesn’t work when there are array properties.
The JSON serialization approach works fine in this scenario:
mockRepo.Setup(t => t.Query(expectedSql,
It.Is<object>(o => o.JsonMatches(new { title = "Programmer", languages = new[] { "C#", "Java" } }))))
.Returns(employees);
Code language: C# (cs)
Problem 2 – It can only do case-sensitive property name matching
Let’s say you’re testing the following method:
public IEnumerable<Employee> GetEmployees(string title)
{
return Repository.Query("SELECT * FROM Employees WHERE Title=@Title",
new { Title = "Programmer" });
}
Code language: C# (cs)
When you’re using GetHashCode(), the property names must have the same casing to match. For example, this wouldn’t match the anonymous type above (title vs Title):
var expectedParam = new { title = "Programmer" };
mockRepo.Setup(t => t.Query(expectedSql,
It.Is<object>(o => o.GetHashCode() == expectedParam.GetHashCode())))
.Returns(employees);
Code language: C# (cs)
In comparison, the JSON serialization approach is able to do case-insensitive matching (because passing in JsonNamingPolicy.CamelCase formats all the serialized property names in the same way), and is able to match the anonymous type in this scenario:
mockRepo.Setup(t => t.Query(expectedSql,
It.Is<object>(o => o.JsonMatches(new { title = "Programmer" }))))
.Returns(employees);
Code language: C# (cs)
When the anonymous type properties aren’t declared in the same order
This is one scenario where none of the approaches shown will automatically solve the problem.
Let’s say the code you’re testing uses the following anonymous type:
new { title = "Programmer", name = "Bob" }
Code language: C# (cs)
And in the unit test you’ve declared the anonymous type with the properties in a different order (<name, title> instead of <title, name>):
var expectedParam = new { name = "Bob", title = "Programmer" };
Code language: C# (cs)
The JSON Serialization approach won’t work with this, because it serializes properties in the order they are declared (but you can set the serialization order explicitly if that’s a path you want to go down). GetHashCode() also won’t work.
The simplest solution here is to just fix the anonymous type declaration in the test.