我对如何将 Linq where 子句翻译成 Sql 有疑问。
我正在使用 EnumToStringConverter
将我的实体的属性(enum
)映射到文本数据库列中。当仅从 DbContext 查询我的实体时,一切正常。
然后我开始使用 LinqKit 和 Expressions 来获得可重复使用的过滤器。我创建了一个表达式,它接受我的实体并根据对实体其他属性的一些计算给出我的枚举。我会尝试用代码来解释自己,因为言语让我失望。 我会写一个例子,所以我不必发布完整的代码,但逻辑是一样的。您可以在此处找到包含项目的 GitHub 存储库以复制该问题:https://github.com/pinoy4/efcore-enum-to-string-test
模型类:
public class MyEntity
{
public Guid Id { get; set; }
public MyEnum Status { get; set; }
public DateTime DueAtDate { get; set; }
}
public MyEnum
{
New = 0,
InProgress = 1,
Overdue = 2
}
FluentAPI 配置
public class MyEntityConfiguration : IEntityTypeConfiguration<MyEntity>
{
public void Configure(EntityTypeBuilder<MyEntity> builder)
{
// irrelevant parts of configuration skipped here
builder.Property(e => e.Status)
.HasColumnName("status")
.IsRequired()
.HasConversion(new EnumToStringConverter<MyEnum>());
}
}
Linq 表达式是用静态方法生成的。 A 有两个:
public static class MyExpressions
{
public static Expression<Func<MyEntity, MyEnum>> CalculateStatus(DateTime now)
{
/*
* This is the tricky part as in one case I am returning
* an enum value that am am setting here and in the other
* case it is an enum value that is taken from the entity.
*/
return e => e.DueAtDate < now ? MyEnum.Overdue : e.Status;
}
public static Expression<Func<MyEntity, bool>> GetOverdue(DateTime now)
{
var calculatedStatus = CalculateStatus(now);
return e => calculatedStatus.Invoke(e) == MyEnum.Overdue;
}
}
既然我们有了上面的代码,我就这样写一个查询:
var getOverdueFilter = MyExpressions.GetOverdue(DateTime.UtcNow);
DbContext.MyEntities.AsExpandable().Where(getOverdueFilter).ToList();
这被翻译成以下 SQL:
SELECT ... WHERE CASE
WHEN e.due_at_date < $2 /* the date that we are passing as a parameter */
THEN 2 ELSE e.status
END = 2;
问题是 CASE
语句将 'Overdue'
(它使用 EnumToStringConverter
正确翻译)与给出的表达式进行比较为真时为 int
(2 是 MyEnum.Overdue 情况下的值),为假时为 string
(e.status)。这显然是无效的 SQL。
我真的不知道如何解决这个问题。有帮助吗?
最佳答案
问题与 LinqKit 无关,而是表达式本身,特别是条件运算符和当前的 EF Core 2 查询翻译和值转换。
问题是当前值转换是按属性(列)指定的,而不是按类型指定的。因此,为了正确翻译成 SQL,翻译器必须从属性“推断”出常量/参数类型。它对大多数类型的表达式都这样做,但对条件运算符不这样做。
因此,您应该做的第一件事就是将其报告给 EF Core 问题跟踪器。
关于解决方法:
不幸的是,该功能位于名为 DefaultQuerySqlGenerator
的基础结构类中,每个数据库提供程序都继承了该类。该类提供的服务是可以替换的,虽然方式有点复杂,可以看我对Ef-Core - What regex can I use to replace table names with nolock ones in Db Interceptor的回答。 ,另外还必须为您要支持的每个数据库提供程序完成。
对于 SqlServer,它需要这样的东西(已测试):
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query.Expressions;
using Microsoft.EntityFrameworkCore.Query.Sql;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Query.Sql.Internal;
namespace Microsoft.EntityFrameworkCore
{
public static partial class CustomDbContextOptionsBuilderExtensions
{
public static DbContextOptionsBuilder UseCustomSqlServerQuerySqlGenerator(this DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.ReplaceService<IQuerySqlGeneratorFactory, CustomSqlServerQuerySqlGeneratorFactory>();
return optionsBuilder;
}
}
}
namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Sql.Internal
{
class CustomSqlServerQuerySqlGeneratorFactory : SqlServerQuerySqlGeneratorFactory
{
private readonly ISqlServerOptions sqlServerOptions;
public CustomSqlServerQuerySqlGeneratorFactory(QuerySqlGeneratorDependencies dependencies, ISqlServerOptions sqlServerOptions)
: base(dependencies, sqlServerOptions) => this.sqlServerOptions = sqlServerOptions;
public override IQuerySqlGenerator CreateDefault(SelectExpression selectExpression) =>
new CustomSqlServerQuerySqlGenerator(Dependencies, selectExpression, sqlServerOptions.RowNumberPagingEnabled);
}
public class CustomSqlServerQuerySqlGenerator : SqlServerQuerySqlGenerator
{
public CustomSqlServerQuerySqlGenerator(QuerySqlGeneratorDependencies dependencies, SelectExpression selectExpression, bool rowNumberPagingEnabled)
: base(dependencies, selectExpression, rowNumberPagingEnabled) { }
protected override RelationalTypeMapping InferTypeMappingFromColumn(Expression expression)
{
if (expression is UnaryExpression unaryExpression)
return InferTypeMappingFromColumn(unaryExpression.Operand);
if (expression is ConditionalExpression conditionalExpression)
return InferTypeMappingFromColumn(conditionalExpression.IfTrue) ?? InferTypeMappingFromColumn(conditionalExpression.IfFalse);
return base.InferTypeMappingFromColumn(expression);
}
}
}
对于 PostgreSQL(未测试):
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query.Expressions;
using Microsoft.EntityFrameworkCore.Query.Sql;
using Microsoft.EntityFrameworkCore.Storage;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal;
namespace Microsoft.EntityFrameworkCore
{
public static partial class CustomDbContextOptionsBuilderExtensions
{
public static DbContextOptionsBuilder UseCustomNpgsqlQuerySqlGenerator(this DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.ReplaceService<IQuerySqlGeneratorFactory, CustomNpgsqlQuerySqlGeneratorFactory>();
return optionsBuilder;
}
}
}
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal
{
class CustomNpgsqlQuerySqlGeneratorFactory : NpgsqlQuerySqlGeneratorFactory
{
private readonly INpgsqlOptions npgsqlOptions;
public CustomNpgsqlQuerySqlGeneratorFactory(QuerySqlGeneratorDependencies dependencies, INpgsqlOptions npgsqlOptions)
: base(dependencies, npgsqlOptions) => this.npgsqlOptions = npgsqlOptions;
public override IQuerySqlGenerator CreateDefault(SelectExpression selectExpression) =>
new CustomNpgsqlQuerySqlGenerator(Dependencies, selectExpression, npgsqlOptions.ReverseNullOrderingEnabled);
}
public class CustomNpgsqlQuerySqlGenerator : NpgsqlQuerySqlGenerator
{
public CustomNpgsqlQuerySqlGenerator(QuerySqlGeneratorDependencies dependencies, SelectExpression selectExpression, bool reverseNullOrderingEnabled)
: base(dependencies, selectExpression, reverseNullOrderingEnabled) { }
protected override RelationalTypeMapping InferTypeMappingFromColumn(Expression expression)
{
if (expression is UnaryExpression unaryExpression)
return InferTypeMappingFromColumn(unaryExpression.Operand);
if (expression is ConditionalExpression conditionalExpression)
return InferTypeMappingFromColumn(conditionalExpression.IfTrue) ?? InferTypeMappingFromColumn(conditionalExpression.IfFalse);
return base.InferTypeMappingFromColumn(expression);
}
}
}
除了样板代码,修复是
if (expression is UnaryExpression unaryExpression)
return InferTypeMappingFromColumn(unaryExpression.Operand);
if (expression is ConditionalExpression conditionalExpression)
return InferTypeMappingFromColumn(conditionalExpression.IfTrue) ?? InferTypeMappingFromColumn(conditionalExpression.IfFalse);
在 InferTypeMappingFromColumn
方法覆盖内。
为了生效,需要在使用Use{Database}
的地方添加UseCustom{Database}QuerySqlGenerator
,例如
.UseSqlServer(...)
.UseCustomSqlServerQuerySqlGenerator()
或
.UseNpgsql(...)
.UseCustomNpgsqlQuerySqlGenerator()
等等
一旦你这样做了,翻译(至少对于 SqlServer 来说)就是预期的:
WHERE CASE
WHEN [e].[DueAtDate] < @__now_0
THEN 'Overdue' ELSE [e].[Status]
END = 'Overdue'
关于c# - where 子句中未使用 EFCore 枚举到字符串值的转换,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55182602/