避免在实体类中使用方法定义进行查询的查询客户端评估错误

时间:2019-02-22 15:46:05

标签: c# sql-server entity-framework entity-framework-core lazy-evaluation

.NET Core 2.1 项目中,我在SQL Server数据库上使用带有命令模式的EF Core(使用 MediatR 库)。

我通过使用以下设置来设置项目以避免客户端查询评估:

var phaseOptions = new DbContextOptionsBuilder<PhaseDbContext>().UseSqlServer(configuration.GetConnectionString("PhaseDbContext"),
        sqlServerOptions => sqlServerOptions
            .EnableRetryOnFailure(
                maxRetryCount: 5,
                maxRetryDelay: TimeSpan.FromSeconds(30),
                errorNumbersToAdd: null))
    .ConfigureWarnings(warnings => warnings
        .Throw(RelationalEventId.QueryClientEvaluationWarning)) // Disable Client query evaluation
    .Options;

现在我得到一个QueryClientEvaluationException的查询:

var articleCodes = await PhaseContext.PhaseArticles
    .Where(a => !request.ArticleFamily.HasValue || a.GetArticleFamily() == request.ArticleFamily.Value)
    .ToListAsync(cancellationToken);

问题出在a.GetArticleFamily()方法调用上,因为该方法现在在PhaseArticle实体类中的定义如下:

public class PhaseArticle
{
    public int Id { get; set; }
    public string Code { get; set; }
    public string Description { get; set; }
    public string UnitOfMeasure { get; set; }
    public string Category { get; set; }
    public string Group { get; set; }
    public string Family { get; set; }
    public double UnitCost { get; set; }
    public string AdditionalDescription { get; set; }
    public string ExternalCode { get; set;}
    public string ColorCode { get; set;}
    public string Note { get; set; }

    public ArticleFamily GetArticleFamily()
    {
        switch (Family)
        {
            case "CEL":
                return ArticleFamily.Cell;
            case "STR":
                return ArticleFamily.String;
            case "RAW":
                return ArticleFamily.OtherRawMaterial;
            case "SFP":
                return ArticleFamily.SemiFinishedPanel;
            case "FP":
                return ArticleFamily.FinishedPanel;
            default:
                return ArticleFamily.Other;
        }
    }
}

现在,我想知道是否可以通过某种方式重构GetArticleFamily()方法(并可能远离实体类)来保留QueryClientEvaluationWarning选项。

更新2019/02/26

@StriplingWarrior我再次更新了代码,其中包含您对ValueConverter()的建议,但现在却出现此错误:

  

无法将Lambda表达式转换为表达式树。

更新2019/02/25

按照@StriplingWarrior的建议,我正在尝试编写一个自定义转换器,但无法使我的代码编译。

以下代码的错误与第一个switch块(为string,但预计为enum)的返回值有关,并且关于第二个开关块的预期输入值(是string,但应该是enum)。

这是代码:

public static void ApplyPhaseConversions<T>(this ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<PhaseArticle>()
        .Property(e => e.Family)
        .HasConversion(new ValueConverter<ArticleFamily, string> {
            v =>
            {
                switch (v)
                {
                    case ArticleFamily.Cell:
                        return "CEL";
                    case ArticleFamily.String:
                        return "STR";
                    case ArticleFamily.OtherRawMaterial:
                        return "RAW";
                    case ArticleFamily.SemiFinishedPanel:
                        return "SFP";
                    case ArticleFamily.FinishedPanel:
                        return "FP";
                    default:
                        return "";
                }
            },
            v =>
            {
                switch (v)
                {
                    case "CEL":
                        return ArticleFamily.Cell;
                    case "STR":
                        return ArticleFamily.String;
                    case "RAW":
                        return ArticleFamily.OtherRawMaterial;
                    case "SFP":
                        return ArticleFamily.SemiFinishedPanel;
                    case "FP":
                        return ArticleFamily.FinishedPanel;
                    default:
                        return ArticleFamily.Other;
                }
            }});
}

3 个答案:

答案 0 :(得分:2)

似乎您正在使用GetArticleFamily()在数据库值和C#枚举之间进行转换。 EF Core具有称为“价值转换”的内置功能,旨在解决此问题:https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions

您应该能够定义一个ValueConverter来与ArticleFamily值进行相互转换,然后将Family属性的类型更改为ArticleFamily,并在查询中使用该属性:

var articleCodes = await PhaseContext.PhaseArticles
    .Where(a => !request.ArticleFamily.HasValue || a.Family == request.ArticleFamily.Value)
    .ToListAsync(cancellationToken);

PS--我不确定上面的代码会产生哪种查询,但是最好这样编写查询:

var articleQuery = PhaseContext.PhaseArticles.AsQueryable();
if(request.ArticleFamily.HasValue)
{
    articleQuery = articleQuery.Where(a => a.Family == request.ArticleFamily.Value);
}
var articleCodes = await articleQuery.ToListAsync(cancellationToken);

答案 1 :(得分:1)

您可以创建一个新变量并传输request.ArticleFamily.Value结果,以便它可以返回ArticleFamily.Cell或ArticleFamily.String,然后运行查询

例如

if(request != null && !request.ArticleFamily.HasValue)
// or throw an exception here
 return ...;

ArticleFamily newVariable = (ArticleFamily)Enum.Parse(typeof(ArticleFamily), request.ArticleFamily);
var articleCodes = await PhaseContext.PhaseArticles
    .Where(a => a.Family == newVariable)
    .ToListAsync(cancellationToken);

应该在运行查询之前完成方法参数的验证。另一件事是,如果请求为null,会发生什么?

修改

还需要验证request对象。在发送到API的JSON对象的结构中可能会出现输入错误或输入错误(定义了字段的值后忘记添加逗号)。在这种情况下,请求对象将具有一个null值,因此需要验证这种行为。例如。您可以添加

if (!ModelState.IsValid)
{
   return BadRequest(ModelState);
}

在控制器的操作中验证整个请求主体。客户端将收到正确的错误消息。

答案 2 :(得分:0)

最后,就像@StriplingWarrior所说的,解决方案几乎就在那里。

由于C#编译器的局限性,例如不能为该代码创建表达式树,因此解决方案是将转换代码转换为方法,然后在HasConversion中调用它们。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<PhaseArticle>()
        .Property(e => e.Family)
        .HasConversion(new ValueConverter<ArticleFamily, string>(
            v => StringFromArticleFamily(v),
            v => ArticleFamilyFromString(v));
}

private static ArticleFamily ArticleFamilyFromString(string family)
{
    switch (family)
    {
        case "CEL":
            return ArticleFamily.Cell;
        case "STR":
            return ArticleFamily.String;
        case "RAW":
            return ArticleFamily.OtherRawMaterial;
        case "SFP":
            return ArticleFamily.SemiFinishedPanel;
        case "FP":
            return ArticleFamily.FinishedPanel;
        default:
            return ArticleFamily.Other;
    }
}

private static string StringFromArticleFamily(ArticleFamily articleFamily)
{
    switch (articleFamily)
    {
        case ArticleFamily.Cell:
            return "CEL";
        case ArticleFamily.String:
            return "STR";
        case ArticleFamily.OtherRawMaterial:
            return "RAW";
        case ArticleFamily.SemiFinishedPanel:
            return "SFP";
        case ArticleFamily.FinishedPanel:
            return "FP";
        default:
            return "";
    }
}