如何改造基于非通用的 EF Core 代码以使用通用 IQueryable

Posted

技术标签:

【中文标题】如何改造基于非通用的 EF Core 代码以使用通用 IQueryable【英文标题】:How to retrofit Non Generic based EF Core code to use Generic IQueryable 【发布时间】:2022-01-17 08:13:41 【问题描述】:

我正在将旧的 EF 解决方案更新到 EF Core,我想删除大部分不使用泛型 (T) 的代码,但我必须分阶段进行。原始代码经历了 10 多年学习实体框架的有机增长,并且混合了“哲学”和代码中的模式,例如从 Microsoft 到 Repository 模式的教程。

理想的解决方案是允许我为特定实体建立查询,然后打开连接并运行“ToList()”。然后我可以将部分代码分阶段移动到更干净的地方。

//logic to figure out what is needed with no connection or dbconnection yet
public void GetOrder()
     var query = new List<Order>().AsQueryable();
     query = query.Where(x => x.Id > 100);
     var orders = repo.GetItems<Order>(query);


public IEnumerable<T> GetItems<T>(IQueryable<T> query, int page = 0, int maxcount = 0)
        
            using (MyEntities context = new MyEntities(getConnectionString()))
            
                context.Attach(query); //This does not seem to work
                if (page > 1)
                    query = query.Skip(page * maxcount);
                if (maxcount > 0)
                    query = query.Take(maxcount);

                return query.ToList();
            
        


在原始旧代码中,EF 存储库在代码/请求构造函数中初始化实体,然后调用上述方法 GetItems。看起来正在对数据库进行两个不同的连接,并且正在发生两个不同的实体初始化以向数据库发出 1 个请求。

我正在使用 EF Core 6 和 .NET 6(Core 6)。

【问题讨论】:

EF Core 不应打开连接,直到您实现查询(即运行 ToList 或任何其他“模拟”)。 EF Core 根据定义是通用的。 DbSet 是通用的并实现IQueryable&lt;T&gt;。你想做什么?实现分页?为此,您甚至不需要对 DbContext 的引用。你为什么要使用context.Attach(query);?这用于附加数据类,而不是执行查询 代码使用“通用存储库”anti-模式(引号和小写有意)是真正的问题,因此控制器无法使用IQueryable&lt;T&gt; 或@987654326 @ 可能?即便如此,无论如何实现存储库,获取 DbContext 实例都是的工作。顺便说一句,DbSet 已经实现了存储库。 DbContext 实现工作单元 【参考方案1】:

您使用context.Set&lt;T&gt;() 获取DbSet&lt;T&gt;(已经是IQueryable&lt;T&gt;)。

context.Attach 将实体附加到上下文的更改跟踪器,这不是您想要的。

所以你想要这样的东西:

public void GetOrders()
    List<Order> orders;
    using(MyEntities context = new MyEntities(getConnectionString()))
    
       var query = new List<Order>().AsQueryable();
       orders = context.GetItems<Order>().Where(x => x.Id > 100).ToList();    
    
    // Do whatever with "orders", which is already materialized and doesn't need the dbcontext instance anymore



public static class DbContextExtensions

    // On some extension static class
    public static IQueryable<T> GetItems<T>(this MyEntities context, int page = 0, int maxcount = 0)
    where T: class
    
        var query = context.Set<T>().AsQueryable();
        if (page > 1)
            query = query.Skip(page * maxcount);
        if (maxcount > 0)
            query = query.Take(maxcount);

        return query;
    

请注意,这与您现在应该使用 dbcontext 的方式不同(有依赖注入、异步等),但我基于您在问题上发布的代码

【讨论】:

【参考方案2】:

您可以构建Func&lt;IQueryable&lt;T&gt;, IQueryable&lt;T&gt;&gt; 并使用context.Set&lt;T&gt;() 方法。类似这样的事情:

public void GetOrder()
     Func<IQueryable<Order>, IQueryable<Order>> queryProvider = 
     query => query.Where(x => x.Id > 100);
     var orders = repo.GetItems<Order>(query);


public IEnumerable<T> GetItems<T>(Func<IQueryable<T>, IQueryable<T>> queryProvider, int page = 0, int maxcount = 0)

    using (MyEntities context = new MyEntities(getConnectionString()))
    
         var query = queryProvider(context.Set<T>()); 
         if (page > 1)
            query = query.Skip(page * maxcount);
         if (maxcount > 0)
            query = query.Take(maxcount);

         return query.ToList();
        
    

请注意,手动创建 DbContext 不再是惯用的方法,通常由 DI 处理。

另外我建议只创建一个Paginate 扩展方法。像这样的:

public static class QueryableExt

    public static IQueryable<T> Paginate<T>(this IQueryable<T> query, int page = 0, int maxcount = 0)
    
        if (page > 1)
            query = query.Skip(page * maxcount);
        if (maxcount > 0)
            query = query.Take(maxcount);
        return query;
    

【讨论】:

完全没有理由使用 DbContet。该分页方法(就是这样)可以简单地构建在顶部IQueryable&lt;&gt; @PanagiotisKanavos 是的,当然。只是添加代码。【参考方案3】:

代码的问题不是泛型 - 根据定义,EF Core 是泛型的。 DbSet&lt;T&gt;IQueryable&lt;T&gt; 是通用的。所有运算符都是通用的。

问题在于混淆了两件事。对 IQueryable 进行分页并创建特定的 IQueryable 实现。如果你把这两个分成不同的部分,问题就变得很简单了。

分页方式可以是:

public static IQueryable<T> GetPage<T>(this IQueryable<T> query,int page,int maxcount)

    if (page > 1)
        query = query.Skip(page * maxcount);
    if (maxcount > 0)
        query = query.Take(maxcount);
    return query;
 

这可以与任何IQueryable&lt;T&gt; 实现一起使用,无论是实现IEnumerable&lt;T&gt; 的容器(如List&lt;T&gt;)还是EF Core 查询:

var myList=new List<Order>();
...
var query=myList.AsQueryable()
                .Where(...)
                .GetPage(2,10);
var pageOrders=query.ToList();

或者

var query=context.Orders
                 .Where(...)
                 .GetPage(2,10);
var pageOrders=query.ToList();

没有理由创建GetItems 方法,因为它所做的只是创建一个特定 DbContext 并使用一个特定 DbSet。无需隐藏context.Orders 或创建context 的代码。事实上,在像 ASP.NET Core 这样的依赖注入环境中,DbContext 将由 DI 容器注入

【讨论】:

以上是关于如何改造基于非通用的 EF Core 代码以使用通用 IQueryable的主要内容,如果未能解决你的问题,请参考以下文章

如何首先将 EF Core 代码与 azure synapse 一起使用

非托管代码中的 ASP Net Core 1.1 和 EF 6 异常?

跨项目的 EF Core 数据上下文注入

在启动时访问 EF Core 数据库以设置依赖注入的常量值

如何编写此扩展方法以由 EF Core 转换为 SQL Server?

EF Core性能优化