Преобразование перечисления EFCore в строковое значение не используется в предложении where - PullRequest
4 голосов
/ 15 марта 2019

У меня проблема с тем, как мое предложение Linq where переводится в Sql.

Я использую EnumToStringConverter, чтобы отобразить свойство моей сущности, которое является enum, в столбец text db. Все это прекрасно работает, когда просто запрашиваю мою сущность из 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), когда true и string (e.status), когда false. Это явно недопустимый SQL.

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

1 Ответ

3 голосов
/ 20 марта 2019

Проблема связана не с LinqKit, а с самим выражением, а именно с условным оператором и текущими преобразованиями запросов EF Core 2 и преобразованиями значений.

Проблема в том, что в настоящее время преобразования значений указываются для свойства (столбца), а не для типа. Поэтому для правильного перевода в SQL транслятор должен «вывести» тип константы / параметра из свойства. Это делается для большинства выражений типа, но не для условного оператора.

Итак, первое, что вы должны сделать, это сообщить об этом в систему отслеживания проблем EF Core.

Относительно обходного пути:

К сожалению, функциональность находится внутри класса инфраструктуры, называемого DefaultQuerySqlGenerator, который наследуется каждым поставщиком базы данных. Служба, предоставляемая этим классом, может быть заменена, хотя и несколько сложным способом, что видно из моего ответа на Ef-Core - какое регулярное выражение я могу использовать для замены имен таблиц на nolock в 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'
...