1

Given an entity framework query, such as

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

I have applied filters such as:

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

Given the following multi-dimensional array

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

How can the "query" be further restricted to only include those entries that match the specified locations {Continent, Country, State(optional)} ?

2 Answers2

0

This calls for what is called a correlated subquery in SQL. Assuming they will always occupy the same position, you can use array indexers to access the elements within your locations jagged array.

query = query.Where(model => 
    locations.Any(location =>
        location[0] == model.Continent &&
        location[1] == model.Country &&
        (string.IsNullOrEmpty(location[2]) || location[2] == model.State)));

Update: Since LINQ to Entities does not support array indexers, you could convert your jagged array into a collection of anonymous types. (In the long term, it would be preferable to create a class for instantiating your filters. This would be more intuitive than remembering what the elements at each index represent.)

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)));
Douglas
  • 53,759
  • 13
  • 140
  • 188
  • Thank you, this looked like what I was looking for. However, at runtime I'm getting the following error: The LINQ expression node type 'ArrayIndex' is not supported in LINQ to Entities. – Mark Atkins Apr 09 '16 at 08:32
  • @MarkAtkins: I've added some (untested) code for converting the jagged array to an anonymous type; see whether that works. – Douglas Apr 09 '16 at 08:41
  • Thanks @Douglas, but again I've not had any luck with that, it didn't seem to like the anonymous type. So, I've tried to use an actual class to store the details in a new List(), but this has given the runtime error: Unable to create a constant value of type 'MyNamespace.SearchFilters'. Only primitive types or enumeration types are supported in this context. – Mark Atkins Apr 09 '16 at 09:39
0

Unfortunately LINQ to Entities currently does not support joins to inmemory collection, nor Contains for non primitive inmemory collection. The only way I see (actually there is another one described here Entity Framework LINQ Get all items part of another collection, but now I think this is more appropriate) is to construct OR filter using some expression build helper.

For instance, using the PredicateUtils class from Establish a link between two lists in linq to entities where clause, it could be like this:

First, add a little helper method

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

and then use

var locationsFilter = locations.Select(location => location.Select(LocationFilter)
    .Aggregate(PredicateUtils.And)).Aggregate(PredicateUtils.Or);
if (locationsFilter != null)
    query = query.Where(locationsFilter);

For completeness, here is the helper class used:

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

UPDATE: As requested in the comments, here is the solution for locations being 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);
Community
  • 1
  • 1
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Thanks Ivan, this seems to work (I've literally spent hours fighting with this and this is the first solution that has worked). However, is it possible to make this work with a new List(), as it would be much easier to build up that list, instead of using a multi-dimensional array, such as: var locations = new[] – Mark Atkins Apr 09 '16 at 13:41
  • @MarkAtkins You are welcome. And yes, it's possible, see the update. – Ivan Stoev Apr 09 '16 at 14:15
  • Thank you Ivan, you've solved my difficult problem. I don't think I would've easily come up with this myself. You're a star :) – Mark Atkins Apr 09 '16 at 21:08