Linq: Best way to select parent rows order by count of children - c#

I have got a table for Comments like following:
Comment{
ID,
Text,
ParentID
}
I am using following query to select the popular comments with paging based on number of replies.
var comments = db.Comments
.OrderByDescending(c => db.Comments.Count(r => r.ParentID == c.ID)).Skip(skip).Take(recordsPerPage).ToList();
Please let me know the best way of handling this situation when we have thousands of comments?

I would consider adding an extra column to Comment that stores the replies count.Then instead making a nested query you can easily order your Comments by replies count.
var comments = db.Comments.Skip(skip).Take(recordsPerPage)
.OrderByDescending(c => c.ReplyCount)
.ToList();

Unless you are prepared to pre-calculate this in the database then you have the problem that you need to either do nested queries or to do a single full fetch an then do everything in memory. The latter is my choice until it is proven to be too slow.
Here's how I'd initially do it.
First, pre-fetch:
var allComments = Comments.ToArray();
Then create a function that will quickly return the count of comments:
var childrenLookup = allComments.ToLookup(x => x.ParentID);
var parentMap = allComments.ToDictionary(x => x.ID, x => x.ParentID);
Func<int, int> getCommentsCount = n =>
{
var r = 0;
if (parentMap.ContainsKey(n))
{
r = childrenLookup[parentMap[n]].Count();
}
return r;
};
Now it is almost trivial to return the results:
var comments = allComments
.OrderByDescending(c => getCommentsCount(c.ID))
.Skip(skip)
.Take(recordsPerPage)
.ToList();
(And, yes, your ordering is in the wrong order to you skip and take for paging.)
If you can't do this in memory then go with the pre-calculate approach.

Related

Linq one to many with filter

I have an Entity Framework database that I'm querying, so I'm using linq-to-entities.
Here's my query:
// 'Find' is just a wrapper method that returns IQueryable
var q = r.Find(topic =>
topic.PageId != null &&
!topic.Page.IsDeleted &&
topic.Page.IsActive)
// These are standard EF extension methods, which are used to include
linked tables. Note: Page_Topic has a one-to-many relationship with topic.
.Include(topic => topic.Page.Route)
.Include(topic => topic.Page_Topic.Select(pt => pt.Page.Route))
// HERE'S THE QUESTION: This select statement needs to flatten Page_Topic (which it does). But it seems to do it in the wrong place. To explain, if I were to include another column that depended on Page_Topic (for example: 'PillarRoutName2', I'd have to apply the same flattening logic to that column too. Surely the filtering of Page_Topic should be done higher up the query in a DRY way.
.Select(x => new
{
TopicName = x.Name,
HubRouteName = x.Page.Route.Name,
PillarRouteName = x.Page_Topic.FirstOrDefault(y => y.IsPrimary).Page.Route.Name
}).ToList();
Surely the filtering of Page_Topic should be done higher up the query in a DRY way.
Correct! And it's easy to do this:
.Select(x => new
{
TopicName = x.Name,
HubRouteName = x.Page.Route.Name,
FirstTopic = x.Page_Topic.FirstOrDefault(y => y.IsPrimary)
})
.Select(x => new
{
TopicName = x.TopicName,
HubRouteName = x.HubRouteName,
PillarRouteName = x.FirstTopic.Page.Route.Name,
PillarRoutName2 = x.FirstTopic. ...
}).ToList();
Depending on where you start to get properties from FirstTopic you can also use x.Page_Topic.FirstOrDefault(y => y.IsPrimary).Page or .Page.Route in the first part.
Note that you don't need the Includes. They will be ignored because the query is a projection (Select(x => new ...).

C# Linq union multiple properties to one list

Basically I have an object with 2 different properties, both int and I want to get one list with all values from both properties. As of now I have a couple of linq queries to do this for me, but I am wondering if this could be simplified somehow -
var componentsWithDynamicApis = result
.Components
.Where(c => c.DynamicApiChoicesId.HasValue ||
c.DynamicApiSubmissionsId.HasValue);
var choiceApis = componentsWithDynamicApis
.Select(c => c.DynamicApiChoicesId.Value);
var submissionApis = componentsWithDynamicApis
.Select(c => c.DynamicApiSubmissionsId.Value);
var dynamicApiIds = choiceApis
.Union(submissionApis)
.Distinct();
Not every component will have both Choices and Submissions.
By simplify, I assume you want to combine into fewer statements. You can also simplify in terms of execution by reducing the number of times you iterate the collection (the current code does it 3 times).
One way is to use a generator function (assuming the type of items in your result.Components collection is Component):
IEnumerable<int> GetIds(IEnumerable<Component> components)
{
foreach (var component in components)
{
if (component.DynamicApiChoicesId.HasValue) yield return component.DynamicApiChoicesId.Value;
if (component.DynamicApiSubmissionsId.HasValue) yield return component.DynamicApiSubmissionsId.Value;
}
}
Another option is to use SelectMany. The trick there is to create a temporary enumerable holding the appropriate values of DynamicApiChoicesId and DynamicApiSubmissionsId. I can't think of a one-liner for this, but here is one option:
var dynamicApiIds = result
.Components
.SelectMany(c => {
var temp = new List<int>();
if (c.DynamicApiChoicesId.HasValue) temp.Add(c.DynamicApiChoicesId.Value);
if (c.DynamicApiSubmissionsId.HasValue) temp.Add(c.DynamicApiSubmissionsId.Value);
return temp;
})
.Distinct();
#Eldar's answer gave me an idea for an improvement on option #2:
var dynamicApiIds = result
.Components
.SelectMany(c => new[] { c.DynamicApiChoicesId, c.DynamicApiSubmissionsId })
.Where(c => c.HasValue)
.Select(c => c.Value)
.Distinct();
Similar to some of the other answers, but I think this covers all your bases with a very minimal amount of code.
var dynamicApiIds = result.Components
.SelectMany(c => new[] { c.DynamicApiChoicesId, c.DynamicApiSubmissionsId}) // combine
.OfType<int>() // remove nulls
.Distinct();
To map each element in the source list onto more than one element on the destination list, you can use SelectMany.
var combined = componentsWithDynamicApis
.SelectMany(x => new[] { x.DynamicApiChoicesId.Value, x.DynamicApiSubmissionsId.Value })
.Distinct();
I have not tested it but you can use SelectMany with filtering out the null values like below :
var componentsWithDynamicApis = result
.Components
.Select(r=> new [] {r.DynamicApiChoicesId,r.DynamicApiSubmissionsId})
.SelectMany(r=> r.Where(p=> p!=null).Cast<int>()).Distinct();

Getting list of child entity nested several levels with LINQ

I have entities that are nested in this order:
RootDomain
Company
CompaniesHouseRecord
CompanyOfficer
When given a RootDomain I want to create a list of all CompanyOfficers that have an email address but I am not sure how to do this.
Here Is my non-working attempt:
RootDomain rd = db.RootDomains.Find(123);
List<CompanyOfficer> col = rd.Companies.Where(x => x.CompaniesHouseRecords.Any(chr => chr.CompanyOfficers.Any(co => co.Email != null)))
.Select(x => x.CompaniesHouseRecords.Select(chr => chr.CompanyOfficers)).ToList();
I am obviously way off the mark here. Can someone show me or point me to the correct method for dong this?
Like this:
RootDomain rd = db.RootDomains.Find(123);
List<CompanyOfficer> col = rd.Companies
.SelectMany(c => c.CompaniesHouseRecords)
.SelectMany(c => c.CompanyOfficers)
.Where(o => null != o.Email).ToList();
Someone answered before me, but I can show something different, which can be more convenient for someone who is used to DB requests.
Using LINQ, you can do this type of request:
var officersWithEmail = from company in rd.Companies
from companiesHouseRecord in company.CompaniesHouseRecords
from companyOfficer in companiesHouseRecord.CompanyOfficers
where (companyOfficer.Email != null)
select companyOfficer;
Some people will find it more readable.
If you want to obtain a List<> as output, just use .ToList on the query.

Most efficient way to order by and update a row in LINQ

I'm using this code to rank players in a game.
private void RecalculateUserRanks(GWDatabase db)
{
// RankedScore is a precalculated Double.
var users = db.UserStatistics.Where(x => x.RankedScore > 0);
var usersWithRank = users.OrderByDescending(x => x.RankedScore)
.Select(x => new
{
x.Id,
x.RankedScore
});
int position = 0;
foreach (var u in usersWithRank)
{
position++;
db.UserStatistics.First(x => x.Id == u.Id).Rank = position;
}
db.SaveChanges();
}
It's not the prettiest and as the number of players grows this will probably take some time and use a bit of memory.
I could do this in pure TSQL like this:
;WITH r AS
(
SELECT
[Id]
,[RankedScore]
,ROW_NUMBER() OVER(ORDER BY [RankedScore] DESC) AS Rnk
FROM [dbo].[UsersStatistics]
)
UPDATE u
SET u.Rank = r.Rnk
FROM [dbo].[UsersStatistics] u
INNER JOIN r ON r.Id = u.Id
But I would prefer to keep all my logic in the C# code as the database gets rebuilt all the time right now (and all other logic is there as well).
So my question is if there is a smarter way to do this in C# LINQ (or Lambda if thats your thing) without iterating over it in a for loop, and without dragging all the data outside of the SQL?
I assume by 'efficient' you mean 'efficient to read'. For a faster calculation you might consider to use a sorted list for db.UserStatistics; Those keep themselves sorted, while using log n time to insert a new member.
This is pretty much the same you posted, except lazy-evaluation might save a little time:
//get sorted list of IDs
var SortedIds = db.UserStatistics
.OrderByDescending(x => x.RankedScore)
.Select(x => x.Id);
//Fill in Values into result-set
db.UserStatistics = db.UserStatistics
.Where(x => x.RankedScore > 0)
.ForEach(x => u.Rank = SortedIds.IndexOf(x.id));
It seems a little inconsistent to have ranked and unranked players together.
This will give unranked players the rank -1 while saving a step. The downside would be, that all user will be altered, instead just those with a rank:
db.UserStatistics = db.UserStatistics.ForEach(u =>
u.Rank = db.UserStatistics
.Where(x => x.RankedScore > 0)
.OrderByDescending(x => x.RankedScore)
.IndexOf(u.id));

C# EF Deep Lambda Distinct Count Query

This is the query I am trying to do.
var commentActivity = project.ProjectDoc
.Select(c => c.Comment.Select(i => i.UserID))
.Distinct()
.Count();
What I want is the number of comments from distinct users on a specific project, but ANY ProjectDoc. This query "works" the result is just wrong. The model is like this, generically sketched.
Project
ProjectDoc
Comment
Update: I had to go one level deeper, based on the answer below I tried a few things that didn't work so I though I would post this as a reference. Note the two SelectMany methods.
var replyActivity = project.ProjectDoc
.SelectMany(c => c.Comment.SelectMany(r => r.CommentReply.Select(u => u.UserID)))
.Distinct()
.Count();
Use SelectMany instead of Select
project.ProjectDoc
.SelectMany(c => c.Comment.Select(i => i.UserID))
.Distinct()
.Count()
var data = (from con in project.ProjectDoc
select new
{
CommentCount=project.Comment.Count(x=>x.UserID==con.UserID)
}).ToList();
i think this will help you.

Categories