The issue is not related to LinqKit, but the expression itself, specifically the conditional operator and current EF Core 2 query translation and value conversions.
The problem is that currently value conversions are specified per property (column) rather than per type. So in order to be translated correctly to SQL, the translator must "infer" the constant / parameter type from a property. It does that for most of the type of expressions, but not for conditional operator.
So the first thing you should do is to report it to the EF Core issue tracker.
Regarding workaround:
Unfortunately the functionality is inside an infrastructure class called DefaultQuerySqlGenerator
, which is inherited by every database provider. The service provided by that class can be replaced, although in a bit complicated way, which can be seen in my answer to Ef-Core - What regex can I use to replace table names with nolock ones in Db Interceptor, and additionally has to be done for every database provider you want to support.
For SqlServer it requires something like this (tested):
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);
}
}
}
and for PostgreSQL (not tested):
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);
}
}
}
Besides the boilerplate code, the fix is
if (expression is UnaryExpression unaryExpression)
return InferTypeMappingFromColumn(unaryExpression.Operand);
if (expression is ConditionalExpression conditionalExpression)
return InferTypeMappingFromColumn(conditionalExpression.IfTrue) ?? InferTypeMappingFromColumn(conditionalExpression.IfFalse);
inside InferTypeMappingFromColumn
method override.
In order to have effect, you need to add UseCustom{Database}QuerySqlGenerator
anywhere you use Use{Database}
, e.g.
.UseSqlServer(...)
.UseCustomSqlServerQuerySqlGenerator()
or
.UseNpgsql(...)
.UseCustomNpgsqlQuerySqlGenerator()
etc.
Once you do that, the translation (at least for SqlServer) is as expected:
WHERE CASE
WHEN [e].[DueAtDate] < @__now_0
THEN 'Overdue' ELSE [e].[Status]
END = 'Overdue'