EF Core:分离延迟加载导航 属性

EF Core: Detached lazy-loading navigation property

我有以下查询:

        var query = _context.QuestOrders.Include(a => a.Driver).ThenInclude(i => i.DlStateProvince)
            .Where(p => carrierIds.Contains(p.Driver.CarrierId))
            ....
            ;

然后尝试调用以下内容:

        var queryDto = query.AsNoTracking().ProjectTo<DcReportDonorResultDto>(_mapperConfiguration);
        var reports = new PagedList<DcReportDonorResultDto>(queryDto, pageIndex, pageSize);

其中 DcReportDonorResultDto 有一个 属性:

public string PrimaryId { get; set; }

映射了以下内容:

        CreateMap<QuestOrder, DcReportDonorResultDto>()
            .ForMember(destinationMember => destinationMember.PrimaryId, opt => opt.MapFrom(src => src.Driver.PrimaryId))

PrimaryIdQuestOrder中定义为:

    public string PrimaryId
    {
        get
        {
            if (!string.IsNullOrWhiteSpace(DlNumber) && DlStateProvinceId.HasValue)
                return DlStateProvince.Abbreviation + DlNumber.Replace("-", "");
            else
                return string.Empty;
        }
    }

我收到以下错误:

System.InvalidOperationException: 'Error generated for warning 'Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning: An attempt was made to lazy-load navigation property 'DlStateProvince' on detached entity of type ''. Lazy-loading is not supported for detached entities or entities that are loaded with 'AsNoTracking()'.'. This exception can be suppressed or logged by passing event ID 'CoreEventId.DetachedLazyLoadingWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.'

如何解决这个问题?

问题是由计算 QuestOrder.PrimaryId 属性.

引起的

在 LINQ to Entities 查询中使用时,此类属性无法转换为 SQL 并且需要客户端评估。即使在支持的情况下,客户端评估在访问内部导航属性时也无法正常运行 - 急切或延迟加载都无法正常运行并导致运行时异常或错误的 return 值。

所以最好的办法是使它们可翻译,这需要处理可翻译的表达式

在所有情况下,首先将计算的 属性 正文从块转换为条件运算符(使其可翻译):

public string PrimaryId =>
    !string.IsNullOrWhiteSpace(this.DlNumber) && this.DlStateProvinceId.HasValue) ?
    this.DlStateProvince.Abbreviation + this.DlNumber.Replace("-", "") :
    string.Empty; 

现在,快速而简单的解决方案是提取映射中计算的 属性、copy/paste 的实际表达式,并将 this 替换为 src.Driver:

.ForMember(dst => dst.PrimaryId, opt => opt.MapFrom(src => 
    //src.Driver.PrimaryId
    !string.IsNullOrWhiteSpace(src.Driver.DlNumber) && src.Driver.DlStateProvinceId.HasValue) ?
    src.Driver.DlStateProvince.Abbreviation + src.Driver.DlNumber.Replace("-", "") :
    string.Empty
))

从长远来看,或者如果您有很多这样的属性,或者您需要在其他 mappings/queries 中使用它,或者只是因为代码重复,这不是一个好的解决方案。您需要一种方法来用从正文中提取的相应表达式替换查询表达式树中计算的 属性 访问器。

在这方面,C#、BCL 或 EF Core 都没有帮助。几个第 3 方软件包正试图在某种程度上解决这个问题 - LinqKit, NeinLinq etc., but there is a little not very well known gem called DelegateDecompiler 以最少的代码更改做到这一点。

您只需要安装 DelegateDecompiler or DelegateDecompiler.EntityFrameworkCore 包,用 [Computed] 属性标记您的计算属性

[Computed] // <--
public string PrimaryId =>
    !string.IsNullOrWhiteSpace(this.DlNumber) && this.DlStateProvinceId.HasValue) ?
    this.DlStateProvince.Abbreviation + this.DlNumber.Replace("-", "") :
    string.Empty; 

然后在可查询的顶层调用Decompile(或DecompileAsync

var queryDto = query.AsNoTracking()
    .ProjectTo<DcReportDonorResultDto>(_mapperConfiguration)
    .Decompile(); // <--

AutoMapper 不需要特殊映射,例如你可以保持平常

.ForMember(dst => dst.PrimaryId, opt => opt.MapFrom(src => src.Driver.PrimaryId)

对于 AutoMapper 投影查询(使用 ProjectTo 生成),您甚至可以通过在两个图书馆:

namespace AutoMapper
{
    using DelegateDecompiler;
    using QueryableExtensions;

    public static class AutoMapperExtensions
    {
        public static IMapperConfigurationExpression UseDecompiler(this IMapperConfigurationExpression config)
        {
            var resultConverters = config.Advanced.QueryableResultConverters;
            for (int i = 0; i < resultConverters.Count; i++)
            {
                if (!(resultConverters[i] is ExpressionResultDecompiler))
                    resultConverters[i] = new ExpressionResultDecompiler(resultConverters[i]);
            }
            return config;
        }

        class ExpressionResultDecompiler : IExpressionResultConverter
        {
            IExpressionResultConverter baseConverter;
            public ExpressionResultDecompiler(IExpressionResultConverter baseConverter) => this.baseConverter = baseConverter;
            public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, PropertyMap propertyMap) => baseConverter.CanGetExpressionResolutionResult(expressionResolutionResult, propertyMap);
            public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, ConstructorParameterMap propertyMap) => baseConverter.CanGetExpressionResolutionResult(expressionResolutionResult, propertyMap);
            public ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, PropertyMap propertyMap, LetPropertyMaps letPropertyMaps) => Decompile(baseConverter.GetExpressionResolutionResult(expressionResolutionResult, propertyMap, letPropertyMaps));
            public ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, ConstructorParameterMap propertyMap) => Decompile(baseConverter.GetExpressionResolutionResult(expressionResolutionResult, propertyMap));
            static ExpressionResolutionResult Decompile(ExpressionResolutionResult result)
            {
                var decompiled = DecompileExpressionVisitor.Decompile(result.ResolutionExpression);
                if (decompiled != result.ResolutionExpression)
                    result = new ExpressionResolutionResult(decompiled, result.Type);
                return result;
            }
        }
    }
}

并且只需在 AutoMapper 初始化期间调用 UseDecompiler(),例如

var mapperConfig = new MapperConfiguration(config =>
{
    config.UseDecompiler(); // <--
    // the rest (add profiles, create maps etc.) ...
});