where 子句中未使用 EFCore 枚举到字符串值的转换
EFCore enum to string value conversion not used in where clause
我的 Linq where 子句如何转换为 Sql。
我正在使用 EnumToStringConverter
将我的 enum
实体的 属性 映射到文本数据库列中。当仅从 DbContext 查询我的实体时,这一切工作正常。
然后我开始使用 LinqKit 和 Expressions 来获得可重用的过滤器。我创建了一个表达式,它接受我的实体并根据对实体其他属性的一些计算给出我的枚举。我会尝试用代码来解释自己,因为言语让我失望。
我会写一个例子,所以我不必 post 完整的代码,但逻辑是一样的。您可以在此处找到一个包含项目的 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 < /* 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,翻译器必须 "infer" 来自 属性 的常量/参数类型。它对大多数类型的表达式都这样做,但对条件运算符不这样做。
因此,您应该做的第一件事就是将其报告给 EF Core 问题跟踪器。
关于解决方法:
不幸的是,该功能位于名为 DefaultQuerySqlGenerator
的基础结构 class 中,每个数据库提供商都继承了该功能。 class 提供的服务可以被替换,尽管方式有点复杂,这可以在我对 的回答中看到,此外还必须为您想要支持的每个数据库提供商完成。
对于 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'
我的 Linq where 子句如何转换为 Sql。
我正在使用 EnumToStringConverter
将我的 enum
实体的 属性 映射到文本数据库列中。当仅从 DbContext 查询我的实体时,这一切工作正常。
然后我开始使用 LinqKit 和 Expressions 来获得可重用的过滤器。我创建了一个表达式,它接受我的实体并根据对实体其他属性的一些计算给出我的枚举。我会尝试用代码来解释自己,因为言语让我失望。 我会写一个例子,所以我不必 post 完整的代码,但逻辑是一样的。您可以在此处找到一个包含项目的 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 < /* 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,翻译器必须 "infer" 来自 属性 的常量/参数类型。它对大多数类型的表达式都这样做,但对条件运算符不这样做。
因此,您应该做的第一件事就是将其报告给 EF Core 问题跟踪器。
关于解决方法:
不幸的是,该功能位于名为 DefaultQuerySqlGenerator
的基础结构 class 中,每个数据库提供商都继承了该功能。 class 提供的服务可以被替换,尽管方式有点复杂,这可以在我对
对于 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'