Question

I'm getting an error, see below, when I try to generate a list of the class MappedItem. In short the code example below tries to find products by category, date range and SKU. The requirement I have is that the user should be able to enter a comma separated list of SKUs and the search is to find any product whos SKU starts with one of the SKUs entered by the user. When I run the code, I get.

Local sequence cannot be used in LINQ to SQL implementation of query operators except the Contains() operator.

The abbreviated sequence is this:

Convert the comma separated string of SKUs into a list of strings.

string sku = TextSKU.Text;
List<string> skuList = sku.Split(new char[] { ',' }).ToList();

Define elsewhere in the code the class that will accept the search results.

public class MappedItem
{
    public string ItemDescription { get; set; }
    public int ItemCount { get; set; }

    public MappedItem()
    {

    }

    public MappedItem(string itemDescription, int itemCount)
    {
        ItemDescription = itemDescription;
        ItemCount = itemCount;
    }
}

Here's the query that I generate my results from

List<MappedItem> widgetItems = (from c1 in db.CCRCodes
                                join pac in db.widgetAssignedCodes on c1.code_id equals pac.code_id
                                join ph in db.widgetHistories on pac.history_id equals ph.history_id
                                where ph.contact_dt.Value.Date >= startDate && ph.contact_dt.Value.Date <= endDate &&
                                    (string.IsNullOrEmpty(baanCatFam) || ph.baan_cat_family_code == baanCatFam) &&
                                    (string.IsNullOrEmpty(baanCat) || ph.baan_cat_code == baanCat) &&
                                    (string.IsNullOrEmpty(baanSubCat) || (ph.baan_sub_cat_code == baanSubCat)) &&
                                    (string.IsNullOrEmpty(sku) || skuList.All(sl => ph.product_mod.StartsWith(sl)))
                                group c1 by c1.code_desc into ct
                                select new MappedItem
                                {
                                    ItemDescription = ct.Key.ToUpper(),
                                    ItemCount = ct.Count()
                                }).OrderByDescending(m => m.ItemCount)
                                .ToList();

I believe that the culprit is the line of code that I've extracted and displayed below.

skuList.All(sl => ph.product_mod.StartsWith(sl))

This identifies all skus that start with an element from the skuList which is derived from a comma delimited lists of skus entered by the user. My question is, what causes this error, and given the code examples, what do I do to get around them.

Was it helpful?

Solution

First - logically you want Any, not All.

Second, this is a poor way to build up a query filter. All of those operations are sent into the database, while the information to determine which filters should be applied is already local. The explicit joins are also bad (association properties could be used instead).

IQueryable<WidgetHistory> query =  db.widgetHistories
  .Where(ph => ph.contact_dt.Value.Date >= startDate
    && ph.contact_dt.Value.Date <= endDate);

if (!string.IsNullOrEmpty(baanCatFam))
{
  query = query.Where(ph => ph.baan_cat_family_code == baanCatFam);
}
if (!string.IsNullOrEmpty(baanCat))
{
  query = query.Where(ph => ph.baan_cat_code == baanCat);
}
if (!string.IsNullOrEmpty(baanSubCat))
{
  query = query.Where(ph => ph.baan_sub_cat_code == baanSubCat);
}

//TODO sku filtering here.

List<MappedItem> widgetItems =
  from ph in query
  let c1 = ph.widgetAssignedCode.CCRCode
  group c1 by c1.code_desc into g
  select new MappedItem
  {
    ItemDescription = g.Key.ToUpper(),
    ItemCount = g.Count()
  }).OrderByDescending(m => m.ItemCount)
  .ToList();

Third: the answer to your question.

what causes this error

LinqToSql's query provider cannot translate your local collection into sql. There's only a limitted set of scenarios where it can translate... .Where(ph => idList.Contains(ph.Id)) is translated into an IN clause with 1 parameter per int in idList.

To get around this limitation, you need to convert the local collection into an expression. Start by tranforming each item in the collection into a filtering expression:

List<Expression<Func<WidgetHistory, bool>>> skuFilters =
  skuList.Select<string, Expression<Func<WidgetHistory, bool>>>(skuItem =>
    ph => ph.ProductMod.StartsWith(skuItem)
  ).ToList();

Next, a helper method:

public static Expression<Func<T, bool>> OrTheseFiltersTogether<T>(
  this IEnumerable<Expression<Func<T, bool>>> filters)
{
  Expression<Func<T, bool>> firstFilter = filters.FirstOrDefault();
  if (firstFilter == null)
  {
    Expression<Func<T, bool>> alwaysTrue = x => true;
    return alwaysTrue;
  }
  var body = firstFilter.Body;
  var param = firstFilter.Parameters.ToArray();
  foreach (var nextFilter in filters.Skip(1))
  {
    var nextBody = Expression.Invoke(nextFilter, param);
    body = Expression.OrElse(body, nextBody);
  }
  Expression<Func<T, bool>> result = Expression.Lambda<Func<T, bool>>(body, param);
  return result;
}

And now putting it all together:

if (skuFilters.Any())  //this part goes into where it says "TODO"
{
  Expression<Func<WidgetHistory, bool>> theSkuFilter = skuFilters.OrTheseFiltersTogether()
  query = query.Where(theSkuFilter);
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top