2

I have a string:

strCheckedCategories = "2;"

an EntityList representing a SharePoint list, with item IDs from 1 to 21:

EntityList<VendorSearchesItem> vendorSearches = 
    dataContext.GetList<VendorSearchesItem>("Vendor Searches");

a LINQ query returning fields from two SharePoint lists that are joined to the "Vendor Searches" list:

var vendorSearchesQuery = (from s in vendorSearches
                           orderby s.VendorID.Title
                           select new
                           {
                               Vendor = s.VendorID.Title,
                               Website = s.VendorID.VendorWebsite,
                               VendorID = s.VendorID.Id,
                               SearchType = s.SearchTypeID.Title,
                               SearchTypeId = s.SearchTypeID.Id
                           });

and another LINQ query returning only the items where the item ID is in the list:

var q2 = from m2 in vendorSearchesQuery 
         where strCheckedCategories.Contains(m2.SearchTypeId.ToString())
         select m2

The problem is that, in addition to returning the item with ID 2 (desired result) the query also returns items with ID 12, 20, and 21. How can I fix that?

LFurness
  • 669
  • 2
  • 10
  • 21

5 Answers5

4

So, fundamentally, what you want to do here is have an IN clause in which you specify a bunch of values for a field and you want rows who's value for that column is in that set.

CAML does actually have an IN clause which you could use, but sadly LINQ to Sharepoint doesn't provide any means of generating an IN clause; it's simply not supported by the query provider.

You're trying to use a bit of a hack to get around that problem by trying to do a string comparison rather than using the proper operators, and you're running into the pitfals of stringifying all of your operations. It's simply not well suited to the task.

Since, as I said, you cannot get LINQ to SharePoint to use an IN, one option would simply be to not use LINQ, build the CAML manually, and execute it using the standard server object model. But that's no fun.

What we can do is have a series of OR checks. We'll see if that column value is the first value, or the second, or the third, etc. for all values in your set. This is effectively identical to an IN clause, it's just a lot more verbose.

Now this brings us to the problem of how to OR together an unknown number of comparisons. If it were ANDs it'd be easy, we'd just call Where inside of a loop and it would AND those N clauses.

Instead we'll need to use expressions. We can manually build the expression tree ourselves of a dynamic number of OR clauses and then the query provider will be able to parse it just fine.

Our new method, WhereIn, which will filter the query to all items where a given property value is in a set of values, will need to accept a query, a property selector of what property we're using, and a set of values of the same type to compare it to. After we have that it's a simple matter of creating the comparison expression of the property access along with each key value and then ORing all of those expressions.

public static IQueryable<TSource> WhereIn<TSource, TKey>(
    this IQueryable<TSource> query,
    Expression<Func<TSource, TKey>> propertySelector,
    IEnumerable<TKey> values)
{
    var t = Expression.Parameter(typeof(TSource));
    Expression body = Expression.Constant(false);
    var propertyName = ((MemberExpression)propertySelector.Body).Member.Name;

    foreach (var value in values)
    {
        body = Expression.OrElse(body,
            Expression.Equal(Expression.Property(t, propertyName),
                Expression.Constant(value)));
    }

    return query.Where(Expression.Lambda<Func<TSource, bool>>(body, t));
}

Now to call it we just need the query, the property we're filtering on, and the collection of values:

var q2 = vendorSearchesQuery.WhereIn(vendor => vendor.SearchTypeId
    , strCheckedCategories.Split(';'));

And voila.

While I'd expect that to work as is, you may need to call the WhereIn before the Select. It may not work quite right with the already mapped SearchTypeId.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • looks like you missed the `this` keyword to define a method extension? – King King Oct 11 '13 at 16:49
  • @KingKing Right. I actually intentionally didn't make it an extension method when I wrote it, but then forgot that when I was calling it. Fixed. – Servy Oct 11 '13 at 16:50
  • Could you add some explanation for why this is better than using `Contains`? I'm not sure if `Contains` works in `LINQ-to-Sharepoint` but as the OP said, it compiles OK, using it is also shorter than having to define `WhereIn`, I guess it must have something better to do this way, thanks. – King King Oct 11 '13 at 16:55
  • 1
    @KingKing It's the difference between writing a command that can be properly translated into a query on the database verses needing to pull down the entire data set into memory and performing the filtering on that in-memory collection. The linq to sharepoint query provider isn't as robust as say EF or linq to SQL. In either of those you could create a list and call `Contains` on it to generate an `IN` clause in SQL. The Sharepoint query provider simply didn't build in support for that operation, so you need to build the support yourself. – Servy Oct 11 '13 at 16:56
  • +1 for detecting a performance problem while answering a separate programming problem. – Trevor Elliott Oct 11 '13 at 17:27
1

You should probably use a Regex, but if you want a simpler solution then I would avoid string searching and split those strings to an array:

string strCheckedCategories = "2;4;5;7;12;16;17;19;20;21;";
string[] split = strCheckedCategories.Split(';');

It will create an empty entry in the array for the trailing semicolon delimiter. I would check for that and remove it if this is a problem:

strCheckedCategories.TrimEnd(';');

Finally now you can change your where clause:

where split.Contains(m2.SearchTypeId.ToString())

If you have a very large list it is probably worth comparing integers instead of strings by parsing strCheckedCategories into a list of integers instead:

int[] split = strCheckedCategories.Split(';').Select(x => Convert.ToInt32(x)).ToArray();

Then you can do a quicker equality expression:

where split.Contains(m2.SearchTypeId)
Trevor Elliott
  • 11,292
  • 11
  • 63
  • 102
0

try:

strCheckedCategories.Split(new []{';'}).Any(x => x == m2.SearchTypeId.ToString())

Contains will do a substring match. And "20" has a substring "2".

lisp
  • 4,138
  • 2
  • 26
  • 43
  • This will be unable to translate the query into CAML that could be executed on the database. The query provider will realize that it cannot evaluate the expression and will instead pull down the entire result set and do the filtering within the application. If the size of the list is not small that can be a major problem. It can also be confusing for future readers who may not realize the query provider cannot translate it. If you *want* the filtering to be on the application side you should at least use `AsEnumerable` to make it clear to the reader that this is happening. – Servy Oct 11 '13 at 18:40
0
var q2 = from m2 in vendorSearchesQuery 
         where strCheckedCategories.Split(';').Contains(m2.SearchTypeId.ToString())
         select m2
Dan Hunex
  • 5,172
  • 2
  • 27
  • 38
  • I'm not sure if the optimizer is smart enough to only perform that `Split` operation once instead of for each element in the `vendorSearchesQuery`... – Trevor Elliott Oct 11 '13 at 16:33
  • 1
    @TrevorElliott Well, first off, I can assure you that the query provider isn't going to be able to properly translate this query into one that can be executed on the database end, which is likely a problem, as that should be the goal here. Next, since this needs to be executed on the client side using LINQ to objects instead, you are correct that it will be splitting the string on each iteration, not once and then re-using it. So you're correct, but that's really the least of the problems. – Servy Oct 11 '13 at 16:55
0
var q2 = from m2 in vendorSearchesQuery 
         where strCheckedCategories.Contains(";" + m2.SearchTypeId + ";")
         select m2

And your strCheckedCategories should always end with ; and start with ;, for example ;2;, ;2;3;, ...

NOTE: This trick works only when your SearchTypeId should always not contain ;. I think you should use another kind of separator like \n or simply store your checked categories in a list or some array. That's the more standard way to do.

King King
  • 61,710
  • 16
  • 105
  • 130