3

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?

Neo
  • 4,145
  • 6
  • 53
  • 76
  • 1
    Do these types override 'Equals'? – Dennis Doomen Mar 16 '18 at 21:27
  • @DennisDoomen No. – Neo Mar 16 '18 at 22:08
  • 1
    Did 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 Answers2

2

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());
Neo
  • 4,145
  • 6
  • 53
  • 76
1

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.