Как выполнять операции с навигационными свойствами?

Я пытался создать динамически генерируемые деревья выражений для моделей фильтрации в моей базе данных (PSQL, используя Npgsql с EF Core). Однако когда я пытаюсь отфильтровать список моделей Player по списку идентификаторов (идентификаторы int модели Hero), я получаю исключение.

System.ArgumentNullException: значение не может быть нулевым. (Параметр «параметр»)

У меня отображается следующая модель:

public class Player : IProfile
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }

    public string? Name { get; set; }
    public string? Description { get; set; }

    public bool? Displayed { get; set; }

    public int? PositionId { get; set; }
    public Position? Position { get; set; }

    public DateTime UpdatedAt { get; set; }

    public ICollection<Hero> Heroes { get; protected set; } = [];
    public ICollection<Team> Teams { get; protected set; } = [];
    public ICollection<TeamPlayer> TeamPlayers { get; protected set; } = [];
}

Дерево выражений генерируется и затем используется в Where в этой функции в зависимости от предоставленных параметров фильтрации:

public static IQueryable<Player> FilterWith(this IQueryable<Player> query, PlayerConditions queryConfig)
{
     var parameter = Expression.Parameter(typeof(Player), "profile");
     Expression expr = Expression.Constant(true);

     if (queryConfig.NameFilter != null)
     {
         var condExpr = GetStringFilteringExpression<Player>(queryConfig.NameFilter, "Name", parameter);

         if (condExpr != null) 
             expr = Expression.AndAlso(expr, condExpr);
     }

     // ...
     if (queryConfig.HeroFilter != null)
     {
         var condExpr = GetValueListFilteringOnListExpression<Player, Hero, int>(queryConfig.HeroFilter, "Heroes", "Id", parameter);

         if (condExpr != null) 
             expr = Expression.AndAlso(expr, condExpr);
     }

     // ...
     Expression<Func<Player, bool>> finalLambda = Expression.Lambda<Func<Player, bool>>(expr, parameter);

     return query.Where(finalLambda);
}

Исключение возникает при попытке отфильтровать игроков на основе переданного списка идентификаторов героев.

Вот как создается выражение HeroFilter:

public static Expression? GetValueListFilteringOnListExpression<TProfile, TListItem, TListItemProp>(ValueFilter<TListItemProp>? filter, string listPropertyName, string listItemPropertyName, ParameterExpression parameter) where TProfile : class, IProfile
{
    var listType = typeof(ICollection<TListItem>);
    var property = typeof(TProfile).GetProperty(listPropertyName);
    var listItemProperty = typeof(TListItem).GetProperty(listItemPropertyName);

    if (filter == null || property == null || property.PropertyType != listType || listItemProperty == null || listItemProperty.PropertyType != typeof(TListItemProp)) 
        return null;

    var listItemType = typeof(TListItem);
    var valueList = Expression.Constant(filter.ValueList, typeof(ICollection<TListItemProp>));
    var memberAccess = Expression.MakeMemberAccess(parameter, property);

    var intersectByMethodInfo = typeof(Enumerable)
        .GetMethods()
        .Where(x => x.Name == "IntersectBy")
        .Single(x => x.GetParameters().Length == 3)
        .MakeGenericMethod(typeof(TListItem), typeof(TListItemProp));

    var countMethod = typeof(Enumerable)
       .GetMethods(BindingFlags.Static | BindingFlags.Public)
       .First(m => m.Name == "Count" && m.GetParameters().Length == 1)
       .MakeGenericMethod(typeof(TListItem));

    var listItemParameter = Expression.Parameter(listItemType, "li");
    var listItemPropertyEx = Expression.MakeMemberAccess(listItemParameter, listItemProperty);
    var listItemPropertyLambda = Expression.Lambda(listItemPropertyEx, listItemParameter);

    //var countProperty = typeof(ICollection<>).MakeGenericType(typeof(TListItemProp)).GetProperty("Count");
    var intersectByExpression = Expression.Call(intersectByMethodInfo, memberAccess, valueList, listItemPropertyLambda);
    //var intersectedCount = Expression.Property(intersectByExpression, countProperty);
    var intersectedCount = Expression.Call(countMethod, intersectByExpression);

    Expression? finalExpression = null;

    switch (filter.FilterType)
    {
        case ValueListFilterType.Exact:
            {
                var valueCount = Expression.Constant(filter.ValueList.Count);
                finalExpression = Expression.Equal(intersectedCount, valueCount);
                break;
            }

        case ValueListFilterType.Including:
            {
                var valueCount = Expression.Constant(filter.ValueList.Count);
                finalExpression = Expression.GreaterThanOrEqual(intersectedCount, valueCount);
                break;
            }

        case ValueListFilterType.Excluding:
            {
                var valueCount = Expression.Constant(0);
                finalExpression = Expression.Equal(intersectedCount, valueCount);
                break;
            }

        case ValueListFilterType.Any:
            {
                var valueCount = Expression.Constant(0);
                finalExpression = Expression.GreaterThan(intersectedCount, valueCount);
                break;
            }

        default:
            break;
    }

    return finalExpression;
}

Я все еще пытаюсь понять, как отладить LINQ, но я протестировал свою функцию фильтрации непосредственно на списке игроков в памяти, и она работает! Фильтрует правильно. Итак, осмотревшись еще немного, я обнаружил, что не все функции C# Collection имеют переводы в синтаксис SQL в Npgsql (похоже, это только Collections.Contains). Это моя проблема? Если да, то как лучше всего выполнять операции над наборами со свойствами навигации?

Итак, вы хотите построить p => p.Heroes.Any(h => h.something == "")? (он же Expression.Any(p.Heroes, ...)) У вас должна быть возможность повторно использовать большую часть конструкции динамического дерева выражений для внутренней лямбды.

Jeremy Lakeman 26.08.2024 03:45

Не совсем, мне нужно сравнить список в модели со списком идентификаторов. Конечно, я проводил простые сравнения значений с деревьями выражений, но операции над множествами ускользают от меня. Я хочу выполнить их в БД, а не в памяти.

brainpostman 26.08.2024 20:58
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
2
68
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Если вы собираетесь написать свой метод FilterWith настолько явно, вы можете также пропустить динамические деревья выражений linq и вызвать .Where() повторно, что будет вести себя так, как если бы каждый фильтр && был вместе;

public static IQueryable<Player> FilterWith(this IQueryable<Player> query, PlayerConditions queryConfig)
{
    if (queryConfig.NameFilter != null)
        query = query.Where(p => p.Name == queryConfig.NameFilter);
    if (queryConfig.HeroFilter != null)
        query = query.Where(p => p.Heroes.Any(h => h.Id == queryConfig.HeroFilter));
    // etc
    return query;
}

Я бы динамически строил дерево выражений только в том случае, если ни один из ваших кодов не знает, какие свойства фильтровать.

Даже если вы хотите || объединить термины, гораздо проще изменить два дерева выражений, чем строить все с нуля.

Я собирался повторно использовать выражения для некоторых других моделей и просто решил попрактиковаться в использовании деревьев выражений. Я знаю, что LINQ объединяет вызовы Where. Я думаю, это было написано не так ясно, но у HeroFilter есть свойство ValueList, которое представляет собой список идентификаторов. Поэтому мне нужно сравнить связанных героев в каждом игроке (Герои — это свойство навигации для отношения MTM) со списком идентификаторов на основе поведения фильтрации (если Player.Heroes включает все идентификаторы в список, имеет какие-либо идентификаторы в список имеет точно такие же идентификаторы, что и список, и т. д.). Проблема заключается в создании выражения, которое выполняется в БД.

brainpostman 26.08.2024 18:16
Ответ принят как подходящий

Я сделал это. Я использовал методы IEnumerable, когда LINQ to SQL требовал IQueryable, все, что было необходимо, было приведение к IQueryable с помощью AsQueryable (и методы были из Queryable):

 public static Expression? GetValueListFilteringOnListExpression<TProfile, TListItem, TListItemProp>(ValueFilter<TListItemProp>? filter, string listPropertyName, string listItemPropertyName, ParameterExpression parameter) where TProfile : class, IProfile
 {
     var listType = typeof(ICollection<TListItem>);
     var property = typeof(TProfile).GetProperty(listPropertyName);
     var listItemProperty = typeof(TListItem).GetProperty(listItemPropertyName);
     if (filter == null || property == null || property.PropertyType != listType || listItemProperty == null || listItemProperty.PropertyType != typeof(TListItemProp)) return null;
     var valueList = Expression.Constant(filter.ValueList, typeof(ICollection<TListItemProp>));
     var memberAccess = Expression.Property(parameter, listPropertyName);
     var asQueryable = typeof(Queryable)
         .GetMethods()
         .First(m => m.Name == "AsQueryable" && m.IsGenericMethodDefinition);
     var asQueryableListItemProp = asQueryable.MakeGenericMethod(typeof(TListItemProp));
     var asQueryableListItem = asQueryable.MakeGenericMethod(typeof(TListItem));
     var select = typeof(Queryable)
         .GetMethods()
         .Where(m => m.Name == "Select" && m.IsGenericMethodDefinition)
         .Single(m =>
         {
             var parameters = m.GetParameters();
             if (parameters.Length != 2)
                 return false;

             var delegateType = parameters[1].ParameterType.GetGenericArguments()[0];
             return delegateType.IsGenericType && delegateType.GetGenericTypeDefinition() == typeof(Func<,>);
         }).MakeGenericMethod(typeof(TListItem), typeof(TListItemProp));
     var intersect = typeof(Queryable)
         .GetMethods()
         .Where(x => x.Name == "Intersect")
         .Single(x => x.GetParameters().Length == 2)
         .MakeGenericMethod(typeof(TListItemProp));
     var count = typeof(Queryable)
         .GetMethods()
         .First(m => m.Name == "Count" && m.GetParameters().Length == 1)
         .MakeGenericMethod(typeof(TListItemProp));
     var listItemParameter = Expression.Parameter(typeof(TListItem), "li");
     var listItemPropertyEx = Expression.Property(listItemParameter, listItemPropertyName);
     var listItemPropertyLambda = Expression.Lambda(listItemPropertyEx, listItemParameter);
     var asQueryableExpression = Expression.Call(asQueryableListItem, memberAccess);
     var valueListAsQueryable = Expression.Call(asQueryableListItemProp, valueList);
     var selectExpression = Expression.Call(select, asQueryableExpression, listItemPropertyLambda);
     var intersectExpression = Expression.Call(intersect, selectExpression, valueListAsQueryable);
     var intersectedCount = Expression.Call(count, intersectExpression);
     Expression? finalExpression = null;
     switch (filter.FilterType)
     {
         case ValueListFilterType.Including:
             {
                 var valueCount = Expression.Constant(filter.ValueList.Count);
                 finalExpression = Expression.GreaterThanOrEqual(intersectedCount, valueCount);
                 break;
             }
         case ValueListFilterType.Exact:
             {
                 var valueCount = Expression.Constant(filter.ValueList.Count);
                 finalExpression = Expression.Equal(intersectedCount, valueCount);
                 break;
             }
         case ValueListFilterType.Excluding:
             {
                 var valueCount = Expression.Constant(0);
                 finalExpression = Expression.Equal(intersectedCount, valueCount);
                 break;
             }
         case ValueListFilterType.Any:
             {
                 var valueCount = Expression.Constant(0);
                 finalExpression = Expression.GreaterThan(intersectedCount, valueCount);
                 break;
             }
         default:
             break;
     }
     return finalExpression;
 }

Другие вопросы по теме