简体   繁体   English

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

[英]Is there a way to generically mock the DbSet.Find method with Moq?

I'm currently using an extension method to generically mock DbSets as a list:我目前正在使用扩展方法将 DbSet 模拟为列表:

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

However, I can't figure out a way to mock the Find method, which searches based on the table's primary key.但是,我想不出一种模拟 Find 方法的方法,该方法基于表的主键进行搜索。 I could do it at a specific level for each table because I can inspect the database, get the PK, and then just mock the Find method for that field.我可以在每个表的特定级别执行此操作,因为我可以检查数据库,获取 PK,然后只是模拟该字段的 Find 方法。 But then I can't use the generic method.但是我不能使用泛型方法。

I suppose I could also go add to the partial classes that EF auto-generated to mark which field is the PK with an attribute or something.我想我也可以添加到 EF 自动生成的部分类中,以标记哪个字段是具有属性或其他东西的 PK。 But we have over 100 tables and it makes the code more difficult to manage if you're relying on people to manually maintain this.但是我们有 100 多个表,如果您依靠人员手动维护它,这会使代码更难以管理。

Does EF6 provide any way of finding the primary key, or does it only know dynamically after it's connected to the database? EF6 是否提供任何查找主键的方法,还是仅在连接到数据库后才动态知道?

As far as I can tell, there is no 'best practice' answer to this question, but here's how I've approached it.据我所知,这个问题没有“最佳实践”的答案,但我是这样处理的。 I've added an optional parameter to the AsDbSet method which identifies the primary key, then the Find method can be mocked up easily.我向AsDbSet方法添加了一个可选参数,用于标识主键,然后可以轻松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()));
    }

    ...
}

I've written this on the assumption of a single guid being used as primary key as that seemed to be how you're working, but the principle should be easy enough to adapt if you need more flexibility for composite keys, etc.我写这篇文章的前提是假设将单个 guid 用作主键,因为这似乎是您的工作方式,但是如果您需要对复合键等具有更大的灵活性,那么该原则应该很容易适应。

After pondering this for awhile, I think I've found the "best" solution currently available.经过一段时间的思考,我想我已经找到了目前可用的“最佳”解决方案。 I just have a series of if statements that directly checks the type in the extension method.我只有一系列 if 语句直接检查扩展方法中的类型。 Then I cast to the type I need to set the find behavior and cast it back to generic when I'm done.然后我转换为我需要设置查找行为的类型,并在完成后将其转换回通用类型。 It's only pseudo-generic, but I can't think of anything else better.它只是伪通用,但我想不出还有什么更好的了。

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

I ended in the following class:我在以下课程中结束:

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
}

Which can be used:可以使用:

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

I'm using now Entity Framework Core 2, and this solution works fine to me.我现在正在使用 Entity Framework Core 2,这个解决方案对我来说很好用。

At first, I will find the primary key by using the class name with the suffix “Id”.首先,我会通过使用后缀“Id”的类名来查找主键。 (If you follow other convention you must change it to fit your necessity.) (如果您遵循其他约定,则必须更改它以适合您的需要。)

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

Now that you know the Pk, you can support the Find with the following code既然知道了Pk,就可以用下面的代码支持Find了

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

So, the complete code for generically mock supporting DbSet.Find you can see below:因此,您可以在下面看到通用模拟支持 DbSet.Find 的完整代码:

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

My solution was to add a parameter to specify the key of the entity:我的解决方案是添加一个参数来指定实体的键:

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

You can use it as您可以将其用作

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

The BuildMockDbSet comes from the MockQueryable library (available from NuGet). BuildMockDbSet 来自 MockQueryable 库(可从 NuGet 获得)。


Edit: By the way, if you really do not want to specify the key everytime you call the above function, and you know most of your keys are of type int, you can create another overload like:编辑:顺便说一句,如果您真的不想在每次调用上述函数时都指定键,并且您知道大多数键的类型为 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>());
}

where GetKey is implemented by the extension methods:其中 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);
}

So now you can call it simply as所以现在你可以简单地称之为

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM