FluentAssertions()。BeEquivalentTo不会比较EF动态代理上的运行时派生类型

时间:2018-03-16 18:49:18

标签: c# entity-framework dynamic-proxy fluent-assertions

我使用FluentAssertions比较使用Should().BeEquivalentTo()的两个对象,其中一个对象是EF动态代理。但是,在使用ShouldBeEquivalentTo时,5.0.0中ShouldAllBeEquivalentToRespectingRuntimeTypes#593)的统一似乎已经破坏了功能。除非我为对象图中的每个类型显式添加ComparingByMembers,否则不再比较已声明类型的派生类型的属性成员。有没有办法使用其他设置?

2 个答案:

答案 0 :(得分:1)

为了解决这个问题,我编写了以下扩展方法,但在动态代理上运行时修复派生类型的问题似乎很麻烦:

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;
    }
}

这应该像这样调用:

foo.Should().BeEquivalentTo(bar, o => o
    .RespectingRuntimeTypes()
    .ComparingByMembersRecursive(foo)
    .ExcludingMissingMembers());

答案 1 :(得分:0)

我最近使用Microsoft.EntityFrameworkCore.Proxies遇到了同样的问题。就我而言,我必须比较持久属性,而忽略比较其余甚至导航属性。

该解决方案正在实现Intercafe FluentAssertions.Equivalency.IMemberSelectionRule以排除不必要的属性。

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;
}

然后编写扩展方法可以帮助方便使用并提高可读性。扩展方法可以类似于以下代码。

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));
    }
}

最后,您可以像下面的代码一样在测试中调用扩展方法。

// Assert something
using (var context = DbContextFactory.Create())
{
    var myEntitySet = context.MyEntities.ToArray();
    myEntitySet.Should().BeEquivalentTo(expectedEntities, options => options
        .IncludingPersistentProperties(context)
        .Excluding(r => r.MyPrimaryKey));
}

此实现解决了我的问题,代码看起来整洁。