У меня проблема с тем, как мое предложение 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 генерируются статическими методами. А есть два:
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.
Я действительно не знаю, как это исправить. Любая помощь?
Видимо проблема. Но я не могу воспроизвести точный случай - я получаю перевод в CASE WHEN ….THEN 2 ELSE e.status END = 2, т.е. все еще неправильный перевод, но обе константы перечисления не преобразуются в строки. Какую именно версию EF Core вы используете?
@IvanStoev, ты прав. Когда я писал этот вопрос, у меня не было под рукой точного проекта, поэтому я пошел по памяти. Теперь я отредактирую вопрос с исправленной версией. Кроме того, я свяжу проект GitHub, чтобы воспроизвести эту проблему.
@TanvirArjel, чтобы вызвать toString() для перечислений, было бы очень неудобно, так как мне пришлось бы вызывать это для всех из них на этом этапе. Кроме того, toString() не транслируется в SQL, поэтому операция выполняется в памяти.
Я хотел бы предложить обходной путь, но проблема довольно сложна, и я не могу найти простую точку расширения EF Core, чтобы исправить ее. Проблема в основном в том, что «вывод типа данных» не работает для условных выражений (оператор ? :) — они пропустили этот случай.
Считаете ли вы, что сейчас уместно довести эту проблему до сведения участников проекта EF Core, LinqKit (или кого-либо еще)?
@ФранческоД.М. Абсолютно - см. мой ответ. Но имейте в виду, что все их усилия в настоящее время направлены на v3, поэтому, скорее всего, они не впишутся в v2. По-прежнему полезно сообщить им, чтобы они могли вписать это в следующую версию (если это еще не сделано). Вы можете использовать мой обходной путь на данный момент и удалить его, когда они его исправят.





Проблема связана не с LinqKit, а с самим выражением, в частности с условным оператором и текущим преобразованием запросов и значений EF Core 2.
Проблема в том, что в настоящее время преобразования значений указываются для каждого свойства (столбца), а не для каждого типа. Таким образом, для корректного перевода в SQL транслятор должен «выводить» тип константы/параметра из свойства. Он делает это для большинства типов выражений, но не для условного оператора.
Итак, первое, что вы должны сделать, — это сообщить об этом в средство отслеживания проблем EF Core.
Что касается обходного пути:
К сожалению, эта функциональность находится внутри класса инфраструктуры под названием DefaultQuerySqlGenerator, который наследуется каждым поставщиком базы данных. Службу, предоставляемую этим классом, можно заменить, хотя и несколько сложным способом, что можно увидеть в моем ответе на Ef-Core — какое регулярное выражение я могу использовать для замены имен таблиц на имена без блокировки в 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.
Чтобы получить эффект, вам нужно добавить UseCustom{Database}QuerySqlGenerator везде, где вы используете Use{Database}, например.
.UseSqlServer(...)
.UseCustomSqlServerQuerySqlGenerator()
или
.UseNpgsql(...)
.UseCustomNpgsqlQuerySqlGenerator()
и т.п.
Как только вы это сделаете, перевод (по крайней мере, для SqlServer) будет таким, как ожидалось:
WHERE CASE
WHEN [e].[DueAtDate] < @__now_0
THEN 'Overdue' ELSE [e].[Status]
END = 'Overdue'
Большое спасибо за время, которое вы потратили на это. Я обязательно сообщу об этом команде EF Core и вам, не ожидая, что это будет исправлено в версиях 2.x, но если это войдет в версию 3, это будет здорово. Как только я сообщу о проблеме, я отпишусь здесь со ссылкой. Хорошего дня
Вы должны использовать
MyEnum.Overdue.ToSring()