I am always finding myself creating linq expressions that still use nested foreach loops heavily. Below is a simple example of what I'm talking about, and I'd really appreciate it if someone on here can show me how to condense this low-efficiency code into a single linq expression?
The database context (db) has three tables: Blog, Tag, Junc_Tag_Blog. The junction table simply stores a record of blogs with their tags.
Anyway, here's my messy code:
public static Collection<Blog> GetByTag(string tagName)
{
// Get the tag record.
var tag = (from t in db.Tags
where t.Name == tagName
select t).Single();
// Get the list of all junction table records.
var tagJunc = from tj in db.Junc_Tag_Blogs
where tj.Fk_Tag_Id == tag.Id
select tj;
// Get a list of all blogs.
var blogs = from b in db.BlogPosts
select b;
// Work out if each blog is associated with given tag.
foreach(var blog in blogs)
{
foreach(var junc in tagJunc)
{
if(blog.Id == junc.Fk_Blog_Id)
{
// We have a match! - do something with this result.
}
}
}
}
Thanks in advance to the person who can help me clean this code up!
You can construct a query that lets the database find the matches for you.
List<Blog> blogs =
(
from t in tag
where t.Name == tagName
from tj in t.Junc_Tag_Blogs
let b = tj.Blog
select b
).ToList();
var blogsWithGivenTag =
from blog in db.BlogPosts
where blog.BlogTags.Any(bt => bt.Tag.Name == tagName)
select blog;
Read about performing inner joins with LINQ.
You can put the blogs in a dictionary, group the junctions on the blog id, and loop through the groups:
var blogDict = blogs.ToDictionary(b => b.Id);
foreach(var group in tagJunk.GroupBy(j => j.Fk_Blog_Id)) {
if (blogDict.ContainsKey(group.Key)) {
var blog = blogDict[group.Key];
foreach (var junction in group) {
// here you have the blog and the junction
}
}
}
This is also nested loops, but for each blog you only loop through the junctions that actually belong to that blog, instead of all junctions.
Related
I want to use an id from one table to list a title from another table in wpf, so i did this:
var q = from a in context.associations
select a;
associations = q.ToList();
associationViewSource.Source = associations;
foreach (var item in q)
{
var qTitles = from b in context.textbooks
where b.Id == item.book_id
select b.Title;
assocListView.ItemsSource = qTitles.ToList();
}
in the first portion of the code i am making the main body of the information, it lists all the information from associations table, after that i want to list the relevant titles from textbooks table, thats where i add items to the assocListview, but it of course fails and the data isn't displayed, no errors are thrown either. i hope i was clear enough.
Please help
You can get a list of Titles that have associations with a join...
var qTitles = context.textbooks
.Join(context.associations,
b => b.Id,
a => a.book_id,
b => b.Title)
.ToList();
The intention of the code isn't clear though because assocListView.ItemsSource is assigned to in each iteration of the loop. Is that a bug?
Since you are setting assocListView.ItemsSource each pass through the loop, the resultant list will have the results of the last query.
If you want a list of all associated titles you can get it with a single query:
var titles =
from a in context.associations
join b in context.textbooks on a.book_id equals b.Id
select b.Title;
assocListView.ItemsSource = titles.ToList();
This will return every title linked to any record in your associations table, in no particular order, with duplicates, etc. Normally it makes sense to extract a little more information to make it more usable. For instance, define a structure to hold an associated title:
public struct AssocTitle
{
public int AssocID;
public int BookID;
public string Title;
}
Then query it like:
var titles =
from a in context.associations
join b in context.textbooks on a.book_id equals b.Id
select new AssocTitle { AssocID = a.Id, BookID = b.Id, Title = b.Title };
Then when you click on things in the list view you can find out which book and which association, even if you have lots of associations with the same titles.
I have a table called "Articles".
it includes the following field:
ArticleIndex, ArticleLevel, ArticleParentIndex.
I made query which returns all the articles with ArticleLevel=1 - let's call it query1.
The query which returns all the articles with ArticleLevel=2 - query2.
I would like to have a query that would return Articles of level=1 with at least one child article (the child articles have level=2), and also the number of child articles.
So far I have the following query:
var filteredItemsGrouped = from i in filteredItems
group i by i.ArticleParentIndex into g
select new { Node = g, NodeItemsCount = g.Count() };
and then, in order to get the actual articles with level=1 I do:
IList<ArticleNodeInfo> Nodes = new List<ArticleNodeInfo>();
foreach (var node in filteredItemsGrouped)
{
Nodes.Add(new ArticleNodeInfo
{
Node = articlesService.GetArticleByIndex((int)(node.Node.FirstOrDefault().ArticleParentIndex)),
NodeItemsCount = node.NodeItemsCount
});
}
This process is too expensive. Is it possible to acheive the same with one query (instead of retreiving by article index every time)?
Hope I'm clear enough...
this should do the trick:
var articlesLevel1 = (
from al1 in Articles
join al2 in Articles on new
{
al1.ArticleIndex,
ArticleLevel = 2
} equals new
{
ArticleIndex = al2.ArticleParentIndex,
al2.ArticleLevel
} into g_al2
where (al1.ArticleLevel == 1) && g_al2.Any()
select new
{
ArticlesLevel1 = al1,
ArticlesLevel2Count = g_al2.Count()
});
I am selecting the following data from an SQL database table in my controller
var fixtures = from f in _context.Fixtures
where f.MatchDate.Year == year &&
(gender.Contains( f.Gender )) &&
(type.Contains( f.MatchType )) &&
(team.Contains( f.TeamName ))
orderby f.TeamName, f.MatchDate
select f;
return View( "DisplayLeagues", fixtures.ToList() );
and calling the DisplayLeagues view to display it. As you can see from the orderby, it contains more than one team. I want to display a separate table on the webpage for each team. My page declaration contains
Inherits="System.Web.Mvc.ViewPage<IEnumerable<RLSBCWebSite.Domain.Entities.Fixture>>"
as the declaration of the Model. To display the individual lines of the table, I have
<% foreach (var item in Model.AsEnumerable()) { %>
But how do I add a loop control based on the name of the team around the table as a whole? It doesn't seem possible to code
<% foreach (var team in Model.TeamName ....
Something like:
group g by f.TeamName into tmp
select new {TeamName = f.Key, Fixtures = tmp.ToList() }
this will give you one group per team, i.e.
foreach(var grp in query) {
Console.WriteLine(grp.TeamName);
foreach(var fixture in grp.Fixtures) {
// write fixture details
}
}
Note that this changes each item to an IGrouping<string, List<Fixture>>, and note that ToLookup will do something very similar if that is easier.
For the outer loop, which produces a separate table for each team, I used the following
<% foreach (var teamName in Model.AsEnumerable().GroupBy( r => r.TeamName )) {
For the inner loop, which produces the individual matches for each team, I used
<% foreach (var item in Model.AsEnumerable().Where(item => item.TeamName == teamName.Key.ToString())) {
This works very well.
I need help converting this SQL statement, into EF4:
Select Posts.PostID, Post, Comment
from Posts left join
Comments on posts.PostID = Comments.PostID
Where CommentID not in
(
Select PostID
from Votes
where VoteTypeID = 4 --4 = flagged comment type
)
In my database, the Votes table stores either the PostID of reported posts, or CommentID of reported comments in the column Votes.PostID
Thanks in advance!
using (var context = new Model1Container())
{
var posts = context.Posts.
All((p)=> p.Votes.All((v) => v.VoteTypeId!=4));
//Or
var posts2 = from p in context.Posts
where p.Votes.All((v)=> v.VoteTypeId != 4)
select p;
}
Update:
Based on my understanding you want all the posts, but for each post, you want to filter its comments, if that's the case, you could use ToDictionary:
var posts =
context.Posts.
//Include("Comments").
ToDictionary(
(p) => p,
(p) => p.Comments.Where((c)=> c.Votes.All((v) => v.VoteTypeId !=4))
);
foreach (var item in posts)
{
var post = item.Key;
var comments = item.Value;
}
Note: uncomment the Include method if lazy-loading is disabled, and you explicitly want to eager-load the comments in this query.
Update2:
var postsCollection = posts.Keys.ToArray();
var commentsCollection = posts.Values.ToArray();
This is hard to determine without seeing your Model. But this should get you started.
If you would like you can post a picture of your model or the EDMX and I can take a better look at this.
var myPosts = from p in posts
where !p.Comments.Any(c => c.Votes.VoteID != 4)
PostId = p.PostId,
//other field needed here
};
I have a class...
class Document
{
public int GroupID { get; set; }
public bool Valid { get; set; }
// more
}
... and a list of instances: IEnumerable<Document> documents. In a first step which runs object by object through this list those documents have been validated, which means: the property Valid will be true for some objects and false for other objects in the list.
Now in a second step I have to do the following:
If for at least one document per document group (defined by all documents with the same GroupID) the flag Valid is false then set Valid to false for all documents of the group.
To do this I have created so far the following code fragment:
var q = from d in documents
group d by d.GroupID;
// q is now of type IEnumerable<IGrouping<int, Document>>
foreach (var dg in q) // dg = "document group", of type IGrouping<int, Document>
{
if (dg.Any(d => !d.Valid))
{
foreach (var d in dg)
d.Valid = false;
}
}
I believe, this does what I want (I didn't test it until now, though) but not very efficiently.
Question: Is there a way to improve this code, especially to move the semantics of the Any method in the outer foreach loop "somehow" into the initial LINQ query, so that q only represents the groups which have at least one invalid document? (Also I am obviously not interested in groups which have only one element, so those groups could be filtered out as well.)
Thank you for suggestions in advance!
I think this does what you want:
var q = from d in documents
group d by d.GroupID into g
where g.Count() > 1 && g.Any(d => !d.Valid)
select g;
foreach (var dg in q)
{
foreach (var d in dg)
{
d.Valid = false;
}
}
The top part in fluent syntax would look like:
var q = documents.GroupBy(d => d.GroupID)
.Where(g => g.Count() > 1 && g.Any(d => !d.Valid));
If you're just trying to set the Valid flag to false if one of the documents is not Valid, you could try grabbing the list of the GroupId's who are not valid and then set all of the documents whose share a group with them to be invalid. Sample code as follows:
//Find the invalid GroupIds.
var invalidIds = documents.Where(d => !d.IsValid).Select(p => p.GroupId).Distinct();
//invalidIds now holds the bad groupIds.
//So we can find out if each document's GroupId is an invalid one, and if it is, mark it as invalid.
documents.Where(d => invalidIds.Contains(d.GroupId)).ToList().ForEach(p => p.IsValid = false);
Hope this helps.