我目前正在使用扩展方法将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是否提供了查找主键的任何方法,或者只有在连接到数据库后才能动态地知道主键?
答案 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);