Преобразование перечисления EFCore в строковое значение не используется в предложении where

У меня проблема с тем, как мое предложение 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.

Я действительно не знаю, как это исправить. Любая помощь?

Вы должны использовать MyEnum.Overdue.ToSring()

TanvirArjel 17.03.2019 09:37

Видимо проблема. Но я не могу воспроизвести точный случай - я получаю перевод в CASE WHEN ….THEN 2 ELSE e.status END = 2, т.е. все еще неправильный перевод, но обе константы перечисления не преобразуются в строки. Какую именно версию EF Core вы используете?

Ivan Stoev 17.03.2019 14:06

@IvanStoev, ты прав. Когда я писал этот вопрос, у меня не было под рукой точного проекта, поэтому я пошел по памяти. Теперь я отредактирую вопрос с исправленной версией. Кроме того, я свяжу проект GitHub, чтобы воспроизвести эту проблему.

Francesco D.M. 18.03.2019 13:46

@TanvirArjel, чтобы вызвать toString() для перечислений, было бы очень неудобно, так как мне пришлось бы вызывать это для всех из них на этом этапе. Кроме того, toString() не транслируется в SQL, поэтому операция выполняется в памяти.

Francesco D.M. 18.03.2019 15:58

Я хотел бы предложить обходной путь, но проблема довольно сложна, и я не могу найти простую точку расширения EF Core, чтобы исправить ее. Проблема в основном в том, что «вывод типа данных» не работает для условных выражений (оператор ? :) — они пропустили этот случай.

Ivan Stoev 19.03.2019 21:34

Считаете ли вы, что сейчас уместно довести эту проблему до сведения участников проекта EF Core, LinqKit (или кого-либо еще)?

Francesco D.M. 20.03.2019 15:24

@ФранческоД.М. Абсолютно - см. мой ответ. Но имейте в виду, что все их усилия в настоящее время направлены на v3, поэтому, скорее всего, они не впишутся в v2. По-прежнему полезно сообщить им, чтобы они могли вписать это в следующую версию (если это еще не сделано). Вы можете использовать мой обходной путь на данный момент и удалить его, когда они его исправят.

Ivan Stoev 20.03.2019 16:30
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
7
2 266
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема связана не с 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, это будет здорово. Как только я сообщу о проблеме, я отпишусь здесь со ссылкой. Хорошего дня

Francesco D.M. 20.03.2019 18:28

Другие вопросы по теме