EF Core 映射到 Select 中的对象时查询 SQL 中的所有列

Mad*_*ist 11 c# linq entity-framework-core .net-core

在尝试使用 EF Core 组织一些数据访问代码时,我注意到生成的查询比以前更糟糕,它们现在查询不需要的列。基本查询只是从一个表中选择并将列的子集映射到 DTO。但是在重写它之后,现在所有的列都被提取了,而不仅仅是 DTO 中的那些。

我创建了一个最小示例,其中包含一些显示问题的查询:

ctx.Items.ToList();
// SELECT i."Id", i."Property1", i."Property2", i."Property3" FROM "Items" AS i


ctx.Items.Select(x => new
{
  Id = x.Id,
  Property1 = x.Property1
}
).ToList();
// SELECT i."Id", i."Property1" FROM "Items" AS i

ctx.Items.Select(x => new MinimalItem
{
  Id = x.Id,
  Property1 = x.Property1
}
).ToList();
// SELECT i."Id", i."Property1" FROM "Items" AS i

ctx.Items.Select(
  x => x.MapToMinimalItem()
).ToList();
// SELECT i."Id", i."Property1", i."Property2", i."Property3" FROM "Items" AS i

ctx.Items.Select(
  x => new MinimalItem(x)
).ToList();

// SELECT i."Id", i."Property1", i."Property2", i."Property3" FROM "Items" AS i
Run Code Online (Sandbox Code Playgroud)

对象定义如下:

  public class Item
  {
    public int Id { get; set; }
    public string Property1 { get; set; }
    public string Property2 { get; set; }
    public string Property3 { get; set; }

  }

  public class MinimalItem
  {
    public MinimalItem() { }

    public MinimalItem(Item source)
    {
      Id = source.Id;
      Property1 = source.Property1;
    }
    public int Id { get; set; }
    public string Property1 { get; set; }
  }

  public static class ItemExtensionMethods
  {
    public static MinimalItem MapToMinimalItem(this Item source)
    {
      return new MinimalItem
      {
        Id = source.Id,
        Property1 = source.Property1
      };
    }
  }
Run Code Online (Sandbox Code Playgroud)

第一个查询按预期查询所有列,使用匿名对象的第二个查询仅查询选定的查询,这一切正常。MinimalItem只要直接在 Select 方法中创建,使用我的DTO 也可以使用。但是最后两个查询获取所有列,即使它们与第三个查询完全相同,只是分别移动到构造函数或扩展方法。

显然 EF Core 无法遵循此代码并确定如果我将其移出 Select 方法,它只需要两列。但我真的很想这样做,以便能够重用映射代码,并使实际查询代码更易于阅读。如何提取这种简单的映射代码而不会使 EF Core 一直低效地获取所有列?

Iva*_*oev 13

这是IQueryable从一开始就存在的根本问题,这么多年后没有现成的解决方案。

问题在于IQueryable翻译和代码封装/可重用性是相互排斥的。IQueryable翻译是基于事先的知识,这意味着查询处理器必须能够“看到”实际代码,然后翻译“已知”的方法/属性。但是自定义方法/可计算属性的内容在运行时不可见,因此查询处理器通常会失败,或者在支持“客户端评估”的有限情况下(EF Core 仅对最终预测执行此操作),它们会生成低效的翻译,从而检索很多比您的示例中需要的数据多。

回顾一下,C# 编译器和 BCL 都没有帮助解决这个“核心问题”。一些 3rd 方库正试图在不同程度上解决它 - LinqKitNeinLinq和类似的。他们的问题是,它们需要额外重构现有的代码调用一个特殊的方法一样AsExpandable()ToInjectable()等等。

最近我发现了一个名为DelegateDecompiler的小宝石,它使用另一个名为Mono.Reflection.Core 的包将方法体反编译为其 lambda 表示。

使用它很容易。安装后,您只需要使用自定义提供的[Computed][Decompile]属性标记您的自定义方法/计算属性(只需确保使用表达式样式实现而不是代码块),并在链中的某处调用Decompile()DecompileAsync()自定义扩展方法IQueryable。它不适用于构造函数,但支持所有其他构造。

例如,以您的扩展方法为例:

public static class ItemExtensionMethods
{
    [Decompile] // <--
    public static MinimalItem MapToMinimalItem(this Item source)
    {
        return new MinimalItem
        {
            Id = source.Id,
            Property1 = source.Property1
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

(注意:它支持告诉反编译哪些方法的其他方式,例如特定类的所有方法/属性等)

现在

ctx.Items.Decompile()
    .Select(x => x.MapToMinimalItem())
    .ToList();
Run Code Online (Sandbox Code Playgroud)

产生

// SELECT i."Id", i."Property1" FROM "Items" AS i
Run Code Online (Sandbox Code Playgroud)

这种方法(和其他 3rd 方库)的唯一问题是需要调用自定义扩展方法Decompile,以便使用自定义提供程序包装可查询,以便能够预处理最终查询表达式。

如果 EF Core 允许在其 LINQ 查询处理管道中插入自定义查询表达式预处理器,从而消除在每个查询中调用自定义方法的需要,这很容易被遗忘,并且自定义查询提供程序不能很好地与 EF 配合使用,那就太好了核心特定扩展如AsTracking, AsNoTracking, Include/ ThenInclude,所以它应该它们之后调用等。

目前有一个未解决的问题请打开扩展#19748 的查询翻译管道,我试图说服团队添加一种简单的方法来添加表达式预处理器。您可以阅读讨论并投票。

在此之前,这是我对 EF Core 3.1 的解决方案:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.EntityFrameworkCore
{
    public static partial class CustomDbContextOptionsExtensions
    {
        public static DbContextOptionsBuilder AddQueryPreprocessor(this DbContextOptionsBuilder optionsBuilder, IQueryPreprocessor processor)
        {
            var option = optionsBuilder.Options.FindExtension<CustomOptionsExtension>()?.Clone() ?? new CustomOptionsExtension();
            if (option.Processors.Count == 0)
                optionsBuilder.ReplaceService<IQueryTranslationPreprocessorFactory, CustomQueryTranslationPreprocessorFactory>();
            else
                option.Processors.Remove(processor);
            option.Processors.Add(processor);
            ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(option);
            return optionsBuilder;
        }
    }
}

namespace Microsoft.EntityFrameworkCore.Infrastructure
{
    public class CustomOptionsExtension : IDbContextOptionsExtension
    {
        public CustomOptionsExtension() { }
        private CustomOptionsExtension(CustomOptionsExtension copyFrom) => Processors = copyFrom.Processors.ToList();
        public CustomOptionsExtension Clone() => new CustomOptionsExtension(this);
        public List<IQueryPreprocessor> Processors { get; } = new List<IQueryPreprocessor>();
        ExtensionInfo info;
        public DbContextOptionsExtensionInfo Info => info ?? (info = new ExtensionInfo(this));
        public void Validate(IDbContextOptions options) { }
        public void ApplyServices(IServiceCollection services)
            => services.AddSingleton<IEnumerable<IQueryPreprocessor>>(Processors);
        private sealed class ExtensionInfo : DbContextOptionsExtensionInfo
        {
            public ExtensionInfo(CustomOptionsExtension extension) : base(extension) { }
            new private CustomOptionsExtension Extension => (CustomOptionsExtension)base.Extension;
            public override bool IsDatabaseProvider => false;
            public override string LogFragment => string.Empty;
            public override void PopulateDebugInfo(IDictionary<string, string> debugInfo) { }
            public override long GetServiceProviderHashCode() => Extension.Processors.Count;
        }
    }
}

namespace Microsoft.EntityFrameworkCore.Query
{
    public interface IQueryPreprocessor
    {
        Expression Process(Expression query);
    }

    public class CustomQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor
    {
        public CustomQueryTranslationPreprocessor(QueryTranslationPreprocessorDependencies dependencies, RelationalQueryTranslationPreprocessorDependencies relationalDependencies, IEnumerable<IQueryPreprocessor> processors, QueryCompilationContext queryCompilationContext)
            : base(dependencies, relationalDependencies, queryCompilationContext) => Processors = processors;
        protected IEnumerable<IQueryPreprocessor> Processors { get; }
        public override Expression Process(Expression query)
        {
            foreach (var processor in Processors)
                query = processor.Process(query);
            return base.Process(query);
        }
    }

    public class CustomQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
    {
        public CustomQueryTranslationPreprocessorFactory(QueryTranslationPreprocessorDependencies dependencies, RelationalQueryTranslationPreprocessorDependencies relationalDependencies, IEnumerable<IQueryPreprocessor> processors)
        {
            Dependencies = dependencies;
            RelationalDependencies = relationalDependencies;
            Processors = processors;
        }
        protected QueryTranslationPreprocessorDependencies Dependencies { get; }
        protected RelationalQueryTranslationPreprocessorDependencies RelationalDependencies { get; }
        protected IEnumerable<IQueryPreprocessor> Processors { get; }
        public QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
            => new CustomQueryTranslationPreprocessor(Dependencies, RelationalDependencies, Processors, queryCompilationContext);
    }
}
Run Code Online (Sandbox Code Playgroud)

您不需要理解该代码。其中大部分(如果不是全部)是样板管道代码,以支持当前缺少的IQueryPreprocessorAddQueryPreprocesor(类似于最近添加的拦截器)。如果 EF Core 将来添加该功能,我会更新它。

现在您可以使用它来插入DelegateDecompilerEF Core:

using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query;
using DelegateDecompiler;

namespace Microsoft.EntityFrameworkCore
{
    public static class DelegateDecompilerDbContextOptionsExtensions
    {
        public static DbContextOptionsBuilder AddDelegateDecompiler(this DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder.AddQueryPreprocessor(new DelegateDecompilerQueryPreprocessor());
    }
}

namespace Microsoft.EntityFrameworkCore.Query
{
    public class DelegateDecompilerQueryPreprocessor : IQueryPreprocessor
    {
        public Expression Process(Expression query) => DecompileExpressionVisitor.Decompile(query);
    }
}
Run Code Online (Sandbox Code Playgroud)

很多代码只是为了能够调用

DecompileExpressionVisitor.Decompile(query)
Run Code Online (Sandbox Code Playgroud)

在 EF Core 处理之前,但现在您只需要调用

optionsBuilder.AddDelegateDecompiler();
Run Code Online (Sandbox Code Playgroud)

在您派生的上下文OnConfiguring覆盖中,您的所有 EF Core LINQ 查询都将被预处理并注入反编译体。

用你的例子

ctx.Items.Select(x => x.MapToMinimalItem())
Run Code Online (Sandbox Code Playgroud)

会自动转换为

ctx.Items.Select(x => new
{
    Id = x.Id,
    Property1 = x.Property1
}
Run Code Online (Sandbox Code Playgroud)

因此由 EF Core 翻译为

// SELECT i."Id", i."Property1" FROM "Items" AS I
Run Code Online (Sandbox Code Playgroud)

这是目标。

此外,组合投影也有效,因此以下查询

ctx.Items
    .Select(x => x.MapToMinimalItem())
    .Where(x => x.Property1 == "abc")
    .ToList();
Run Code Online (Sandbox Code Playgroud)

最初会生成运行时异常,但现在转换并成功运行。