如何使用内存数据库提供程序中的Entity Framework Core测试数据库视图?

时间:2019-03-27 09:31:58

标签: c# entity-framework-core

当我尝试向视图中添加对象时,它引发了异常unable to track an instance of type because it is a query type。有办法解决这个问题吗?

4 个答案:

答案 0 :(得分:2)

根据定义,

Query Types只读的(对于所有数据库提供程序,不仅适用于内存):

  
      
  • 永远不会跟踪DbContext上的更改,因此永远不会在数据库上对其进行插入,更新或删除。
  •   

但是,除了通常的

使用场景之外
  
      
  • 映射到数据库视图。
  •   
  • 映射到未定义主键的表。
  •   

他们允许

  
      
  • 映射到模型中定义的查询。
  •   

或换句话说

  
      
  • 可能会映射到定义查询-定义查询是模型中声明的辅助查询,它充当查询类型的数据源。
  •   

这是通过ToQuery流利的API实现的:

  

配置用于提供查询类型数据的查询。

因此,要在内存数据库中测试查询类型,应使用定义查询映射功能。

例如,在OnModelCreating内,您可以添加如下内容:

if (Database.IsInMemory())
{
   // In memory test query type mappings
    modelBuilder.Query<MyQueryType>().ToQuery(() => LINQ_query);
    // ... similar for other query types
}
else
{
    // Database query type mappings
    modelBuilder.Query<MyQueryType>().ToView("MyQueryTypeView");
    // ... 
}

其中LINQ_query是访问上下文DbSetDbQuery并投影到MyQueryType的普通LINQ查询。

然后,测试将为涉及的实体提供数据,并且使用DbQuery s进行的查询将从定义的查询中检索数据。


以上是推荐使用内存数据库测试视图的方法。

仅出于完整性考虑,可以通过创建某种查询存储库直接向DbQuery s提供数据(基本上是模拟它们),但有以下限制-必须 shared static,因为当前EF Core除DbSet<T>DbQuery<T>之外无法正确处理数据库上下文成员(如全局查询过滤器一样)。

类似这样的东西:

public static class FakeQueryProvider
{
    static Dictionary<Type, IQueryable> queries = new Dictionary<Type, IQueryable>();

    public static void SetQuery<T>(IQueryable<T> query)
    {
        lock (queries)
            queries[typeof(T)] = query;
    }

    public static IQueryable<T> GetQuery<T>()
    {
        lock (queries)
            return queries.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
    }

    public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder)
        where T : class
    { 
        return builder.ToQuery(() => GetQuery<T>());
    }
}

然后代替

.ToQuery(() => LINQ_query);

您将使用

.ToFakeQuery(); 

并像这样将其喂入测试

 List<MyQueryType> data = ...;
 FakeQueryProvider.SetQuery(data.AsQueryable());

由于共享存储限制了并行运行MyQueryType相关测试的能力,因此我仍然推荐第一种方法。

答案 1 :(得分:1)

我最终根据以下建议/建议重构了Ivan的扩展类代码。我在ToFakeQuery方法中添加了重载以引入字典。

public static class InMemoryQueryProviderExtensions
{
    static Dictionary<Type, IQueryable> queries = new Dictionary<Type, IQueryable>();

    public static void SetQuery<T>(IQueryable<T> query)
    {
        lock (queries)
            queries[typeof(T)] = query;
    }

    public static IQueryable<T> GetQuery<T>()
    {
        lock (queries)
            return queries.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
    }
    private static IQueryable<T> GetQuery<T>(Dictionary<Type, IQueryable> queryDictionary)
    {
        return queryDictionary.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
    }

    public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder)
        where T : class
    {
        return builder.ToQuery(() => GetQuery<T>());
    }
    public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder, Dictionary<Type, IQueryable> queryDictionary)
        where T : class
    {
        return builder.ToQuery(() => GetQuery<T>(queryDictionary));
    }
}

然后,如下所示为我的DBContext创建一个新的派生类。基本上,使内存DBContext的派生实例可以维护字典。

public class TlInMemoryDbContext : TlDbContext
{
    public TlInMemoryDbContext(DbContextOptions<TlDbContext> options)
        : base(options)
    { }

    Dictionary<Type, IQueryable> queries = new Dictionary<Type, IQueryable>();
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Query<EffectiveTimeEntry>().ToFakeQuery(queries);
    }

    public void SetQuery<T>(IQueryable<T> query)
    {
        lock (queries)
            queries[typeof(T)] = query;
    }
}

答案 2 :(得分:0)

我按照Ivan Stoev的建议使用了以上内容。

这是它的样子

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  if(Database.IsSqlServer())
  {
     modelBuilder.Query<ProviderRating>(entity =>
                {
                    entity.ToView("vGetProviderRatingData", "dbo");
                    entity.Property(e => e.col1)
                    .HasMaxLength(10)
                    .IsUnicode(false);
                    entity.Property(e => e.col2)
                    .HasMaxLength(60)
                    .IsUnicode(false);
                    entity.Property(e => e.col3)
                    .HasMaxLength(10)
                    .IsUnicode(false);

                });
  }
  else
  {
    modelBuilder.Query<ProviderRating>().ToQuery(() =>
                 ProviderRatingFake.Select(m => new ProviderRating()
                 {
                     col1 = m.col1,
                     col2 = m.col2,
                     col3 = m.col3,
                 }
                ));
  }
}

ProviderRatingFake类与ProviderRating类完全一样

我还将此代码添加到了DbContext文件(ProviderQualityContext

 public virtual DbSet<ProviderRatingFake> ProviderRatingFake { get; set; }

 public virtual DbQuery<ProviderRating> ProviderRating { get; set; }

然后我像这样测试

[TestMethod]
public void TestingWithInMemoryDb()
{
   var options = new DbContextOptionsBuilder<ProviderQualityContext>()
                .UseInMemoryDatabase(databaseName: "Read_From_Database")
                .Options;
  var fakeProviderRating = new ProviderRatingFake
            {
                col1 = 1,
                col2 = "Something",
                col3 = "Something",
            };
  using (var context = new ProviderQualityContext(options))
  {
    context.ProviderRatingFake.Add(fakeProviderRating);
    context.SaveChanges();
  }

  //use the newly created context and inject it into controller or repository
  using (var context = new ProviderQualityContext(options))
  {
    //use the test context here and make assertions that you are returning the
   //fake data
   //Note that the actual code uses the Query like this
   //This query will be populated with fake data using the else block
   //in the method OnModelCreating
   var returnedData = this.dbContext.Query<ProviderRating>().Where(m => m.col1 == 
   "Something")
  }

}

答案 3 :(得分:0)

public static class InMemoryQueryProviderExtensions
{
    static Dictionary<Type, IQueryable> queries = new Dictionary<Type, IQueryable>();

    public static void SetQuery<T>(IQueryable<T> query)
    {
        lock (queries)
            queries[typeof(T)] = query;
    }

    public static IQueryable<T> GetQuery<T>()
    {
        lock (queries)
            return queries.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
    }
    private static IQueryable<T> GetQuery<T>(Dictionary<Type, IQueryable> queryDictionary)
    {
        return queryDictionary.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
    }

    public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder)
        where T : class
    {
        return builder.ToQuery(() => GetQuery<T>());
    }
    public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder, Dictionary<Type, IQueryable> queryDictionary)
        where T : class
    {
        return builder.ToQuery(() => GetQuery<T>(queryDictionary));
    }
}