基于多维数组应用 LINQ 过滤器
Applying LINQ filters based on a multi-dimensional array
给定一个 entity framework 查询,例如
var query = (from property in _dbContext.Properties
join location in _db.Locations
on property.Id equals location.PropertyId
select new PropertyDetail
{
Url = property.Url,
Type = property.Type,
Title = property.Title,
Continent = location.Continent,
Country = location.Country,
State = location.State,
});
我应用了以下过滤器:
if (!string.IsNullOrWhitespace(searchFilters.Type))
{
query = query.Where(model => model.Type == searchFilters.Type);
}
if (!string.IsNullOrWhitespace(searchFilters.Title))
{
query = query.Where(model => model.Title.Contains(searchFilters.Title));
}
给定如下多维数组
var locations = new[]
{
new[] {"Africa", "Algeria", ""},
new[] {"Asia", "Hong Kong", ""},
new[] {"Asia", "Singapore", ""},
new[] {"Oceania", "Australia", "New South Wales"},
new[] {"North America", "United States", "California"}
};
如何 "query" 进一步限制为仅包含与指定位置 {Continent, Country, State(optional)} 匹配的条目?
这需要 SQL 中所谓的相关子查询。假设它们总是占据相同的位置,您可以使用数组索引器访问 locations
交错数组中的元素。
query = query.Where(model =>
locations.Any(location =>
location[0] == model.Continent &&
location[1] == model.Country &&
(string.IsNullOrEmpty(location[2]) || location[2] == model.State)));
更新:由于 LINQ to Entities 不支持数组索引器,您可以将交错数组转换为匿名类型的集合。 (从长远来看,最好创建一个 class 来实例化您的过滤器。这比记住每个索引处的元素代表什么更直观。)
var locationsTyped =
locations.Select(location => new
{
Continent = location[0],
Country = location[1],
State = location[2],
}).ToArray();
query = query.Where(model =>
locationsTyped.Any(location =>
location.Continent == model.Continent &&
location.Country == model.Country &&
(string.IsNullOrEmpty(location.State) || location.State == model.State)));
遗憾的是,LINQ to Entities 当前不支持连接内存集合,Contains
也不支持非原始内存集合。我看到的唯一方法(实际上这里描述了另一种 Entity Framework LINQ Get all items part of another collection,但现在我认为这更合适)是使用一些表达式构建助手来构造 OR
过滤器。
例如,使用 中的 PredicateUtils
class,它可能是这样的:
首先,添加一个小辅助方法
static Expression<Func<PropertyDetail, bool>> LocationFilter(string value, int index)
{
if (!string.IsNullOrEmpty(value))
{
if (index == 0) return d => d.Continent == value;
if (index == 1) return d => d.Country == value;
if (index == 2) return d => d.State == value;
}
return null;
}
然后使用
var locationsFilter = locations.Select(location => location.Select(LocationFilter)
.Aggregate(PredicateUtils.And)).Aggregate(PredicateUtils.Or);
if (locationsFilter != null)
query = query.Where(locationsFilter);
为了完整起见,这里是使用的助手 class:
public static class PredicateUtils
{
sealed class Predicate<T>
{
public static readonly Expression<Func<T, bool>> True = item => true;
public static readonly Expression<Func<T, bool>> False = item => false;
}
public static Expression<Func<T, bool>> Null<T>() { return null; }
public static Expression<Func<T, bool>> True<T>() { return Predicate<T>.True; }
public static Expression<Func<T, bool>> False<T>() { return Predicate<T>.False; }
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
{
if (Equals(left, right)) return left;
if (left == null || Equals(left, True<T>())) return right;
if (right == null || Equals(right, True<T>())) return left;
if (Equals(left, False<T>()) || Equals(right, False<T>())) return False<T>();
var body = Expression.AndAlso(left.Body, right.Body.Replace(right.Parameters[0], left.Parameters[0]));
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
{
if (Equals(left, right)) return left;
if (left == null || Equals(left, False<T>())) return right;
if (right == null || Equals(right, False<T>())) return left;
if (Equals(left, True<T>()) || Equals(right, True<T>())) return True<T>();
var body = Expression.OrElse(left.Body, right.Body.Replace(right.Parameters[0], left.Parameters[0]));
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
}
static Expression Replace(this Expression expression, Expression source, Expression target)
{
return new ExpressionReplacer { Source = source, Target = target }.Visit(expression);
}
class ExpressionReplacer : ExpressionVisitor
{
public Expression Source;
public Expression Target;
public override Expression Visit(Expression node)
{
return node == Source ? Target : base.Visit(node);
}
}
}
更新: 根据评论中的要求,这是 locations
的解决方案 List<Location>
:
var locationsFilter = locations.Select(location =>
{
var filter = PredicateUtils.Null<PropertyDetail>();
if (!string.IsNullOrEmpty(location.Continent))
filter = filter.And(d => d.Continent == location.Continent);
if (!string.IsNullOrEmpty(location.Country))
filter = filter.And(d => d.Country == location.Country);
if (!string.IsNullOrEmpty(location.State))
filter = filter.And(d => d.State == location.State);
return filter;
}).Aggregate(PredicateUtils.Or);
给定一个 entity framework 查询,例如
var query = (from property in _dbContext.Properties
join location in _db.Locations
on property.Id equals location.PropertyId
select new PropertyDetail
{
Url = property.Url,
Type = property.Type,
Title = property.Title,
Continent = location.Continent,
Country = location.Country,
State = location.State,
});
我应用了以下过滤器:
if (!string.IsNullOrWhitespace(searchFilters.Type))
{
query = query.Where(model => model.Type == searchFilters.Type);
}
if (!string.IsNullOrWhitespace(searchFilters.Title))
{
query = query.Where(model => model.Title.Contains(searchFilters.Title));
}
给定如下多维数组
var locations = new[]
{
new[] {"Africa", "Algeria", ""},
new[] {"Asia", "Hong Kong", ""},
new[] {"Asia", "Singapore", ""},
new[] {"Oceania", "Australia", "New South Wales"},
new[] {"North America", "United States", "California"}
};
如何 "query" 进一步限制为仅包含与指定位置 {Continent, Country, State(optional)} 匹配的条目?
这需要 SQL 中所谓的相关子查询。假设它们总是占据相同的位置,您可以使用数组索引器访问 locations
交错数组中的元素。
query = query.Where(model =>
locations.Any(location =>
location[0] == model.Continent &&
location[1] == model.Country &&
(string.IsNullOrEmpty(location[2]) || location[2] == model.State)));
更新:由于 LINQ to Entities 不支持数组索引器,您可以将交错数组转换为匿名类型的集合。 (从长远来看,最好创建一个 class 来实例化您的过滤器。这比记住每个索引处的元素代表什么更直观。)
var locationsTyped =
locations.Select(location => new
{
Continent = location[0],
Country = location[1],
State = location[2],
}).ToArray();
query = query.Where(model =>
locationsTyped.Any(location =>
location.Continent == model.Continent &&
location.Country == model.Country &&
(string.IsNullOrEmpty(location.State) || location.State == model.State)));
遗憾的是,LINQ to Entities 当前不支持连接内存集合,Contains
也不支持非原始内存集合。我看到的唯一方法(实际上这里描述了另一种 Entity Framework LINQ Get all items part of another collection,但现在我认为这更合适)是使用一些表达式构建助手来构造 OR
过滤器。
例如,使用 PredicateUtils
class,它可能是这样的:
首先,添加一个小辅助方法
static Expression<Func<PropertyDetail, bool>> LocationFilter(string value, int index)
{
if (!string.IsNullOrEmpty(value))
{
if (index == 0) return d => d.Continent == value;
if (index == 1) return d => d.Country == value;
if (index == 2) return d => d.State == value;
}
return null;
}
然后使用
var locationsFilter = locations.Select(location => location.Select(LocationFilter)
.Aggregate(PredicateUtils.And)).Aggregate(PredicateUtils.Or);
if (locationsFilter != null)
query = query.Where(locationsFilter);
为了完整起见,这里是使用的助手 class:
public static class PredicateUtils
{
sealed class Predicate<T>
{
public static readonly Expression<Func<T, bool>> True = item => true;
public static readonly Expression<Func<T, bool>> False = item => false;
}
public static Expression<Func<T, bool>> Null<T>() { return null; }
public static Expression<Func<T, bool>> True<T>() { return Predicate<T>.True; }
public static Expression<Func<T, bool>> False<T>() { return Predicate<T>.False; }
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
{
if (Equals(left, right)) return left;
if (left == null || Equals(left, True<T>())) return right;
if (right == null || Equals(right, True<T>())) return left;
if (Equals(left, False<T>()) || Equals(right, False<T>())) return False<T>();
var body = Expression.AndAlso(left.Body, right.Body.Replace(right.Parameters[0], left.Parameters[0]));
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
{
if (Equals(left, right)) return left;
if (left == null || Equals(left, False<T>())) return right;
if (right == null || Equals(right, False<T>())) return left;
if (Equals(left, True<T>()) || Equals(right, True<T>())) return True<T>();
var body = Expression.OrElse(left.Body, right.Body.Replace(right.Parameters[0], left.Parameters[0]));
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
}
static Expression Replace(this Expression expression, Expression source, Expression target)
{
return new ExpressionReplacer { Source = source, Target = target }.Visit(expression);
}
class ExpressionReplacer : ExpressionVisitor
{
public Expression Source;
public Expression Target;
public override Expression Visit(Expression node)
{
return node == Source ? Target : base.Visit(node);
}
}
}
更新: 根据评论中的要求,这是 locations
的解决方案 List<Location>
:
var locationsFilter = locations.Select(location =>
{
var filter = PredicateUtils.Null<PropertyDetail>();
if (!string.IsNullOrEmpty(location.Continent))
filter = filter.And(d => d.Continent == location.Continent);
if (!string.IsNullOrEmpty(location.Country))
filter = filter.And(d => d.Country == location.Country);
if (!string.IsNullOrEmpty(location.State))
filter = filter.And(d => d.State == location.State);
return filter;
}).Aggregate(PredicateUtils.Or);