EF Core: Generi c StartsWith () выражение не переводимо - PullRequest
0 голосов
/ 14 июля 2020

Я пытаюсь создать своего рода обобщенное выражение c "StartsWith" в EF Core 3.1.5. Управляемый объект выглядит так:

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 должно произойти какое-то другое c, я попытался оберните его в собственное выражение, например:

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

Я видел аналогичные примеры, где вместо MemberExpression используется PropertyExpression используется, но это не удалось для меня, как только я попытался получить доступ не только к MyEntity.TopLevelString, но и к вложенному MyEntity.AnotherEntity.NestedString.

Код завершился с ошибкой InvalidOperationException:

Выражение LINQ 'DbSet .Where (m => x.TopLevelString! = Null && "A"! = Null && x.TopLevelString.StartsWith ("A"))' не может быть переведено. Либо перепишите запрос в форме, которая может быть переведена, либо переключитесь на оценку клиента ... +

Как мне настроить общий c и переводимый StartsWith Expression?

1 Ответ

3 голосов
/ 14 июля 2020

Ну проблема не в 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: 'variable' x 'of type' ExpressionTest.MyEntity 'referenced from scope' ', но это не определено '

...