Tricky LINQ and nested collection - linq

I am trying to figure out an easier way I can take the following code and condense it into as few lines as possible. Ideally, I would like to get an IDictionary> out of this.
var report = db.Reports.Select(rid => rid.ID == reportId) as Report;
Dictionary<string, List<string>> notes = new Dictionary<string, List<string>>();
foreach (var subreport in report.SubReports)
{
foreach (var subreportitem in subreport.SubReportItems)
{
notes[subreportitem.Title] = new List<string>();
foreach (var note in subreportitem.SubReportItemNotes)
{
notes[subreportitem.Title].Add(note.NoteDetails);
}
}
}
Ideally, I had wanted to do something like so:
from report in db.Reports
where report.ID == reportId
from subreports in report.SubReports
from subreportitems in subreports.SubReportItems
from notes in subreportitems.SubReportItemNotes
//Unsure how to select into the desired dictionary...

This should be equivalent:
db.Reports
.Where(rpt => rpt.ID == reportId)
.Cast<Report>()
.SelectMany(rpt => rpt.SubReports)
.SelectMany(subRpt => subRpt.SubReportItems)
.ToDictionary(
sri => sri.Title,
sri => sri.SubReportItemNotes.SelectMany(note => note.NoteDetails);
Notes:
The first line of your code uses .Select(rid => rid.Id == reportId), but I assumed this should have been Where instead of Select, otherwise you would end up with a collection of null because the Select results would be of type bool and the as Report would output null for each.
This only works if the Titles of all of your SubReportItems are unique. It is conceivable that one Report could have 10 SubReports and that among those SubReports there are two or more SubReportItems with the same Title. If that is the case, then you may need to rethink this a bit, otherwise you will get a DuplicateKeyException when you try to add a Title that is already in the dictionary.
Explanation:
Basically, we are taking the set of reports, and applying the condition that we only want reports where the ID is the desired one. Personally, I would put this in a separate line, and use SingleOrDefault instead of Where because I expect only one result.
Next, we call .Cast<Report> just because you are using as Report, so I guess you had a need for it. This may be redundant and unnecessary in practice.
The first .SelectMany call will get all SubReports of all Reports. Again, we probably will only ever have one Report object at this point.
Now we have a bunch of SubReports, but we really want to get all the SubReportItems, so we use another SelectMany to get those.
Now that we have all the SubReportItems from all the SubReports from all the (1) Reports, we create the dictionary. For each SubReportItem we create a key from the Title property, and then for the value we use one final SelectMany to get all the NoteDetails objects associated with all the current SubReportItemNotes.

Related

LINQ to EF - Find records where string property of a child collection at least partially matches all records in a list of strings

I am currently writing a web-based 'recipe' application using LINQ and Entity Framework 5.0. I've been struggling with this query for awhile, so any help is much appreciated!
There will be a search function where the users can enter a list of ingredients that they want the recipe results to match. I need to find all recipes where the associated ingredient collection (name property) contains the text of every record in a list of strings (the user search terms). For example, consider the following two recipes:
Tomato Sauce: Ingredients 'crushed tomatoes', 'basil', 'olive oil'
Tomato Soup: Ingredients 'tomato paste', 'milk', 'herbs
If the user used the search terms 'tomato' and 'oil' it would return the tomato sauce but not the tomato soup.
var allRecipes = context.Recipes
.Include(recipeCategory => recipeCategory.Category)
.Include(recipeUser => recipeUser.User);
IQueryable<Recipe> r =
from recipe in allRecipes
let ingredientNames =
(from ingredient in recipe.Ingredients
select ingredient.IngredientName)
from i in ingredientNames
let ingredientsToSearch = i where ingredientList.Contains(i)
where ingredientsToSearch.Count() == ingredientList.Count()
select recipe;
I've also tried:
var list = context.Ingredients.Include(ingredient => ingredient.Recipe)
.Where(il=>ingredientList.All(x=>il.IngredientName.Contains(x)))
.GroupBy(recipe=>recipe.Recipe).AsQueryable();
Thank you for your help!
Just off the top of my head i would go for something like this
public IEnumerable<Recipe> SearchByIngredients(params string[] ingredients)
{
var recipes = context.Recipes
.Include(recipeCategory => recipeCategory.Category)
.Include(recipeUser => recipeUser.User);
foreach(var ingredient in ingredients)
{
recipes = recipes.Where(r=>r.Ingredients.Any(i=>i.IngredientName.Contains(ingredient)));
}
//Finialise the queriable
return recipes.AsEnumerable();
}
You can then call it using:
SearchByIngredients("tomatoes", "oil");
or
var ingredients = new string[]{"tomatoes", "oil"};
SearchByIngredients(ingredients );
What this is going to do is attach where clauses to the queriable recipes for each of your search terms. Multiple where clauses are treated as ANDs in SQL (which is what you want here anyway). Linq is quite nice in the way that we can do this, at then end of the function we finalise the queriable essentially saying all that stuff that we just did can get turned into a single query back to the DB.
My only other note would be you really want to be indexing/full text indexing the Ingredient name column or this wont scale terribly well.

How can I use set operations to delete objects in an entitycollection that match a collection of view models?

Here is a very basic example of what I want to do. The code I have come up with seems quite verbose... ie looping through the collection, etc.
I am using a Telerik MVC grid that posts back a collection of deleted, inserted and updated ViewModels. The view models are similar but not exactly the same as the entity.
For example... I have:
Order.Lines. Lines is an entity collection (navigation property) containing OrderDetail records. In the update action of my controller using the I have a List names DeletedLines pulled from the POST data. I also have queried the database and have the Order entity including the Lines collection.
Now I basically want to tell it to delete all the OrderDetails in the Lines EntityCollection.
The way I have done it is something like:
foreach (var line in DeletedLines) {
db.DeleteObject(Order.Lines.Where(l => l.Key == line.Key).SingleOrDefault())
}
I was hoping there was a way that I could use .Interset() to get a collection of entities to delete and pass that to DeleteObject.. however, DeleteObject seems to only accept a single entity rather than a collection.
Perhaps the above is good enough.. but it seemed like there should be an easier method.
Thanks,
BOb
Are the items in DeletedLines attached to the context? If so, what about this?
foreach (var line in DeletedLines) db.DeleteObject(line);
Response to comment #1
Ok, I see now. You can make your code a bit shorter, but not much:
foreach (var line in DeletedLines) {
db.DeleteObject(Order.Lines.SingleOrDefault(l => l.Key == line.Key))
}
I'm not sure if DeleteObject will throw an exception when you pass it null. If it does, you may be even better off using Single, as long as you're sure the item is in there:
foreach (var line in DeletedLines) {
db.DeleteObject(Order.Lines.Single(l => l.Key == line.Key))
}
If you don't want to re-query the database and either already have the mapping table PK values (or can include them in the client call), you could use one of Alex James's tips for deleting without first retrieving:
http://blogs.msdn.com/b/alexj/archive/2009/03/27/tip-9-deleting-an-object-without-retrieving-it.aspx

Filtering Aggregate root entity and child entity by a property on the child entity

Hope that someone out there can help with this!
I'll give an example based on the standard Order-->OrderLine-->Product rather than the actual situation to make it easier to explain!
Basically, I want to run a query that returns all orders for which there is an order line containing a TV. Simple enough:
IEnumerable<Order> orders;
using (var context = new DataContext())
{
var source =
context.Orders.Include("OrderLines").Include(
"OrderLines.Product");
orders= source.Where(o => o.OrderLines.Where(ol => ol.Product.Name == "TV")).ToList();
}
return orders;
This works in the sense that I get the correct collection of Order entities, but when I use look at each Order's collection of OrderLines it contains all OrderLines not just those containing at TV.
Hope that makes sense.
Thanks in advance for any help.
I does make sense in that the query is fulfilling your original criteria "to return all orders for which there is an order line containing a TV", each order will of course have all the orderlines. The filter is only being used to select the Orders, not the OrderLines.
To retrieve just the OrderLines containing TV from an Order you'd use the filter again, thus:
var OrderLinesWithTV = order.OrderLines.Where(ol => ol.Product.Name == "TV");
The main point is to know if you need to keep (or not) a reference to the order header in the filtered lines.
I.e. do you want the list of all the orders with a TV, and more precisely only their TV lines ? or do you want all the TV lines nevermind their order header ?
You seem to prefer the first option.
Then the best solution would certainly be
var relevantOrders = orders.Where(order => order.OrderLines.Any(ol => ol.Product.Name == "TV"))
to get the relevant orders, and then, for each order in relevantOrders :
order.OrderLines.Where(ol => ol.Product.Name == "TV")
to consider only the TV lines.
Other techniques would result in a loss of information or force you to build a new orders collection similar to the initial one but double-filtered on the headers and on the lines, which seems fairly bad as far as elegance and performance is concerned.

LINQ Query to find all tags?

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?
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.
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.
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.

LINQ to SQL many to many int ID array criteria query

Ok this should be really simple, but I am doing my head in here and have read all the articles on this and tried a variety of things, but no luck.
I have 3 tables in a classic many-to-many setup.
ITEMS
ItemID
Description
ITEMFEATURES
ItemID
FeatureID
FEATURES
FeatureID
Description
Now I have a search interface where you can select any number of Features (checkboxes).
I get them all nicely as an int[] called SearchFeatures.
I simply want to find the Items which have the Features that are contained in the SearchFeatures.
E.g. something like:
return db.Items.Where(x => SearchFeatures.Contains(x.ItemFeatures.AllFeatures().FeatureID))
Inside my Items partial class I have added a custom method Features() which simply returns all Features for that Item, but I still can't seem to integrate that in any usable way into the main LINQ query.
Grr, it's gotta be simple, such a 1 second task in SQL. Many thanks.
The following query will return the list of items based on the list of searchFeatures:
from itemFeature in db.ItemFeatures
where searchFeatures.Contains(itemFeature.FeatureID)
select itemFeature.Item;
The trick here is to start with the ItemFeatures table.
It is possible to search items that have ALL features, as you asked in the comments. The trick here is to dynamically build up the query. See here:
var itemFeatures = db.ItemFeatures;
foreach (var temp in searchFeatures)
{
// You will need this extra variable. This is C# magic ;-).
var searchFeature = temp;
// Wrap the collection with a filter
itemFeatures =
from itemFeature in itemFeatures
where itemFeature.FeatureID == searchFeature
select itemFeature;
}
var items =
from itemFeature in itemFeatures
select itemFeature.Item;

Resources