在实体 class 中使用方法定义的查询避免查询客户端评估错误
Avoid Query Client Evaluation error on a query with method definition inside entity class
在 .NET Core 2.1 项目中,我在 MediatR 库中使用带有命令模式的 EF Core =59=]服务器数据库。
我通过使用这些设置来设置项目以避免客户端查询评估:
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
实体中 class:
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;
}
}
}
现在,我想知道是否可以通过某种方式重构(并且可能远离实体 class)GetArticleFamily() 方法来保留 QueryClientEvaluationWarning
选项。
更新 2019/02/26
@StriplingWarrior 我已经根据你关于 ValueConverter()
的建议再次更新了代码,但现在它给出了这个错误:
Cannot convert Lambda expression into a tree of expressions.
更新 2019/02/25
按照@StriplingWarrior 的建议,我正在尝试编写一个自定义转换器,但我无法编译我的代码。
下面代码的错误是关于第一个 switch
块的 return 值(它是 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;
}
}});
}
您可以创建一个新变量并传输 request.ArticleFamily.Value 结果,这样它就可以 return 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);
}
在控制器的操作中验证整个请求正文。客户端将收到正确的错误消息。
您似乎正在使用 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);
正如@StriplingWarrior 所说,最后,解决方案就差不多了。
由于C#编译器的限制,无法为这段代码创建表达式树,解决方案是将转换代码工厂化为方法,然后在HasConversion
.[=12中调用它们=]
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 "";
}
}
在 .NET Core 2.1 项目中,我在 MediatR 库中使用带有命令模式的 EF Core =59=]服务器数据库。
我通过使用这些设置来设置项目以避免客户端查询评估:
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
实体中 class:
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;
}
}
}
现在,我想知道是否可以通过某种方式重构(并且可能远离实体 class)GetArticleFamily() 方法来保留 QueryClientEvaluationWarning
选项。
更新 2019/02/26
@StriplingWarrior 我已经根据你关于 ValueConverter()
的建议再次更新了代码,但现在它给出了这个错误:
Cannot convert Lambda expression into a tree of expressions.
更新 2019/02/25
按照@StriplingWarrior 的建议,我正在尝试编写一个自定义转换器,但我无法编译我的代码。
下面代码的错误是关于第一个 switch
块的 return 值(它是 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;
}
}});
}
您可以创建一个新变量并传输 request.ArticleFamily.Value 结果,这样它就可以 return 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);
}
在控制器的操作中验证整个请求正文。客户端将收到正确的错误消息。
您似乎正在使用 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);
正如@StriplingWarrior 所说,最后,解决方案就差不多了。
由于C#编译器的限制,无法为这段代码创建表达式树,解决方案是将转换代码工厂化为方法,然后在HasConversion
.[=12中调用它们=]
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 "";
}
}