虽然我尝试使用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
对象的定义如下:
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
};
}
}
第一个查询按预期查询所有列,第二个查询带有匿名对象,仅查询所选查询,一切正常。只要直接在Select方法中创建DTO,就可以使用我的MinimalItem
DTO。但是,尽管后两个查询的功能与第三个查询的功能完全相同,但最后两个查询会获取所有列,只是分别移至了构造函数或扩展方法。
显然,如果我将其移出Select方法,EF Core将无法遵循此代码并确定仅需要两列。但是我真的很想这样做,以便能够重用映射代码,并使实际的查询代码更易于阅读。如何在不使EF Core始终无效率地获取所有列的情况下提取这种简单的映射代码?
这从一开始就是IQueryable
的基本问题,经过这么多年没有开箱即用的解决方案。
问题是IQueryable
转换和代码封装/可重用性是互斥的。 IQueryable
转换是基于事先的知识,这意味着查询处理器必须能够“查看”实际的代码,然后转换“已知”的方法/属性。但是自定义方法/可计算属性的内容在运行时不可见,因此查询处理器通常会失败,或者在有限的情况下它们支持“客户端评估”(EF Core仅针对最终预测),它们生成的效率低下的翻译会检索很多内容比示例中所需的数据更多。
总而言之,C#编译器和BCL都无法解决这一“核心问题”。一些第三方库正在尝试以不同程度的等级解决它-LinqKit,NeinLinq和类似的等级。它们的问题在于,除了调用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 }; } }
((注意:它支持其他方法来告诉您要反编译的方法,例如特定类的所有方法/属性等。)
和现在
ctx.Items.Decompile() .Select(x => x.MapToMinimalItem()) .ToList();
产生
// SELECT i."Id", i."Property1" FROM "Items" AS i
[此方法(和其他第三方库)的唯一问题是,需要调用自定义扩展方法
Decompile
,以便使用自定义提供程序包装可查询对象,以便能够预处理最终查询表达式。
如果EF Core允许在其LINQ查询处理管道中插入自定义查询表达式预处理器,那么将很容易被遗忘,并且消除了在每个查询中调用自定义方法的需求,而且自定义查询提供程序的运行也不尽人意。具有EF Core特定扩展名,例如AsTracking
,AsNoTracking
,Include
/ ThenInclude
,因此应将其真正称为after
[当前存在一个未解决的问题Please open the query translation pipeline for extension #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); } }
您不需要了解该代码。它的大部分(如果不是全部的话)是样板管道代码,以支持当前缺少的
IQueryPreprocessor
和AddQueryPreprocesor
(类似于最近添加的拦截器)。如果将来有EF Core添加该功能,我将对其进行更新。
现在您可以使用它将DelegateDecompiler
插入EF 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); } }
很多代码只能调用
DecompileExpressionVisitor.Decompile(query)
在进行EF Core处理之前,但现在您只需要致电
optionsBuilder.AddDelegateDecompiler();
在您的派生上下文中,
OnConfiguring
被覆盖,并且您所有的EF Core LINQ查询都将经过预处理并反编译后插入。
有您的例子
ctx.Items.Select(x => x.MapToMinimalItem())
将自动转换为
ctx.Items.Select(x => new { Id = x.Id, Property1 = x.Property1 }
因此被EF Core翻译为
// SELECT i."Id", i."Property1" FROM "Items" AS I
这是目标。
此外,合成投影也有效,因此以下查询
ctx.Items .Select(x => x.MapToMinimalItem()) .Where(x => x.Property1 == "abc") .ToList();
本来会生成运行时异常,但现在可以转换并成功运行。
[Entity Framework]对您的MapToMinimalItem
方法以及如何将其转换为SQL一无所知,因此它获取整个实体并在客户端执行Select
。