有没有办法一般用Moq模拟DbSet.Find方法?

时间:2016-10-04 16:23:25

标签: c# entity-framework generics entity-framework-6 moq

我目前正在使用扩展方法将DbSets一般地模拟为列表:

    public static DbSet<T> AsDbSet<T>(this List<T> sourceList) where T : class
    {
        var queryable = sourceList.AsQueryable();
        var mockDbSet = new Mock<DbSet<T>>();
        mockDbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
        mockDbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
        mockDbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
        mockDbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator());
        mockDbSet.Setup(x => x.Add(It.IsAny<T>())).Callback<T>(sourceList.Add);
        mockDbSet.Setup(x => x.Remove(It.IsAny<T>())).Returns<T>(x => { if (sourceList.Remove(x)) return x; else return null; } );

        return mockDbSet.Object;
    }

但是,我无法找到一种模拟Find方法的方法,该方法根据表格的主键进行搜索。我可以在每个表的特定级别执行此操作,因为我可以检查数据库,获取PK,然后只是模拟该字段的Find方法。但后来我无法使用通用方法。

我想我也可以添加EF自动生成的部分类来标记哪个字段是带有属性的PK。但是我们有超过100个表,如果你依靠人来手动维护它,它会使代码更难管理。

EF6是否提供了查找主键的任何方法,或者只有在连接到数据库后才能动态地知道主键?

5 个答案:

答案 0 :(得分:5)

经过一段时间的思考后,我想我已经找到了目前可用的“最佳”解决方案。我只有一系列if语句直接检查扩展方法中的类型。然后我转换为我需要设置查找行为的类型,并在完成后将其强制转换为泛型。它只是伪通用的,但我想不出更好的其他任何东西。

        if (typeof(T) == typeof(MyFirstSet))
        {
            mockDbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns<object[]>(x => (sourceList as List<MyFirstSet>).FirstOrDefault(y => y.MyFirstSetKey == (Guid)x[0]) as T);
        }
        else if (typeof(T) == typeof(MySecondSet))
        {
            mockDbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns<object[]>(x => (sourceList as List<MySecondSet>).FirstOrDefault(y => y.MySecondSetKey == (Guid)x[0]) as T);
        }
        ...       

答案 1 :(得分:4)

据我所知,没有最好的做法&#39;回答这个问题,但在这里我是如何接近它的。我已在标识主键的AsDbSet方法中添加了一个可选参数,然后可以轻松模拟Find方法。

public static DbSet<T> AsDbSet<T>(this List<T> sourceList, Func<T, object> primaryKey = null) where T : class
{
    //all your other stuff still goes here

    if (primaryKey != null)
    {
        mockSet.Setup(set => set.Find(It.IsAny<object[]>())).Returns((object[] input) => sourceList.SingleOrDefault(x => (Guid)primaryKey(x) == (Guid)input.First()));
    }

    ...
}

我是在假设单个guid被用作主键的情况下编写的,因为这似乎是你工作的方式,但如果你需要更多的灵活性,原则应该很容易适应复合键等

答案 2 :(得分:0)

我在下面的课程中结束了:

public static class DbSetMocking
{
    #region methods

    public static IReturnsResult<TContext> ReturnsDbSet<TEntity, TContext>( this IReturns<TContext, DbSet<TEntity>> setup, ICollection<TEntity> entities, Func<object[], TEntity> find = null )
        where TEntity : class where TContext : DbContext
    {
        return setup.Returns( CreateMockSet( entities, find ).Object );
    }

    private static Mock<DbSet<T>> CreateMockSet<T>( ICollection<T> data, Func<object[], T> find )
        where T : class
    {
        var queryableData = data.AsQueryable();
        var mockSet = new Mock<DbSet<T>>();
        mockSet.As<IQueryable<T>>().Setup( m => m.Provider ).Returns( queryableData.Provider );
        mockSet.As<IQueryable<T>>().Setup( m => m.Expression ).Returns( queryableData.Expression );
        mockSet.As<IQueryable<T>>().Setup( m => m.ElementType ).Returns( queryableData.ElementType );
        mockSet.As<IQueryable<T>>().Setup( m => m.GetEnumerator() ).Returns( queryableData.GetEnumerator() );

        mockSet.SetupData( data, find );

        return mockSet;
    }

    #endregion
}

可以使用哪些:

private static MyRepository SetupRepository( ICollection<Type1> type1s, ICollection<Type2> type2s )
{
    var mockContext = new Mock<MyDbContext>();

    mockContext.Setup( x => x.Type1s ).ReturnsDbSet( type1s, o => type1s.SingleOrDefault( s => s.Secret == ( Guid ) o[ 0 ] ) );
    mockContext.Setup( x => x.Type2s ).ReturnsDbSet( type2s, o => type2s.SingleOrDefault( s => s.Id == ( int ) o[ 0 ] ) );

    return new MyRepository( mockContext.Object );
}

答案 3 :(得分:0)

我现在正在使用Entity Framework Core 2,该解决方案对我来说很好用。

首先,我将使用带有后缀“ Id”的类名称来找到主键。 (如果遵循其他约定,则必须根据需要进行更改。)

        //Find primary key. Here the PK must follow the convention "Class Name" + "Id" 
        Type type = typeof(T);
        string colName = type.Name + "Id";
        var pk = type.GetProperty(colName);
        if (pk == null)
        {
            colName = type.Name + "ID";
            pk = type.GetProperty(colName);
        }

现在您知道Pk了,您可以使用以下代码来支持“查找”

        dbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns((object[] id) =>
        {
            var param = Expression.Parameter(type, "t");
            var col = Expression.Property(param, colName);
            var body = Expression.Equal(col, Expression.Constant(id[0]));
            var lambda = Expression.Lambda<Func<T, bool>>(body, param);
            return queryable.FirstOrDefault(lambda);
        });

因此,用于支持DbSet的通用模拟的完整代码。查找您可以在下面看到:

public static DbSet<T> GetQueryableMockDbSet<T>(List<T> sourceList) where T : class
    {
        var queryable = sourceList.AsQueryable();
        var dbSet = new Mock<DbSet<T>>();

        dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
        dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
        dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
        dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
        dbSet.Setup(d => d.Add(It.IsAny<T>())).Callback<T>((s) => sourceList.Add(s));

        //Find primary key. Here the PK must follow the convention "Class Name" + "Id" 
        Type type = typeof(T);
        string colName = type.Name + "Id";
        var pk = type.GetProperty(colName);
        if (pk == null)
        {
            colName = type.Name + "ID";
            pk = type.GetProperty(colName);
        }

        dbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns((object[] id) =>
        {
            var param = Expression.Parameter(type, "t");
            var col = Expression.Property(param, colName);
            var body = Expression.Equal(col, Expression.Constant(id[0]));
            var lambda = Expression.Lambda<Func<T, bool>>(body, param);
            return queryable.FirstOrDefault(lambda);
        });

        return dbSet.Object;

    } //GetQueryableMockDbSet

答案 4 :(得分:0)

我的解决方案是添加一个参数以指定实体的键:

public static Mock<DbSet<TEntity>> Setup<TContext, TEntity, TKey>(this Mock<TContext> mockContext,
    Expression<Func<TContext, DbSet<TEntity>>> expression, List<TEntity> sourceList,
    Func<TEntity, TKey> id)
    where TEntity : class
    where TContext : DbContext
{
    IQueryable<TEntity> data = sourceList.AsQueryable();
    Mock<DbSet<TEntity>> mock = data.BuildMockDbSet();

    // make adding to and searching the list work
    mock.Setup(d => d.Add(It.IsAny<TEntity>())).Callback(add(sourceList));
    mock.Setup(d => d.Find(It.IsAny<object[]>())).Returns<object[]>(s => find(sourceList, id, s));

    // make context.Add() and Find() work
    mockContext.Setup(x => x.Add(It.IsAny<TEntity>())).Callback(add(sourceList));
    mockContext.Setup(x => x.Find<TEntity>(It.IsAny<object[]>()))
        .Returns<object[]>(s => find(sourceList, id, s));
    mockContext.Setup(x => x.Find(typeof(TEntity), It.IsAny<object[]>()))
        .Returns<Type, object[]>((t, s) => find(sourceList, id, s));
    mockContext.Setup(expression).Returns(mock.Object);
    return mock;
}

private static Action<TEntity> add<TEntity>(IList<TEntity> sourceList)
    where TEntity : class
{
    return s => sourceList.Add(s);
}

private static TEntity find<TEntity, TKey>(IList<TEntity> sourceList, Func<TEntity, TKey> id, object[] s) where TEntity : class
{
    return sourceList.SingleOrDefault(e => id(e).Equals(s[0]));
}

您可以将其用作

mockContext.Setup(m => m.Users, users, x => x.UsedId);

BuildMockDbSet来自MockQueryable库(可从NuGet获得)。


编辑:顺便说一句,如果您真的不想每次调用上述函数时都指定键,并且您知道大多数键都是int类型的,则可以创建另一个重载像:

public static Mock<DbSet<TEntity>> Setup<TContext, TEntity>(this Mock<TContext> mockContext,
    Expression<Func<TContext, DbSet<TEntity>>> expression, List<TEntity> sourceList)
    where TEntity : class
    where TContext : DbContext
{
    return Setup(mockContext, expression, sourceList, x => x.GetKey<int>());
}

其中GetKey由扩展方法实现:

public static object? GetKey(this object entity)
{
    PropertyInfo keyInfo = entity.GetType().GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(KeyAttribute))).SingleOrDefault();

    if (keyInfo == null)
        return null;

    return keyInfo.GetValue(entity);
}

public static TKey GetKey<TKey>(this object entity)
{
    return (TKey)GetKey(entity);
}

所以现在您可以简单地称呼它

var mockUsers = mockContext.Setup(m => m.Users, users);