tags:

views:

109

answers:

3

I have an application that manages documents called Notes. Like a blog, Notes can be searched for matches against one or more Tags, which are contained in a Note.Tags collection property. A Tag has Name and ID properties, and matches are made against the ID. A user can specify multiple tags to match against, in which case a Note must contain all Tags specified to match.

I have a very complex LINQ query to perform a Note search, with extension methods and looping. Quite frankly, it has a real code smell to it. I want to rewrite the query with something much simpler. I know that if I made the Tag a simple string, I could use something like this:

var matchingNotes = from n in myNotes
                    where n.Tags.All(tag => searchTags.Contains(tag))

Can I do something that simple if my model uses a Tag object with an ID? What would the query look like. Could it be written in fluent syntax? what would that look like?

A: 

For starters see my comment; I suspect the query is wrong anyway! I would simplifiy it, by simply enforcing separately that each tag exists:

IQueryable<Note> query = ... // top part of query
foreach(var tagId in searchTagIds) {
    var tmpId = tagId; // modified closures...
    query = query.Where(note => note.Tags.Any(t => t.Id == tmpId));
}

This should have the net effect of enforcing all the tags specified are present and accounted for.

Marc Gravell
+1  A: 

I believe you can find notes that have the relevant tags in a single LINQ expression:

IQueryable<Note> query = ... // top part of query

query = query.Where(note => searchTags.All(st =>
    note.Tags.Any(notetag => notetag.Id == st.Id)));

Unfortunately there is no “fluent syntax” equivalent for All and Any, so the best you can do there is

query = from note in query
        where searchTags.All(st =>
            note.Tags.Any(notetag => notetag.Id == st.Id))
        select note;

which is not that much better either.

Timwi
Oops my bad--I pasted in the wrong ICommand
David Veeneman
The query throws this error: 'Unable to create a constant value of type 'Tag'. Only primitive types ('such as Int32, String, and Guid') are supported in this context.'
David Veeneman
It works fine for me, so there must be something about your code that you haven’t told us. Are you sure that `Tag.Id` is of type `string` for example?
Timwi
Actually, TagID is Type GUID.
David Veeneman
@David Veeneman: I believe I have answered your question as stated. If you think I haven’t, please edit the question to clarify what information is still missing. If you have any further questions, you can post a new, separate question.
Timwi
I believe I found the problem--it is with LINQ to Entities, not with your query. There appears to be a problem with the All() method in LTE. See http://stackoverflow.com/questions/879411/entity-framework-unable-to-create-a-constant-value-of-type-closure-type.
David Veeneman
A: 

Timwi's solution works in most dialects of LINQ, but not in Linq to Entities. I did find a single-statement LINQ query that works, courtesy of ReSharper. Basically, I wrote a foreach block to do the search, and ReSharper offered to convert the block to a LINQ statement--I had no idea it could do this.

I let ReSharper perform the conversion, and here is what it gave me:

return searchTags.Aggregate<Tag, IQueryable<Note>>(DataStore.ObjectContext.Notes, (current, tag) => current.Where(n => n.Tags.Any(t => t.Id == tag.Id)).OrderBy(n => n.Title));

I read my Notes collection from a database, using Entity Framework 4. DataStore is the custom class I use to manage my EF4 connection; it holds the EF4 ObjectContext as a property.

David Veeneman