简体   繁体   English

LINQ 表达式无法翻译,将在本地计算

[英]LINQ expression could not be translated and will be evaluated locally

When using an entity with property of a custom type, the type cannot be translated into SQL.使用具有自定义类型属性的实体时,无法将类型转换为 SQL。

I have created an example to explain my approach to solve it:我创建了一个示例来解释我的解决方法:

A class takes place in a certain semester. class 发生在某个学期。 The semester is stored as a DateTime value in the database.学期作为DateTime值存储在数据库中。

The semester itself is a custom type, with additional properties.学期本身是一种自定义类型,具有附加属性。

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; }
}

The Semester type can be mapped to a DateTime using value converters . Semester类型可以使用 值转换器映射到DateTime

This does not work in Where clause such as这在Where子句中不起作用,例如

db.Classes.Where(c = c.Semester.FirstDay <= DateTime.Now && 
                     c.Semester.LastDay >= DateTime.Now)

When Entity Framework Core tries to translate the expression tree to SQL, it does not know how to translate Semester.FirstDay or Semester.LastDay .当 Entity Framework Core 尝试将表达式树转换为 SQL 时,它不知道如何转换Semester.FirstDaySemester.LastDay

This is a known limitation of value conversions as the documentation states正如文档所述,这是价值转换的已知限制

Use of value conversions may impact the ability of EF Core to translate expressions to SQL.使用值转换可能会影响 EF Core 将表达式转换为 SQL 的能力。 A warning will be logged for such cases.对于此类情况,将记录警告。 Removal of these limitations is being considered for a future release.正在考虑在未来的版本中删除这些限制。

How to solve this issue?如何解决这个问题?

EntityFrameworkCore has 3 extension points that can be used to translate custom types to SQL. EntityFrameworkCore 有 3 个扩展点,可用于将自定义类型转换为 SQL。

  • IMemberTranslator IMemberTranslator
  • IMethodCallTranslator IMethodCallTranslator
  • RelationalTypeMapping关系类型映射

These translators and mapppings can be registered using the corresponding plugins:这些翻译器和映射可以使用相应的插件注册:

  • IMemberTranslatorPlugin IMemberTranslatorPlugin
  • IMethodCallTranslatorPlugin IMethodCallTranslatorPlugin
  • IRelationalTypeMappingSourcePlugin IRelationalTypeMappingSourcePlugin

The plugins are registered with a IDbContextOptionsExtension插件使用IDbContextOptionsExtension注册

The following example illustrates how I have implemented these interfaces to register the custom type Semester:以下示例说明了我如何实现这些接口来注册自定义类型 Semester:

IMemberTranslator 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;
    }

}

IMethodCallTranslator 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;
    }
}

RelationalTypeMapping关系类型映射

 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);
}

IMemberTranslatorPlugin 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() };
}

IRelationalTypeMappingSourcePlugin 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;
}

After you have defined and registered the translators, you have to confgure them in the DbContext.定义并注册翻译器后,您必须在 DbContext 中配置它们。

IDbContextOptionsExtension IDbContextOptionsExtension

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)
    {
    }
}

Extension Methods扩展方法

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;
    }
}

Register the option in the DbContext在 DbContext 中注册选项

dbOptionsBuilder.UseSqlServer(connectionString, builder => builder.UseCustomTypes())

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM