I'm using FluentAssertions to compare two objects using Should().BeEquivalentTo()
where one object is an EF dynamic proxy. However, it appears that the unification of ShouldBeEquivalentTo
and ShouldAllBeEquivalentTo
(#593) in 5.0.0 has broken the functionality when using RespectingRuntimeTypes
. The property members of derived types of the declared types are no longer being compared unless I add ComparingByMembers
explicitly for every type in my object graph. Is there any way around this using other settings?

- 4,145
- 6
- 53
- 76
-
1Do these types override 'Equals'? – Dennis Doomen Mar 16 '18 at 21:27
-
@DennisDoomen No. – Neo Mar 16 '18 at 22:08
-
1Did you read this? http://www.continuousimprover.com/2018/02/fluent-assertions-50-best-unit-test.html?m=1 – Dennis Doomen Mar 17 '18 at 05:35
-
1@DennisDoomen I did indeed, and I read the 'Moving towards a unified API', then skipped straight to 'Upgrading Tips' (viewing this as the "TLDR" section) expecting this would highlight any breaking changes. I found out this morning that the base class indeed implements `Equals` incorrectly, and removing it resolves the issue. Thanks for your reply, but may I suggest that you add an additional bullet point to the 'Upgrading Tips' section (under "changes to BeEquivalentTo") stating to ensure any `Equals` implementation conforms to the reference type value semantics principle it is supposed to? – Neo Mar 19 '18 at 11:04
-
Good idea. Added this to the original blog. – Dennis Doomen Mar 26 '18 at 10:24
-
Got hit by that too. Unlike @Neo none class in the hierarchy implements equals, so that's not the problem for me. Using `IncludingAllRuntimeProperties` or `RespectingRuntimeTypes` fixed it for me. – Samuel Mar 12 '19 at 09:51
2 Answers
I've written the following extension method in an attempt to resolve the problem, but it seems cumbersome just to fix an issue with derived types at run-time on dynamic proxies:
public static class FluentAssertionsExtensions
{
/// <summary>
/// Extends the functionality of <see cref="EquivalencyAssertionOptions{TExpectation}" />.ComparingByMembers by recursing into the entire object graph
/// of the T or passed object and marks all property reference types as types that should be compared by its members even though it may override the
/// System.Object.Equals(System.Object) method. T should be used in conjunction with RespectingDeclaredTypes. The passed object should be used in
/// conjunction with RespectingRuntimeTypes.
/// </summary>
public static EquivalencyAssertionOptions<T> ComparingByMembersRecursive<T>(this EquivalencyAssertionOptions<T> options, object obj = null)
{
var handledTypes = new HashSet<Type>();
var items = new Stack<(object obj, Type type)>(new[] { (obj, obj?.GetType() ?? typeof(T)) });
while (items.Any())
{
(object obj, Type type) item = items.Pop();
Type type = item.obj?.GetType() ?? item.type;
if (!handledTypes.Contains(type))
{
handledTypes.Add(type);
foreach (PropertyInfo pi in type.GetProperties())
{
object nextObject = item.obj != null ? pi.GetValue(item.obj) : null;
Type nextType = nextObject?.GetType() ?? pi.PropertyType;
// Skip string as it is essentially an array of chars, and needn't be processed.
if (nextType != typeof(string))
{
if (nextType.GetInterface(nameof(IEnumerable)) != null)
{
nextType = nextType.HasElementType ? nextType.GetElementType() : nextType.GetGenericArguments().First();
if (nextObject != null)
{
// Look at all objects in a collection in case any derive from the collection element type.
foreach (object enumObj in (IEnumerable)nextObject)
{
items.Push((enumObj, nextType));
}
continue;
}
}
items.Push((nextObject, nextType));
}
}
if (type.IsClass && type != typeof(string))
{
// ReSharper disable once PossibleNullReferenceException
options = (EquivalencyAssertionOptions<T>)options
.GetType()
.GetMethod(nameof(EquivalencyAssertionOptions<T>.ComparingByMembers))
.MakeGenericMethod(type).Invoke(options, null);
}
}
}
return options;
}
}
This should be called like this:
foo.Should().BeEquivalentTo(bar, o => o
.RespectingRuntimeTypes()
.ComparingByMembersRecursive(foo)
.ExcludingMissingMembers());

- 4,145
- 6
- 53
- 76
I Recently faced to the same problem by using Microsoft.EntityFrameworkCore.Proxies
. In my case, I had to compare persistent properties and ignore comparing the rest even navigation properties.
The solution is implementing the intercafe FluentAssertions.Equivalency.IMemberSelectionRule
to excluding unnecessary properties.
public class PersistentPropertiesSelectionRule<TEntity> : IMemberSelectionRule
where TEntity : class
{
public PersistentPropertiesSelectionRule(DbContext dbContext) =>
this.dbContext = dbContext;
public bool IncludesMembers => false;
public IEnumerable<SelectedMemberInfo> SelectMembers(
IEnumerable<SelectedMemberInfo> selectedMembers,
IMemberInfo context,
IEquivalencyAssertionOptions config)
{
var dbPropertyNames = dbContext.Model
.FindEntityType(typeof(TEntity))
.GetProperties()
.Select(p => p.Name)
.ToArray();
return selectedMembers.Where(x => dbPropertyNames.Contains(x.Name));
}
public override string ToString() => "Include only persistent properties";
readonly DbContext dbContext;
}
Then writing an extension method can help for convenient usage and also improve readability. The extension method can be something like the following piece of code.
public static class FluentAssertionExtensions
{
public static EquivalencyAssertionOptions<TEntity> IncludingPersistentProperties<TEntity>(this EquivalencyAssertionOptions<TEntity> options, DbContext dbContext)
where TEntity : class
{
return options.Using(new PersistentPropertiesSelectionRule<TEntity>(dbContext));
}
}
At the end you can call the extension method in you test like the below piece of code.
// Assert something
using (var context = DbContextFactory.Create())
{
var myEntitySet = context.MyEntities.ToArray();
myEntitySet.Should().BeEquivalentTo(expectedEntities, options => options
.IncludingPersistentProperties(context)
.Excluding(r => r.MyPrimaryKey));
}
This implementaion solved my problem and the code looks neat and tidy.

- 11
- 1