EF核心。分离式懒加载导航属性

问题描述 投票:1回答:1

我有以下查询。

        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))

PrimaryId 定义为 QuestOrder 作为。

    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: 尝试在类型为''的分离实体上懒加载导航属性'DlStateProvince'。对于分离的实体或使用'AsNoTracking()'加载的实体,不支持懒加载。 可以通过将事件ID'CoreEventId.DetachedLazyLoadingWarning'传递给'DbContext.OnConfiguring'或'AddDbContext'中的'ConfigureWarnings'方法来抑制或记录这个异常。

如何解决这个问题?

c# entity-framework-core lazy-loading ef-core-2.2
1个回答
3
投票

这个问题是由 计算的 QuestOrder.PrimaryId 属性。

当在LINQ to Entities查询中使用时,这类属性无法转化为SQL,需要客户端评估。而且即使支持,客户端评估在访问里面的导航属性时也不能很好的发挥--无论是急切加载还是懒惰加载都不能正常工作,会导致运行时异常或者错误的返回值。

所以最好的办法是让它们可翻译,这就需要处理可翻译的 表情.

在所有情况下,首先将计算出的属性体从块转换为条件操作符(使其可翻译)。

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

现在,简短的转折,快速而简单的解决方案是提取计算属性的实际表达式,将其复制粘贴到映射中,并替换为: thissrc.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
))

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

无论是C#,还是BCL或EF Core在这方面都没有帮助。有几个第三方包正在尝试在某种程度上解决这个问题----。LinqKit, NeinLinq 等,但有一个不太为人所知的小宝贝,叫 语音识别器(DeproateDecompiler 它只需修改最少的代码即可实现。

您所需要的只是安装 语音识别器(DeproateDecompilerDelegateDecompiler.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),你甚至可以不需要调用 Decompile DecompileAsync 通过提供以下两个库之间的小`"桥梁"。

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;
            }
        }
    }
}

然后直接调用 UseDecompiler() 在AutoMapper初始化过程中,如

var mapperConfig = new MapperConfiguration(config =>
{
    config.UseDecompiler(); // <--
    // the rest (add profiles, create maps etc.) ...
});
© www.soinside.com 2019 - 2024. All rights reserved.