tags:

views:

45

answers:

2

Hi,

I have a two objects as follows:

public class Item
{
    public int ItemId {get;set;}
    public string ItemName {get;set;}
    public List<Tag> ItemTags {get;set;}
    public DateTime DateCreated {get;set;}
}

public class Tag
{
    public int TagId {get;set;}
    public string TagName {get;set;}
}

These are LINQ-to-SQL objects, so the ItemTags will be an EntitySet.

I am trying to perform a search query where a user can provide a comma delimited list of tags as a search filter.

How do I filter my list of items to those which contains all of the tags in the comma delimited list.

EDIT2

e.g.
Item1 has tags of Apple, Banana, Orange
Item2 has tags of Banana, Orange
Item3 has tags of Pineapple, Orange
If the tag filter is "Banana, Orange" I need the results to be Item1 and Item2.

/EDIT2

This is what I have tried thus far:

string tags = "Manchester United,European Cup,2008";
List<string> tagsList = tags.Trim().ToLower()
    .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
    .Distinct(StringComparer.CurrentCultureIgnoreCase)
    .ToList();

List<Item> itemList = ItemRepository.FetchAll();

var query = itemList
    .OrderByDescending(p => p.DateCreated)
    .ToList();

if (tagsList.Count() > 0)
{
    query = query
        .Where(p => p.ItemTags
            .Select(q => q.TagName.ToLower())
            .All(r => tagsList.Contains(r)))
        .ToList();
}

However, this doesn't seem to work. Any ideas on what I am doing wrong please?

EDIT1: tags are trimmed and are 'lowercased'.

+1  A: 

That because you're puting the tags from the items to lowercase, but not the searched tags.

With this modification it should work:

List<string> tagsList = tags
    .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
    .Select(s => s.ToLower())
    .Distinct()
    .ToList();

EDIT: OK, I see what the problem is: you're doing it backwards. You're searching for items that have only the tags that you're looking for.

Try that instead:

query = 
    (from item in query
     let itemTags = p.ItemTags.Select(it => it.TagName.ToLower())
     where tags.All(t => itemTags.Contains(t))
     select item).ToList();

UPDATE: here's a version with the lambda syntax. It's pretty ugly because of the temporary anonymous type, but that's how the let clause translates to lambda...

query =
    query.Select(item => new { item, itemTags = item.ItemTags.Select(it => it.TagName.ToLower()) })
         .Where(x => tagsList.All(t => x.itemTags.Contains(t)))
         .Select(x => x.item)
         .ToList();
Thomas Levesque
Sorry I should have mentioned I tried that as well, I've updated the main question.
Astrofaes
see my updated answer
Thomas Levesque
Hi Thomas, the compiler gives me a compile error when I tried your code at the final parentheses before the .ToList() call. The error is "A query body must end with a select clause or a group clause". Any ideas - sorry I only know how to use linq-to-sql with lambda notation :)
Astrofaes
I forgot the select clause... it's fixed now
Thomas Levesque
Ah brilliant this worked like a charm. I had to change a couple of typos: (from item in query let itemTags = item.ItemTags.Select(it => it.TagName.ToLower()) where tagsList.All(t => itemTags.Contains(t)) select item).ToList();
Astrofaes
As a final question, you wouldn't happen to know how to write this as a lambda expression instead of a linq query? :-)
Astrofaes
Don't forget to upvote and accept if it solves your problem ;). Translating the `let` clause to lambda syntax is a real pain, that's why I used the query syntax... I'll try to post a lambda version. Anyway, why don't you want the query syntax ? In some cases it's more convenient than the lambda syntax, and it's not more difficult to use (actually it's probably easier...)
Thomas Levesque
OK, I added the lambda syntax version
Thomas Levesque
A: 

I think you need to do something like this:

var query = itemList.OrderByDescending(p => p.DateCreated).ToList();

var results = query.Where(i => i.ItemTags
   .All(it => tagsList.Contains(it.TagName.ToLower())));

Then results should then be a list of matching items.

PS. Your code shows you fetching itemList as a List from your repository and then sorting by date created. This means the sorting isn't being done in the database. Once you turn something into a List you give up the benefits of deferred execution as you will bring back the entire collection into memory.

EDIT: Here's the test code to prove it works in Linq to Objects:

public class Item
{
    public int ItemId { get; set; }
    public string ItemName { get; set; }
    public List<Tag> ItemTags { get; set; }
    public DateTime DateCreated { get; set; }
}

public class Tag
{
    public int TagId { get; set; }
    public string TagName { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        RunTags();
    }

    private static void RunTags()
    {
        Item i1 = new Item()
        {
            ItemId = 1,
            ItemName = "Item1",
            ItemTags = new List<Tag>() { new Tag { TagId = 1, TagName = "2008" }, new Tag { TagId = 2, TagName = "Donkey" } }
        };

        Item i2 = new Item()
        {
            ItemId = 2,
            ItemName = "Item2",
            ItemTags = new List<Tag>() { new Tag { TagId = 4, TagName = "Cat" }, new Tag { TagId = 2, TagName = "Donkey" }, new Tag { TagId = 3, TagName = "Seattle" } }
        };

        Item i3 = new Item()
        {
            ItemId = 3,
            ItemName = "Item3",
            ItemTags = new List<Tag>() { new Tag { TagId = 523, TagName = "Manchester united" }, new Tag { TagId = 10, TagName = "European Cup" }, new Tag { TagId = 1, TagName = "2008" } }
        };

        Item i4 = new Item()
        {
            ItemId = 4,
            ItemName = "Item4",
            ItemTags = new List<Tag>() { new Tag { TagId = 05, TagName = "Banana" }, new Tag { TagId = 140, TagName = "Foo" }, new Tag { TagId = 4, TagName = "Cat" } }
        };

        Item i5 = new Item()
        {
            ItemId = 5,
            ItemName = "Item5",
            ItemTags = new List<Tag>() { new Tag { TagId = 05, TagName = "Banana" }, new Tag { TagId = 140, TagName = "Foo" } }
        };

        List<Item> itemList = new List<Item>() { i1, i2, i3, i4, i5 };

        string tags = "Manchester United,European Cup,2008";
        List<string> tagsList = tags.Trim().ToLower()
            .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
            .Distinct(StringComparer.CurrentCultureIgnoreCase)
            .ToList();

        var query = itemList
            .OrderByDescending(p => p.DateCreated).ToList();


        var results = query.Where(i => i.ItemTags.All(it => tagsList.Contains(it.TagName.ToLower())));

        foreach (var item in results)
        {
            Console.WriteLine(item.ItemName); // Should return "Item3"
        }

        Console.ReadLine();
    }

If you want to match any of the tags in the Item's ItemTag list then just change All to Any i.e.

var results = query.Where(i => i.ItemTags.Any(it => tagsList.Contains(it.TagName.ToLower())));
Dan Diplo
Hi Dan, I've tried your code and that doesn't work too unfortunately :( What is really strange is that the records returned by the query return only Items with empty ItemTags. The other strange thing I noticed is that if I replace the All operator with Any, the Any statement return results that I would expect from it (returns records which contains 1 or more of the tags). Now I just need to get this 'All' to work.. grr :(
Astrofaes
It definitely works in Linq to Objects - see example code I've added to the answer. Maybe it doesn't translate to Linq to Entities as SQL? Or maybe your data isn't what you think it is.
Dan Diplo
Hi Dan, if you try changing the value of: string tags = "Banana"; Will your results still work? I would require it to return items 4 and 5.
Astrofaes
@Astrofaes You said in your original statement that, "How do I filter my list of items to only those which contains ALL of the tags in the comma delimited list?" Therefore if you changed string tags to "Banana" it wouldn't match 4 and 5 since it is not matching ALL tags, as per your statement.
Dan Diplo
I apologise, I should have been more specific in my OP. Any ideas on how to filter based on the revised condition?
Astrofaes
Just change the All to Any - I'll edit my response to show alternative.
Dan Diplo