Я пытался создать динамически генерируемые деревья выражений для моделей фильтрации в моей базе данных (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). Это моя проблема? Если да, то как лучше всего выполнять операции над наборами со свойствами навигации?
Не совсем, мне нужно сравнить список в модели со списком идентификаторов. Конечно, я проводил простые сравнения значений с деревьями выражений, но операции над множествами ускользают от меня. Я хочу выполнить их в БД, а не в памяти.
Если вы собираетесь написать свой метод 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 включает все идентификаторы в список, имеет какие-либо идентификаторы в список имеет точно такие же идентификаторы, что и список, и т. д.). Проблема заключается в создании выражения, которое выполняется в БД.
Я сделал это. Я использовал методы 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;
}
Итак, вы хотите построить
p => p.Heroes.Any(h => h.something == "")
? (он жеExpression.Any(p.Heroes, ...)
) У вас должна быть возможность повторно использовать большую часть конструкции динамического дерева выражений для внутренней лямбды.