Using LINQ Expressions to Multiple Left Join and use ISNULL Functionality - c#

I'm building a Step Tracking web app at work. I'm working with the latest EF Core. There are three tables I'm interacting with:
wg: WellnessGroup (WellnessGroupId, Name)
wgu: WellnessGroupUser (Look up table: WellnessGroupId, EmployeeId)
wsl: WellnessStepsLog (EmployeeId, StepCount)
What I want is to get all of the WellnessGroups and the total step amount for that group. If there are no steps attached to that group yet, I would like for the NULL value to be 0. I have this SQL statement which gives me the desired data:
SELECT wg.Name, SUM(ISNULL(wsl.StepCount, 0)) AS steps
FROM dbo.WellnessGroup AS wg
LEFT JOIN dbo.WellnessGroupUser AS wgu
ON wgu.WellnessGroupId = wg.Id
LEFT JOIN dbo.WellnessStepsLog AS wsl
ON wsl.EmployeeId = wgu.AzureAdUserId
GROUP BY wg.Name
ORDER BY steps DESC;
And I have managed to throw 2 LINQ expressions together on my controller which is giving me only the WellnessGroups that have steps associated with them and is not giving me the WellnessGroup data if there are no steps:
var query = _dbContext.WellnessGroupUser
.Include(x => x.WellnessGroup)
.Join(_dbContext.WellnessStepsLog, group =>
group.AzureAdUserId, steps => steps.EmployeeId,
(group, steps) => new
{
Steps = steps.StepCount,
Date = steps.TrackedDate,
Group = group.WellnessGroup.Name
}).Where(x => x.Date >= yearToDate).Where(x => x.Date <= endDate);
var stepsByGroup = query
.GroupBy(x => x.Group)
.Select(s => new
{
Group = s.Key,
Date = s.Max(x => x.Date),
Steps = s.Sum(x => x.Steps)
});

One way is to query all WellnessGroups and build the sum inside as a second subquery. Like this:
var query =
db.WellnessGroup.Select(wg => new {
wg.WellnessGroupId,
sum = (int?) wg.WellnessGroupUser
.Sum(wgu => wgu.Employee.WellnessStepsLog.Sum(wsl => wsl.StepCount))
});
Note, that the cast to (int?) is important. Otherwise sum is assumed to be int, which causes an InvalidOperationException if there is no sum for a row.
Another way is to build all the sums first. And then do the outer join with the WellnessGroups:
// sum up all stepcounts
var q1 =
from wgu in db.WellnessGroupUser
from wsl in db.WellnessStepsLog
where wgu.EmployeeId == wsl.EmployeeId
group wsl.StepCount by wgu.WellnessGroupId
into g
select new {WellnessGroupId = g.Key, Sum = g.Sum()};
// join with all WellnessGroups
var q2 =
from wg in db.WellnessGroup
join s in q1 on wg.WellnessGroupId equals s.WellnessGroupId into sj
from sum in sj.DefaultIfEmpty()
select new {wg, sum = (int?) sum.Sum};
EDIT:
Since the OP later asked in the comments how to group by more then one field. Here is an example which groups by WellnessGroup.WellnessGroupId and the month of WellnessStepsLog.TrackedDate. There can be more then one field in GroupBy by placing them in a new { ... }. So the first query creates a line per possible WellnessGroup / Month combination. The second query performs the outer join with WellnessGroup just as before:
var q1 =
from wgu in db.WellnessGroupUser
from wsl in db.WellnessStepsLog
where wgu.EmployeeId == wsl.EmployeeId
group wsl.StepCount by new { wgu.WellnessGroupId, wsl.TrackedDate.Month }
into g
select new {g.Key.WellnessGroupId, g.Key.Month, Sum = g.Sum()};
// join with all WellnessGroups
var q2 =
from wg in db.WellnessGroup
join s in q1 on wg.WellnessGroupId equals s.WellnessGroupId into sj
from sum in sj.DefaultIfEmpty()
select new {wg.WellnessGroupId, Month = (int?) sum.Month, Sum = (int?) sum.Sum};

Related

LINQ Count returning 1 instead of zero for an empty group

I've got this SQL query:
SELECT oy.ownerId, oy.Year, COUNT(doc.Id) as docCount FROM aavabruf.owneryears oy
left join vastdocuments doc
on oy.ownerId = doc.Ownerid and oy.Year = doc.Year
group by oy.ownerid, oy.year
order by docCount
It shows docCount as ZERO for the OwnerId, Year pairs that have no document match in the vastdocuments table.
I tried to do the same with LINQ using the suggested left outer join solution:
from oy in OwnerYears
join doc in VaStDocuments on new {oy.OwnerId, oy.Year} equals new {doc.OwnerId , doc.Year} into docS
from docIfNull in docS.DefaultIfEmpty()
group oy by new {oy.OwnerId, oy.Year} into g
orderby g.Count() ascending
select new { OwnerId = g.Key.OwnerId, Year = g.Key.Year, docCount = g.Count()}
However, for the OwnerId, Year groups that are not present in the VastDocuments table I get docCount as ONE, not ZERO. If I remove the
from docIfNull in docS.DefaultIfEmpty()
line the "empty" groups will not be shown at all.
How can i get the Count as zero just as it is in the SQL query? I tried the following:
Count = docIfNull == null ? 0 : g.Count()
however in this case I get an error:
The name 'docIfNull' does not exist in the current context
The simplest approach is to count non-null values:
g.Count(x => x != null)
I'd suggest moving the ordering after the select so that you can avoid repeating yourself:
select new { g.Key.OwnerId, g.Key.Year, DocCount = g.Count(x => x != null) } into result
orderby result.DocCount
select result
However, I note that currently you're not using docIfNull at all at the moment... so I suspect your join isn't really doing what you want it to. Perhaps you should be using
group docIfNull by new { oy.OwnerId, oy.Year } into g
?
SQL COUNT function ignores the NULL values, while LINQ Count function w/o predicate counts everything, including nulls.
You can get the same result in LINQ by using the predicate version of Count like this (note the group docIfNull so the g elements will be of the same type as docIfNull):
from oy in OwnerYears
join doc in VaStDocuments on new { oy.OwnerId, oy.Year } equals new { doc.OwnerId, doc.Year } into docS
from docIfNull in docS.DefaultIfEmpty()
group docIfNull by new { oy.OwnerId, oy.Year } into g
let docCount = g.Count(doc => doc != null)
orderby docCount ascending
select new { OwnerId = g.Key.OwnerId, Year = g.Key.Year, docCount = docCount }
(the let clause is just to reuse the expression in orderby and select).
However in LINQ you have another option - in case the (OwnerId, Year) combination inside OwnerYears is unique as it seems, instead of left outer join pattern followed by group by and Count filtering nulls you could use simple group join operator with regular Count call:
from oy in OwnerYears
join doc in VaStDocuments on new { oy.OwnerId, oy.Year } equals new { doc.OwnerId, doc.Year } into docs
let docCount = docs.Count()
orderby docCount ascending
select new { OwnerId = oy.OwnerId, Year = oy.Year, docCount = docCount }

LINQ - Join 2 tables, Group by DateTime.Month , multiple Counts

I'm pretty new to C# and LINQ and I'm trying get a list of emails that holds the sum of emails, attachments and user's (the one's that sent the email).
So my current Problem is the Output of my Query is false. The number of email's is equal to the number of attachment's which obvious is wrong.
My Query:
var monthQuery = from em in dbEdoka.email
join ema in dbEdoka.email_attachment on em.id equals ema.email_id into e
from e2 in e.DefaultIfEmpty()
group e2 by em.erstellt_am.Month into grouped
select new Entities.Month
{
NameOfMonth = grouped.FirstOrDefault().erstellt_am.ToString(),
NumberOfMails = grouped.Distinct().Count(m => m.email_id != null).ToString(),
NumberOfAttachments = grouped.Count(a => a.id != null).ToString(),
NumberOfUsers = grouped.Select(u => u.erstellt_von).Distinct().Count().ToString()
};
months = monthQuery.ToList();
Months = CollectionViewSource.GetDefaultView(months);
As you can see I had to take m.email_id from dbEdoka.email_attachment instead of m.id from dbEdoka.email because it wasn't avaliable (don't know why...).
Yet I have to count "NumberOfMails", "NumberOfAttachments" and "NumberOfUsers".
Thank you!

Converting SQL to LINQ query when I cannot use "IN"

I'm trying to convert this very simple piece of SQL to LINQ:
select * from Projects p
inner join Documents d
on p.ProjectID = d.ProjectID
left join Revisions r
on r.DocumentID = d.DocumentID
and r.RevisionID IN (SELECT max(r2.RevisionID) FROM Revisions r2 GROUP BY r2.DocumentID)
WHERE p.ProjectID = 21 -- Query string in code
This says, if any revisions exist for a document, return me the highest revision ID. As it's a left join, if not revisions exist, I still want the results returned.
This works as expected, any revisions which exist are shown (and the highest revision ID is returned) and so are all documents without any revisions.
When trying to write this using LINQ, I only get results where revisions exist for a document.
Here is my attempt so far:
var query = from p in db.Projects
join d in db.Documents on new { ProjectID = p.ProjectID } equals new { ProjectID = Convert.ToInt32(d.ProjectID) }
join r in db.Revisions on new { DocumentID = d.DocumentID } equals new { DocumentID = Convert.ToInt32(r.DocumentID) } into r_join
from r in r_join.DefaultIfEmpty()
where
(from r2 in db.Revisions
group r2 by new { r2.DocumentID }
into g
select new { MaxRevisionID = g.Max(x => x.RevisionID) }).Contains(
new { MaxRevisionID = r.RevisionID }) &&
p.ProjectID == Convert.ToInt32(Request.QueryString["projectId"])
select new { d.DocumentID, d.DocumentNumber, d.DocumentTitle, RevisionNumber = r.RevisionNumber ?? "<No rev>", Status = r.DocumentStatuse == null ? "<Not set>" : r.DocumentStatuse.Status };
I'm not very good at LINQ and have been using the converter "Linqer" to help me out, but when trying I get the following message:
"SQL cannot be converted to LINQ: Only "=" operator in JOIN expression
can be used. "IN" operator cannot be converted."
You'll see I have .DefaultIfEmpty() on the revisions table. If I remove the where ( piece of code which does the grouping, I get the desired results whether or not a revision exists for a document or not. But the where clause should return the highest revision number for a document IF there is a link, if not I still want to return all the other data. Unlike my SQL code, this doesn't happen. It only ever returns me data where there is a link to the revisions table.
I hope that makes a little bit of sense. The group by code is what is messing up my result set. Regardless if there is a link to the revisions table, I still want my results returned. Please help!
Thanks.
=======
The code I am now using thanks to Gert.
var query = from p in db.Projects
from d in p.Documents
where p.ProjectID == Convert.ToInt32(Request.QueryString["projectId"])
select new
{
p.ProjectID,
d.DocumentNumber,
d.DocumentID,
d.DocumentTitle,
Status = d.Revisions
.OrderByDescending(rn => rn.RevisionID)
.FirstOrDefault().DocumentStatuse.Status,
RevisionNumber = d.Revisions
.OrderByDescending(rn => rn.RevisionID)
.FirstOrDefault().RevisionNumber
};
gvDocumentSelection.DataSource = query;
gvDocumentSelection.DataBind();
Although this works, you'll see I'm selecting two fields from the revisions table by running the same code, but selecting two different fields. I'm guessing there is a better, more efficient way to do this? Ideally I would like to join on the revisions table in case I need to access more fields, but then I'm left with the same grouping problem again.
Status = d.Revisions
.OrderByDescending(rn => rn.RevisionID)
.FirstOrDefault().DocumentStatuse.Status,
RevisionNumber = d.Revisions
.OrderByDescending(rn => rn.RevisionID)
.FirstOrDefault().RevisionNumber
Final working code:
var query = from p in db.Projects
from d in p.Documents
where p.ProjectID == Convert.ToInt32(Request.QueryString["projectId"])
select new
{
p.ProjectID,
d.DocumentNumber,
d.DocumentID,
d.DocumentTitle,
LastRevision = d.Revisions
.OrderByDescending(rn => rn.RevisionID)
.FirstOrDefault()
};
var results = from x in query
select
new
{
x.ProjectID,
x.DocumentNumber,
x.DocumentID,
x.DocumentTitle,
x.LastRevision.RevisionNumber,
x.LastRevision.DocumentStatuse.Status
};
gvDocumentSelection.DataSource = results;
gvDocumentSelection.DataBind();
If you've got 1:n navigation properties there is a much simpler (and recommended) way to achieve this:
from p in db.Projects
from d in p.Documents
select new { p, d,
LastRevision = d.Revisions
.OrderByDescending(r => r.RevisionId)
.FirstOrDefault() }
Without navigation properties it is similar:
from p in db.Projects
join d in db.Documents on new { ProjectID = p.ProjectID }
equals new { ProjectID = Convert.ToInt32(d.ProjectID) }
select new { p, d,
LastRevision = db.Revisions
.Where(r => d.DocumentID = Convert.ToInt32(r.DocumentID))
.OrderByDescending(r => r.RevisionId)
.FirstOrDefault() }
Edit
You can amend this very wide base query with all kinds of projections, like:
from x in query select new { x.p.ProjectName,
x.d.DocumentName,
x.LastRevision.DocumentStatus.Status,
x.LastRevision.FieldA,
x.LastRevision.FieldB
}

Show 3 result from each element in group by

I am working on a system for handling meter reading.
I want to produce a output where the system displays all the meters belonging to the customer and for each meter, the three last readings.
So far, I have to followering code:
var lastMeterReading = from meeters in metermodel.Meeters
join reading in metermodel.Readings on meeters.MeterNumber equals reading.MeterNumber
where (maalers.CustNo == 6085574)
orderby reading.Date descending
group meeters by new { meeters.MeterNumber, reading.Consumption, reading.Date } into result
select new
{
Consumption = result.Key.Consumption, No = result.Key.MeterNumber, Date = result.Key.Date
};
Now, it shows all the meters belonging to the customer. If I put a .take(3), it only shows the first 3 results.
Thx!
Daniel
I think what you need is to put the .Take(3) in the right place.
In you case you probably did result.Take(3) but this means take the first three groups (with all their elements).
Below is an attempt to show what I mean, however, I suppose you will need to fix it in the last part, as I don't have data to test it on, and as such I'm not sure if what I'm trying to access is accessible at that point. But I hope you get what I mean.
var lastMeterReading = (from meeters in metermodel.Meeters
join reading in metermodel.Readings on meeters.MeterNumber equals reading.MeterNumber
where (maalers.CustNo == 6085574)
orderby reading.Date descending
group meeters by new { meeters.MeterNumber, reading.Consumption, reading.Date } into result
from m in result
select new {Key = m.Key, Info = result.OrderByDescending(r => r.Date).Take(3)})
.Select(r => new
{ Consumption = r.Consumption, No = r.MeterNumber, Date = r.Date });
Try this:
var lastMeterReading = from meeters in metermodel.Meeters
join reading in metermodel.Readings on meeters.MeterNumber equals reading.MeterNumber
where (maalers.CustNo == 6085574)
orderby reading.Date descending
group meeters by new { meeters.MeterNumber, reading.Consumption, reading.Date } into result
from m in result.Take(3)
select new
{
Consumption = m.Consumption, No = m.MeterNumber, Date = m.Date
};
You only want to group by MeterNumber. The way you're doing the grouping right now, you'll get a new group for every unique MeterNumber-Consumption-Date combination.
You can also simplify your query using LINQ's GroupJoin operator. In query syntax you use the "join..on..into" pattern:
from meter in meterModel.Meters
where (meter.CustNo == 6085574)
join reading in meterModel.Readings
on meter.MeterNumber equals reading.MeterNumber
into meterGroup
select meterGroup.OrderByDescending(r => r.Date).Take(3);
Or using dot notation:
meterModel.Meters
.Where(x => x.CustNo == 6085574)
.GroupJoin(
meterModel.Readings,
meter => meter.MeterNumber,
reading => reading.MeterNumber,
(meter,readings) => readings.OrderByDescending(r => r.Date).Take(3))
;

Linq to SQL join and group

I have two tables in my database:
Town:
userid, buildingid
Building:
buildingid, buildingname
What i want is to populate a GridView like this:
But I don't want the buildings to be shown more than once. Here is my code:
var buildings = dc.Towns
.Where(t => t.userid == userid)
.GroupJoin(dc.Buildings,
t => t.buildingid,
b => b.buildingid,
(Towns, Buildings) => new
{
BuildningName = Buildings.First().buildingname,
Count = Towns.Building.Towns.Count()
});
gvBuildings.DataSource = buildings.ToList();
gvBuildings.DataBind();
New code which works:
var buildings = (from t in dc.Towns
where t.userid == userid
join b in dc.Buildings
on t.buildingid equals b.buildingid
into j1
from j2 in j1.DefaultIfEmpty()
group j2 by j2.buildingname
into grouped
select new
{
buildingname = grouped.Key,
Count = grouped.Count()
});
gvBuildings.DataSource = buildings.ToList();
gvBuildings.DataBind();
var buildings = from t in dc.Towns
join b in dc.Buildings on t.buildingid equals b.buildingid into j1
from j2 in j1.DefaultIfEmpty()
group j2 by b.buildingname into grouped
select new { buildingname = grouped.key, Count = grouped.Count()}
I think this should do it. I have not tested it so it might give error but it will be something like this.
Wouldn't something like this do it?
Users
.Select(User => new {User, User.Building})
.GroupBy(x => x.Building)
.Select(g=> new {Building = g.Key, Count = g.Count()})
According to my experience with Linq to SQL, when the expression is becoming complicated it is better to write a stored procedure and call it with Linq to SQL. In this way you get better maintainability and upgradeability.
Rather than an option to pure SQL, I see “Linqu to SQL” as a tool to get hard typed object representation of SQL data sets. Nothing more.
Hope it helps you.

Categories

Resources