EF核心:通用StartsWith()表达式不可翻译

时间:2020-07-14 06:01:27

标签: c# linq-to-sql ef-core-3.1

我正在尝试在EF Core 3.1.5中构建一种通用的“ StartsWith”表达式。 受管实体看起来像这样:

public class MyEntity
{
    [System.ComponentModel.DataAnnotations.Key] // key just for the sake of having a key defined, not relevant for the question
    public string TopLevelString { get; set; }

    public AnotherEntity OtherEntity { get; set; }
}
public class AnotherEntity
{
    [System.ComponentModel.DataAnnotations.Key]  // key just for the sake of having a key defined, not relevant for the question
    public string NestedString { get; set; }
}

我将其放在这样的上下文中:

public class MyDbContext : DbContext
{
    public DbSet<MyEntity> MyEntities { get; set; }
    public MyDbContext(DbContextOptions<MyDbContext> options) {}
    protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlite("Data Source=sqlitedemo.db");
}

并尝试在测试类中使用此上下文:

public partial class MyTestClass // this part is just to make the example 100% reproducable
{ 
    // define some minimal examples to work with
    private List<MyEntity> testExampleList = new List<MyEntity>()
    {
        new MyEntity()
        {
            TopLevelString = "ABC",
            OtherEntity = new AnotherEntity(){NestedString = "ABC"}
        },
        new MyEntity()
        {
            TopLevelString = "XYZ",
            OtherEntity = new AnotherEntity(){NestedString = "XYZ"}
        }
    };

    MyDbContext context;
    public MyTestClass()
    {
        // set up database
        var options = new DbContextOptions<MyDbContext>();
        this.context = new MyDbContext(options);
        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        // add examples from above list
        this.context.MyEntities.AddRange(testExampleList);
        this.context.SaveChanges();
    }
}

这是我想作为普通Where过滤器执行的操作:

public partial class MyTestClass // this part works as expected and is just used to illustrate the purpose of below code
{ 
    [Fact]
    public void TestFilteringWithoutOwnExpression()
    {
        Assert.Equal(1, context.MyEntities.Where(x => x.TopLevelString.StartsWith("A")).Count()); // works fine
        Assert.Equal(1, context.MyEntities.Where(x => x.OtherEntity.NestedString.StartsWith("A")).Count()); // works, too
    }
}

由于在应用实际的where子句之前还应该发生其他魔术,因此我尝试将其包装成这样的表达式:

public partial class MyTestClass // this part does not work and I don' know why
{
    [Fact]
    public void TestFilteringWithExpression()
    {
        Assert.Equal(1, context.MyEntities.MyWhere<MyEntity>(x => x.TopLevelString, "A").Count());
        Assert.Equal(1, context.MyEntities.MyWhere<MyEntity>(x => x.OtherEntity.NestedString, "A").Count());
    }
}

在扩展类中定义了MyWhere

public static class IQueryableExtension
{
    public static IQueryable<TEntity> MyWhere<TEntity>(this IQueryable<TEntity> query, Expression<Func<TEntity, string>> stringSelector, string searchString)
    {
        ParameterExpression entityParameter = Expression.Parameter(typeof(TEntity), stringSelector.Parameters.First().Name);
        MemberExpression memberExpr = (MemberExpression)(stringSelector.Body);
        var searchConstant = Expression.Constant(searchString, typeof(string));

        var filterExpression = Expression.Lambda<Func<TEntity, bool>>(
                                         Expression.Call(
                                            memberExpr,
                                            typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }),
                                            searchConstant),
                                         entityParameter);
        query = query.Where(filterExpression);
        return query;
    }
}

我看到了类似的示例,其中使用了PropertyExpression而不是MemberExpression,但是这对我来说失败了,因为我尝试不仅访问MyEntity.TopLevelString而且访问嵌套的MyEntity.AnotherEntity.NestedString

代码失败,并显示InvalidOperationException:

LINQ表达式'DbSet .Where(m => x.TopLevelString!= null &&“ A”!= null && x.TopLevelString.StartsWith(“ A”))'无法翻译。以可以翻译的形式重写查询,或切换到客户评估... +

如何设置通用且可翻译的StartsWith Expression?

1 个答案:

答案 0 :(得分:3)

好吧,问题与StartsWith无关。如果检查异常DbSet .Where(m => x.TopLevelString != null && "A" != null && x.TopLevelString.StartsWith("A")m =>开头,但内部使用x,则是由ParameterExpression entityParameter = Expression.Parameter(typeof(TEntity), stringSelector.Parameters.First().Name);行引起的。您可以使用该参数名称来生成另一个参数,但是它们并不相同。

有2种解决方案。

  1. 是直接使用参数并在其上扩展表达式
  2. 使用新创建的冗余参数重写成员表达式。

所以基本上,这将解决您的问题:

ParameterExpression entityParameter =  stringSelector.Parameters.First();

如果在List<T>之类的集合上尝试扩展方法,则会出现如下错误:

System.InvalidOperationException:从范围”引用的类型为'ExpressionTest.MyEntity'的变量'x',但未定义'