【问题标题】:Multiple Includes() in EF CoreEF Core 中的多个 Includes()
【发布时间】:2017-08-11 18:41:59
【问题描述】:

我有一个扩展方法,可以让您在 EF 中一般包含数据:

public static IQueryable<T> IncludeMultiple<T>(this IQueryable<T> query, params Expression<Func<T, object>>[] includes)
    where T : class
{
    if (includes != null)
    {
        query = includes.Aggregate(query, (current, include) => current.Include(include));
    }
    return query;
}

这允许我在我的存储库中拥有这样的方法:

public Patient GetById(int id, params Expression<Func<Patient, object>>[] includes)
{
    return context.Patients
        .IncludeMultiple(includes)
        .FirstOrDefault(x => x.PatientId == id);
}

我相信扩展方法在 EF Core 之前有效,但现在包括“孩子”是这样完成的:

var blogs = context.Blogs
    .Include(blog => blog.Posts)
        .ThenInclude(post => post.Author);

有没有办法改变我的通用扩展方法以支持 EF Core 的新 ThenInclude() 实践?

【问题讨论】:

  • 只对数组中的第一个表达式执行current.Include(include),对其余的执行`current.ThenInclude(include)`?
  • @Evk 你将一个 Linq 表达式传递给这个扩展方法,所以输入会是这样的:x =&gt; x.Posts.Select(p =&gt; p.Author)
  • 另一种方法是解析传递的表达式并构建字符串包含路径,如 EF6 代码 - source
  • @IvanStoev 有趣的是,我编写的方法几乎与您链接中提供的方法完全相同。对我来说真的很愚蠢,不只是从源头上抓取它......
  • 所以 cmets 没有任何帮助(包括字符串或用于转换的 EF6 源代码)?

标签: c# entity-framework entity-framework-core


【解决方案1】:

正如其他人在comments 中所说,您可以使用EF6 code 解析您的表达式并应用相关的Include/ThenInclude 调用。毕竟它看起来并不难,但由于这不是我的想法,我宁愿不要用代码给出答案。

您可以改为更改您的模式以公开某些接口,允许您从调用者指定您的包含,而不让它访问底层可查询。

这将导致类似:

using YourProject.ExtensionNamespace;

// ...

patientRepository.GetById(0, ip => ip
    .Include(p => p.Addresses)
    .ThenInclude(a=> a.Country));

命名空间上的using 必须与包含在最后一个代码块中定义的扩展方法的命名空间名称匹配。

GetById 现在是:

public static Patient GetById(int id,
    Func<IIncludable<Patient>, IIncludable> includes)
{
    return context.Patients
        .IncludeMultiple(includes)
        .FirstOrDefault(x => x.EndDayID == id);
}

扩展方法IncludeMultiple

public static IQueryable<T> IncludeMultiple<T>(this IQueryable<T> query,
    Func<IIncludable<T>, IIncludable> includes)
    where T : class
{
    if (includes == null)
        return query;

    var includable = (Includable<T>)includes(new Includable<T>(query));
    return includable.Input;
}

Includable 类和接口,它们是简单的“占位符”,附加扩展方法将在其上完成模仿 EF IncludeThenInclude 方法的工作:

public interface IIncludable { }

public interface IIncludable<out TEntity> : IIncludable { }

public interface IIncludable<out TEntity, out TProperty> : IIncludable<TEntity> { }

internal class Includable<TEntity> : IIncludable<TEntity> where TEntity : class
{
    internal IQueryable<TEntity> Input { get; }

    internal Includable(IQueryable<TEntity> queryable)
    {
        // C# 7 syntax, just rewrite it "old style" if you do not have Visual Studio 2017
        Input = queryable ?? throw new ArgumentNullException(nameof(queryable));
    }
}

internal class Includable<TEntity, TProperty> :
    Includable<TEntity>, IIncludable<TEntity, TProperty>
    where TEntity : class
{
    internal IIncludableQueryable<TEntity, TProperty> IncludableInput { get; }

    internal Includable(IIncludableQueryable<TEntity, TProperty> queryable) :
        base(queryable)
    {
        IncludableInput = queryable;
    }
}

IIncludable 扩展方法:

using Microsoft.EntityFrameworkCore;

// others using ommitted

namespace YourProject.ExtensionNamespace
{
    public static class IncludableExtensions
    {
        public static IIncludable<TEntity, TProperty> Include<TEntity, TProperty>(
            this IIncludable<TEntity> includes,
            Expression<Func<TEntity, TProperty>> propertySelector)
            where TEntity : class
        {
            var result = ((Includable<TEntity>)includes).Input
                .Include(propertySelector);
            return new Includable<TEntity, TProperty>(result);
        }

        public static IIncludable<TEntity, TOtherProperty>
            ThenInclude<TEntity, TOtherProperty, TProperty>(
                this IIncludable<TEntity, TProperty> includes,
                Expression<Func<TProperty, TOtherProperty>> propertySelector)
            where TEntity : class
        {
            var result = ((Includable<TEntity, TProperty>)includes)
                .IncludableInput.ThenInclude(propertySelector);
            return new Includable<TEntity, TOtherProperty>(result);
        }

        public static IIncludable<TEntity, TOtherProperty>
            ThenInclude<TEntity, TOtherProperty, TProperty>(
                this IIncludable<TEntity, IEnumerable<TProperty>> includes,
                Expression<Func<TProperty, TOtherProperty>> propertySelector)
            where TEntity : class
        {
            var result = ((Includable<TEntity, IEnumerable<TProperty>>)includes)
                .IncludableInput.ThenInclude(propertySelector);
            return new Includable<TEntity, TOtherProperty>(result);
        }
    }
}

IIncludable&lt;TEntity, TProperty&gt; 几乎类似于 EF 中的 IIncludableQueryable&lt;TEntity, TProperty&gt;,但它不扩展 IQueryable 并且不允许重新调整查询。

当然,如果调用者在同一个程序集中,它仍然可以将IIncludable 转换为Includable 并开始摆弄可查询对象。但是好吧,如果有人想弄错,我们没有办法阻止他这样做(反射允许任何事情)。重要的是暴露的合同。

现在,如果您不关心将 IQueryable 暴露给调用者(我对此表示怀疑),显然只需将您的 params 参数更改为 Func&lt;Queryable&lt;T&gt;, Queryable&lt;T&gt;&gt; addIncludes 参数,并避免对上述所有内容进行编码。

最后最好:我没有测试过这个,我目前不使用实体框架!

【讨论】:

  • 您是否对此进行了全面测试?我无法让它工作。我认为有几个错别字...
  • 它可以编译(使用来自 EF.Core 的 IIncludableQueryable 的本地副本),但未经测试。如果您在 IIncludable 扩展方法中进行了编译,您是否需要 EF 提供的 using 才能让 EF 包含扩展方法?
  • 明白了。我已经实现了你所有的代码,但我无法使用 Include lambda 调用存储库。 Intellisense 没有提供对 Include... 的引用。
  • 你的意思是,你在定义我的上述扩展方法的文件中缺少using Microsoft.EntityFrameworkCore;,现在它可以工作了吗?
  • 或者您在包含代码的文件中丢失了在包含定义我上述扩展方法的类的命名空间上调用您的存储库 using
【解决方案2】:

对于后代来说,另一个不太雄辩但更简单的解决方案是利用 Include() 重载,它使用 navigationPropertyPath

public static class BlogIncludes
{
    public const string Posts = "Posts";
    public const string Author = "Posts.Author";
}

internal static class DataAccessExtensions
{
    internal static IQueryable<T> IncludeMultiple<T>(this IQueryable<T> query, 
        params string[] includes) where T : class
    {
        if (includes != null)
        {
            query = includes.Aggregate(query, (current, include) => current.Include(include));
        }
        return query;
    }
}

public Blog GetById(int ID, params string[] includes)
{
    var blog = context.Blogs
        .Where(x => x.BlogId == id)
        .IncludeMultiple(includes)
        .FirstOrDefault();
    return blog;
}

存储库调用是:

var blog = blogRepository.GetById(id, BlogIncludes.Posts, BlogIncludes.Author);

【讨论】:

    【解决方案3】:

    你可以这样做:

    public Patient GetById(int id, Func<IQueryable<Patient>, IIncludableQueryable<Patient, object>> includes = null)
            {
                IQueryable<Patient> queryable = context.Patients;
    
                if (includes != null)
                {
                    queryable = includes(queryable);
                }
    
                return  queryable.FirstOrDefault(x => x.PatientId == id);
            }
    
    var patient = GetById(1, includes: source => source.Include(x => x.Relationship1).ThenInclude(x => x.Relationship2));
    

    【讨论】:

      【解决方案4】:

      当然有,

      您可以遍历原始参数的表达式树,以及任何嵌套的包含,将它们添加为

       .Include(entity => entity.NavigationProperty)
       .ThenInclude(navigationProperty.NestedNavigationProperty)
      

      但它不是微不足道的,但绝对是非常可行的,如果你这样做,请分享,因为它肯定可以重复使用!

      【讨论】:

        【解决方案5】:
        public Task<List<TEntity>> GetAll()
            {
                var query = _Db.Set<TEntity>().AsQueryable();
                foreach (var property in _Db.Model.FindEntityType(typeof(TEntity)).GetNavigations())
                    query = query.Include(property.Name);
                return query.ToListAsync();
        
            }
        

        【讨论】:

          【解决方案6】:

          我坚持使用使用字符串 navigationPropertyPath 的 Include() 重载的更简单的解决方案。我能写的最简单的就是下面这个扩展方法。

          using Microsoft.EntityFrameworkCore;
          using System.Linq;
          
          namespace MGame.Data.Helpers
          {
              public static class IncludeBuilder
              {
                  public static IQueryable<TSource> Include<TSource>(this IQueryable<TSource> queryable, params string[] navigations) where TSource : class
                  {
                      if (navigations == null || navigations.Length == 0) return queryable;
          
                      return navigations.Aggregate(queryable, EntityFrameworkQueryableExtensions.Include);  // EntityFrameworkQueryableExtensions.Include method requires the constraint where TSource : class
                  }
              }
          }
          

          【讨论】:

            【解决方案7】:

            我用这个方法来做动态包含。这样,“Select”命令就可以在 lambda 中使用,就像过去一样包含在内。

            调用如下:

            repository.IncludeQuery(query, a => a.First.Second.Select(b => b.Third), a => a.Fourth);
            
            private IQueryable<TCall> IncludeQuery<TCall>(
                params Expression<Func<TCall, object>>[] includeProperties) where TCall : class
            {
                IQueryable<TCall> query;
            
                query = context.Set<TCall>();
            
                foreach (var property in includeProperties)
                {
                    if (!(property.Body is MethodCallExpression))
                        query = query.Include(property);
                    else
                    {
                        var expression = property.Body as MethodCallExpression;
            
                        var include = GenerateInclude(expression);
            
                        query = query.Include(include);
                    }
                } 
            
                return query;
            }
            
            private string GenerateInclude(MethodCallExpression expression)
            {
                var result = default(string);
            
                foreach (var argument in expression.Arguments)
                {
                    if (argument is MethodCallExpression)
                        result += GenerateInclude(argument as MethodCallExpression) + ".";
                    else if (argument is MemberExpression)
                        result += ((MemberExpression)argument).Member.Name + ".";
                    else if (argument is LambdaExpression)
                        result += ((MemberExpression)(argument as LambdaExpression).Body).Member.Name + ".";
                }
            
                return result.TrimEnd('.');
            } 
            

            【讨论】:

              【解决方案8】:
                  public TEntity GetByIdLoadFull(string id, List<string> navigatonProoperties)
                  {
                      if (id.isNullOrEmpty())
                      {
                          return null;
                      }
              
                      IQueryable<TEntity> query = dbSet;
              
                      if (navigationProperties != null)
                      {
                          foreach (var navigationProperty in navigationProperties)
                          {
                              query = query.Include(navigationProperty.Name);
                          }
                      }
              
                      return query.SingleOrDefault(x => x.Id == id);
                  }
              

              这是一个更简单的解决方案,想法是将 dbset 转换为 iqueryable,然后递归地包含属性

              【讨论】:

                猜你喜欢
                • 2021-11-23
                • 1970-01-01
                • 2019-02-20
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2020-11-19
                • 2020-07-14
                相关资源
                最近更新 更多