LINQ 表达式无法翻译,将在本地进行评估

Kla*_*usE 3 c# entity-framework-core

使用具有自定义类型属性的实体时,该类型无法转换为 SQL。

我创建了一个例子来解释我解决它的方法:

在某个学期开设一门课。学期作为DateTime值存储在数据库中。

学期本身是一个自定义类型,具有附加属性。

public class Semester 
{
   public enum HalfYear 
   {
      First = 1,
      Second = 7
   }

   DateTime _dateTime;

   public Semester (HalfYear halfYear, int year) 
   {
       _dateTime = new DateTime(year, (int) halfYear, 1)
   }

   public int Year => _dateTime.Year;
   public HalfYear HalfYear => (HalfYear) _dateTime.Month;
   public DateTime FirstDay => new DateTime(Year, _dateTime.Month, 1);
   public DateTime LastDay => new DateTime(Year, _dateTime.Month + 5, DateTime.DaysInMonth(Year, _dateTime.Month + 5));
}

public class Class 
{
   int Id { get; set; }
   string Title { get; set; }
   Semester Semester { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

该Semester类型可以映射到DateTime使用值转换器。

这在Where子句中不起作用,例如

db.Classes.Where(c = c.Semester.FirstDay <= DateTime.Now && 
                     c.Semester.LastDay >= DateTime.Now)
Run Code Online (Sandbox Code Playgroud)

当 Entity Framework Core 尝试将表达式树转换为 SQL 时,它不知道如何转换Semester.FirstDay或Semester.LastDay.

正如文档所述,这是价值转换的已知限制

使用值转换可能会影响 EF Core 将表达式转换为 SQL 的能力。对于此类情况,将记录警告。正在考虑在将来的版本中删除这些限制。

如何解决这个问题?

Kla*_*usE 10

EntityFrameworkCore 有 3 个扩展点,可用于将自定义类型转换为 SQL。

  • IMemberTranslator
  • IMethodCallTranslator
  • 关系类型映射

这些翻译器和映射可以使用相应的插件进行注册:

  • IMemberTranslatorPlugin
  • IMethodCallTranslatorPlugin
  • IRelationalTypeMappingSourcePlugin

插件使用IDbContextOptionsExtension注册

以下示例说明了我如何实现这些接口以注册自定义类型 Semester:

IMemberTranslator

public class SqlServerSemesterMemberTranslator : IMemberTranslator
{
    public Expression Translate(MemberExpression memberExpression)
    {

        if (memberExpression.Member.DeclaringType != typeof(Semester)) {
            return null;
        }

        var memberName = memberExpression.Member.Name;

        if (memberName == nameof(Semester.FirstDay)) {
            return new SqlFunctionExpression(
                "DATEFROMPARTS",
                typeof(DateTime),
                new Expression[] {
                        new SqlFunctionExpression( "YEAR", typeof(int),new[] { memberExpression.Expression }),
                        new SqlFunctionExpression( "MONTH", typeof(int),new[] { memberExpression.Expression }),
                        Expression.Constant(1, typeof(int))
                });
        }

        if (memberName == nameof(Semester.LastDay)) {
            return new SqlFunctionExpression(
                "EOMONTH",
                typeof(DateTime),
                new Expression[] {
                        memberExpression.Expression
                });
        }

        if (memberName == nameof(Semester.HalfYear)) {
            return Expression.Convert(
                new SqlFunctionExpression(
                    "MONTH",
                    typeof(int),
                    new Expression[] {
                            memberExpression.Expression
                    }),
                typeof(HalfYear));
        }

        if (memberName == nameof(Semester.Year)) {
            return new SqlFunctionExpression(
                "YEAR",
                typeof(int),
                new Expression[] {
                        memberExpression.Expression
                });
        }

        return null;
    }

}
Run Code Online (Sandbox Code Playgroud)

IMethodCallTranslator

 public class SqlServerSemesterMethodCallTranslator : IMethodCallTranslator
{
    public Expression Translate(MethodCallExpression methodCallExpression)
    {
        if (methodCallExpression.Method.DeclaringType != typeof(Period)) {
            return null;
        }

        var methodName = methodCallExpression.Method.Name;

        // Implement your Method translations here

        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

关系类型映射

 public class SqlServerSemesterTypeMapping : DateTimeTypeMapping
{
    public SqlServerSemesterTypeMapping(string storeType, DbType? dbType = null) : 
        base(storeType, dbType)
    {
    }

    protected SqlServerSemesterTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters)
    {
    }

    protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => new SqlServerSemesterTypeMapping(parameters);
}
Run Code Online (Sandbox Code Playgroud)

IMemberTranslatorPlugin

 public class SqlServerCustomMemberTranslatorPlugin : IMemberTranslatorPlugin
{
    public IEnumerable<IMemberTranslator> Translators => new IMemberTranslator[] { new SqlServerSemesterMemberTranslator() };
}

 public class SqlServerCustomMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin
{
    public IEnumerable<IMethodCallTranslator> Translators => new IMethodCallTranslator[] { new SqlServerSemesterMethodCallTranslator() };
}
Run Code Online (Sandbox Code Playgroud)

IRelationalTypeMappingSourcePlugin

public class SqlServerCustomTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin
{
    public RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo) 
        => mappingInfo.ClrType == typeof(Semester) || (mappingInfo.StoreTypeName == nameof(DateTime))
            ? new SqlServerSemesterTypeMapping(mappingInfo.StoreTypeName ?? "datetime")
            : null;
}
Run Code Online (Sandbox Code Playgroud)

定义和注册翻译器后,必须在 DbContext 中配置它们。

IDbContextOptions扩展

public class SqlServerCustomTypeOptionsExtension : IDbContextOptionsExtensionWithDebugInfo
{
    public string LogFragment => "using CustomTypes";

    public bool ApplyServices(IServiceCollection services)
    {
        services.AddEntityFrameworkSqlServerCustomTypes();

        return false;
    }

    public long GetServiceProviderHashCode() => 0;

    public void PopulateDebugInfo(IDictionary<string, string> debugInfo) 
        => debugInfo["SqlServer:" + nameof(SqlServerCustomDbContextOptionsBuilderExtensions.UseCustomTypes)] = "1";

    public void Validate(IDbContextOptions options)
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

扩展方法

public static class SqlServerCustomDbContextOptionsBuilderExtensions
{
    public static object UseCustomTypes(this SqlServerDbContextOptionsBuilder optionsBuilder)
    {
        if (optionsBuilder == null) throw new ArgumentNullException(nameof(optionsBuilder));

        // Registere die SqlServerDiamantOptionsExtension.
        var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder;

        var extension = coreOptionsBuilder.Options.FindExtension<SqlServerCustomTypeOptionsExtension>()
            ?? new SqlServerCustomTypeOptionsExtension();

        ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension);

        // Configure Warnings
        coreOptionsBuilder
            .ConfigureWarnings(warnings => warnings
                .Log(RelationalEventId.QueryClientEvaluationWarning)        // Should be thrown to prevent only warnings if a query is not fully evaluated on the db
                .Ignore(RelationalEventId.ValueConversionSqlLiteralWarning));  // Ignore warnings for types that are using a ValueConverter

        return optionsBuilder;
    }
}


 public static class SqlServerServiceCollectionExtensions
{
    public static IServiceCollection AddEntityFrameworkSqlServerCustomTypes(
        this IServiceCollection serviceCollection)
    {
        if (serviceCollection == null) throw new ArgumentNullException(nameof(serviceCollection));

        new EntityFrameworkRelationalServicesBuilder(serviceCollection)
            .TryAddProviderSpecificServices(
                x => x.TryAddSingletonEnumerable<IRelationalTypeMappingSourcePlugin, SqlServerCustomTypeMappingSourcePlugin>()
                      .TryAddSingletonEnumerable<IMemberTranslatorPlugin, SqlServerCustomTypeMemberTranslatorPlugin>()
                      .TryAddSingletonEnumerable<IMethodCallTranslatorPlugin, SqlServerCustomTypeMethodCallTranslatorPlugin>());

        return serviceCollection;
    }
}
Run Code Online (Sandbox Code Playgroud)

在 DbContext 中注册选项

dbOptionsBuilder.UseSqlServer(connectionString, builder => builder.UseCustomTypes())
Run Code Online (Sandbox Code Playgroud)