LINQ to Entities extensions; multiple subqueries, joins and case statements - c#

I have googled thoroughly for examples to my latest LINQ endeavor ("Hi, I'm new to LINQ"). I'll spare you the list of pages I've visited.
Here is sample SQL of what I would like to achieve:
use myDB;
go
declare #requestedDay datetime = convert(datetime, (convert(varchar(10), getdate(), 101)), 101)
declare #type1 int = 20
;with currentQuery as ( select
case when z.someID = #type1 and bet.someValue is null then 1
else 0 end as count1,
case when z.someID = #type1 and bet.someValue = alef.otherID then 1
else 0 end as count2,
from dbo.Work as dalet
left join dbo.Workers as z on alef.workerAssignedID = z.ID
left join dbo.Contracts as alef on alef.workOrderID = gimel.ID
left join dbo.Subcontracts as bet on alef.WorkOrderID = alef.WorkOrderID
and alef.WorkerAssignedID = dalet.WorkerID
where convert(varchar(10),alef.dateTimeofWork,101) = #requestedDay),
futureQuery as ( select
case when gimel.text_EN like '%blah%' and bet.someValue is null then 1
else 0 end as count3,
case z.someID = #type1 and bet.someValue = alef.otherID then 1
else 0 end as count4,
from dbo.Work as dalet
left join dbo.Workers as z on dalet.workerAssignedID = z.ID
left join dbo.Contracts as alef on dalet.workOrderID = alef.ID
left join dbo.Subcontracts as bet on dalet.WorkOrderID = bet.WorkOrderID and wa.WorkerAssignedID = wr.WorkerID
left join dbo.Lookups as gimel on dalet.skillID = gimel.ID
where convert(datetime,(convert(varchar(10),alef.dateTimeofWork,101)),101) > #requestedDay)
select sum(count1) as prop1name, sum(count2) as prop2name,
sum(count3) as prop3name, sum(count4) as prop4name
from currentQuery, futureQuery
This is, of course, a shortened version of a much larger query. It contains just the basics of what I need. The names may be confusing but I was looking for something unique. They also match their LINQ cousins below.
...that said, here is where I am stuck: ("What I have tried so far:") I've included some comments that describe problems I am trying to think through.
//snip injection stuff above
public IQueryable<DailyCasaLatinaReport> DailyCasaLatina(DateTime dateRequested)
{
IQueryable<DailyCasaLatinaReport> query;
var daletQ = waRepo.GetAllQ();
var zQ = zRepo.GetAllQ();
var alefQ = alefRepo.GetAllQ();
var betQ = betRepo.GetAllQ();
var gimelQ = gimelRepo.GetAllQ();
int type1 = 20;
int type2 = 21;
query = daletQ
.GroupJoin(gimelQ, dalet => dalet.skillID, look => look.ID,
(dalet, look) => new
{
dalet,
enSkillText = look.FirstOrDefault().text_EN
}) //currently envisioning a left outer join of
//all .skillID, with English text available
//for column and condition matches from the
//next three joins.
.GroupJoin(betQ, gimel => gimel.dalet.workOrderID, wr => wr.WorkOrderID,
(gimel, wr) => new
{
gimel,
reqWorkerID = wr.FirstOrDefault().WorkerID,
reqOrderID = wr.FirstOrDefault().WorkOrderID
}) //now envisioning a join on the original table
//where any match in workerID is joined. THIS
//IS A PROBLEM, I actually need to join on two
//conditions to avoid duplicates.
.GroupJoin(alefQ, bet => bet.gimel.dalet.workOrderID, wo => wo.ID,
(bet, wo) => new
{
bet,
timeOfWork = wo.FirstOrDefault().dateTimeofWork
}) //now envisioning yet another join where the
//dateTimeofWork property from woQ is stamped
//onto all matches of woQ's ID column. since
//woQ.ID is the common point of reference for
//like, everything, there should be no nulls.
.GroupJoin(zQ, alef => alef.bet.gimel.dalet.workerAssignedID, w => w.ID,
(alef, w) => new
{
alef,
listDWC = alef.bet.someValue == 0 ? (w.FirstOrDefault().someID == type1 ? 1 : 0) : 0,
propioDWC = alef.bet.someValue == alef.bet.gimel.dalet.workerAssignedID ?
(w.FirstOrDefault().someID == type1 ? 1 : 0) : 0,
})
// here I'm stuck because there's no way to do
//the future conditions, i.e., what would be my
//second subquery
.Where(x => x.alef.timeOfWork == dateRequested)
.GroupBy(y => y.alef.bet.gimel.dalet.ID)
.Select(group => new dailyReport
{
count1 = group.Sum(z => z.listDWC),
count2 = group.Sum(z => z.propioDWC),
count3 = //???
count4 = //???
});
return query;
}
//snip class definition below
So, sorry for the long question (though I've seen longer), but any ideas about how to squeeze my second subquery in here would be helpful. I don't know LINQ that well and honestly don't know if I can put a separate .Where clause and/or second .Select clause. One thing to note is that dailyReport is a defined class and must have (in our example) count1 thru count4 defined.
Appreciate any help,
Chaim

Based on comments it was determined that breaking up this monster sql query would be more beneficial to the maintainability of his code and a new question will be created if needed.

Related

Using LINQ Expressions to Multiple Left Join and use ISNULL Functionality

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};

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
}

LINQ to SQL omit field from results while still including it in the where clause

Basically I'm trying to do this in LINQ to SQL;
SELECT DISTINCT a,b,c FROM table WHERE z=35
I have tried this, (c# code)
(from record in db.table
select new table {
a = record.a,
b = record.b,
c = record.c
}).Where(record => record.z.Equals(35)).Distinct();
But when I remove column z from the table object in that fashion I get the following exception;
Binding error: Member 'table.z' not found in projection.
I can't return field z because it will render my distinct useless. Any help is appreciated, thanks.
Edit:
This is a more comprehensive example that includes the use of PredicateBuilder,
var clause = PredicateBuilder.False<User>();
clause = clause.Or(user => user.z.Equals(35));
foreach (int i in IntegerList) {
int tmp = i;
clause = clause.Or(user => user.a.Equals(tmp));
}
var results = (from u in db.Users
select new User {
a = user.a,
b = user.b,
c = user.c
}).Where(clause).Distinct();
Edit2:
Many thanks to everyone for the comments and answers, this is the solution I ended up with,
var clause = PredicateBuilder.False<User>();
clause = clause.Or(user => user.z.Equals(35));
foreach (int i in IntegerList) {
int tmp = i;
clause = clause.Or(user => user.a.Equals(tmp));
}
var results = (from u in db.Users
select u)
.Where(clause)
.Select(u => new User {
a = user.a,
b = user.b,
c = user.c
}).Distinct();
The ordering of the Where followed by the Select is vital.
problem is there because you where clause is outside linq query and you are applying the where clause on the new anonymous datatype thats y it causing error
Suggest you to change you query like
(from record in db.table
where record.z == 35
select new table {
a = record.a,
b = record.b,
c = record.c
}).Distinct();
Can't you just put the WHERE clause in the LINQ?
(from record in db.table
where record.z == 35
select new table {
a = record.a,
b = record.b,
c = record.c
}).Distinct();
Alternatively, if you absolutely had to have it the way you wrote it, use .Select
.Select(r => new { a = r.a, b=r.b, c=r.c }).Distinct();
As shown here LINQ Select Distinct with Anonymous Types, this method will work since it compares all public properties of anonymous types.
Hopefully this helps, unfortunately I have not much experience with LINQ so my answer is limited in expertise.

Linq to Entities Left Outer Join with different types

I've probably spent 40 hours on this problem so far, I've tried every solution on this site and on google, and I still can't make this work.
I need to left join a table to the results of a previous query, stored in a var. The joining field is a varchar in the table queried for the result in the var, and a bigint (long) in the table being joined. Here is the current attempt, which tells me "Object reference not set to an instance of an object." All Entities errors seem like nonsense and lies to me, I assume it's trying to tell me nothing matched, but who knows.
List<reportUser> ru = leaders
.GroupJoin(db.sweeps,
a => a.FBID.ToString(),
s => s.userFBID.First().ToString(),
(a, matching) => new reportUser
{
FBID = a.FBID,
CurrentPoints = a.CurrentPoints,
Name = matching.FirstOrDefault().Name,
Email = matching.FirstOrDefault().email
}
?? new reportUser
{
FBID = 0,
CurrentPoints = 0,
Name = "",
Email = ""
})
.Select(a => a)
.ToList();
Here's the SQL requested below. I've included the SQL to build the Leaders object as well, all the above is really meant to represent is the last line, which is simply a left join.
select s.name, s.email, b.score, c.score overall
from (
select a.userfbid, sum(a.pointvalue) score
from (
select userfbid, pointvalue
from l
left join qa on qa.id = l.qaid
left join q on q.id = qa.qid
left join qz on qz.id = q.qzid
where qa.pointvalue > 0 and qz.cid = 12
union all
select fbid userfbid, pointvalue
from bn
where date >= '5/9/2011 04:00' and
date <= '5/16/2011 04:00'
) a
group by a.userfbid
) b
left join (
select a.userfbid, sum(a.pointvalue) score
from (
select userfbid, pointvalue
from l
left join qa on qa.id = l.qaid
left join q on q.id = qa.qid
left join qz on qz.id = q.qzid
where qa.pointvalue > 0
union all
select fbid userfbid, pointvalue
from bn
) a
group by a.userfbid
) c on c.userfbid=b.userfbid
left join s on s.userfbid=b.userfbid
order by score desc
I'm assuming that in your database s.userFBID.First() is never null?
If that's right, then your problem could be in the FirstOrDefault().Name type statements - when FirstOrDefault() evaluates to null then obviously you will get a nullreferenceexception :/
To get around this, try something like:
List<reportUser> ru = leaders
.GroupJoin(db.sweeps,
a => a.FBID.ToString(),
s => s.userFBID.First().ToString(),
(a, matching) =>
{
var match = matching.FirstOrDefault();
return match != null ?
new reportUser
{
FBID = a.FBID,
CurrentPoints = a.CurrentPoints,
Name = match.Name,
Email = match.email
}
: new reportUser
{
FBID = 0, // a.FBID ?
CurrentPoints = 0, // a.CurrentPoints ?
Name = "",
Email = ""
}})
.Select(a => a)
.ToList();
However, I find it a bit hard to do this without seeing the structure of the database... or some sample data
Once you've got something working... then I highly recommend you try breaking this down into something more easily understandable - I'm really not sure what's going on here!
Here's a simple left outer join for you:
var query = from l leaders
join s in db.sweeps on l.FBID equals s.userFBID.First() into joined
from j in joined.FirstOrDefault()
select new reportUser
{
FBID = l.FBID,
CurrentPoints = l.CurrentPoints,
Name = j == null ? string.Empty : j.Name,
Email = j == null ? string.Empty : j.email
}
If this isn't quite what you are looking for... maybe try posting the SQL for what you actually want.

Adding where clause to nested Linq selects

I'm still new to Linq so if you see something I really shouldn't be doing, please feel free to suggest a change.
I am working on a new system to allow officers to sign up for overtime. Part of the data is displayed on a map with search criteria filtering unwanted positions. In order to make the data easier to work with, it is read into a hierarchy object structure using Linq. In this example, a job can contain multiple shifts and each shift can have multiple positions available. The Linq statement to read them in looks like the following.
var jobs = (from j in db.Job
join s in db.Shift on j.Id equals s.JobId into shifts
select new JobSearchResult
{
JobNumber = j.Id,
Name = j.JobName,
Latitude = j.LocationLatitude,
Longitude = j.LocationLongitude,
Address = j.AddressLine1,
Shifts = (from shift in shifts
join p in db.Position on shift.Id equals p.ShiftId into positions
select new ShiftSearchResult
{
Id = shift.Id,
Title = shift.ShiftTitle,
StartTime = shift.StartTime,
EndTime = shift.EndTime,
Positions = (from position in positions
select new PositionSearchResult
{
Id = position.Id,
Status = position.Status
}).ToList()
}).ToList()
});
That works fine and has been tested. There may be a better way to do it and if you know of a way, feel free to suggest. My problem is this. After the query is created, search criteria will be added. I know that I could add it when the query is created but for this its easier to do it after. Now, I can easy add criteria that looks like this.
jobs = jobs.Where(j => j.JobNumber == 1234);
However, I am having trouble figuring out how to do the same for Shifts or Positions. In other words, how would I could it to add the condition that a shift starts after a particular time? The following example is what I am trying to accomplish but will not (obviously) work.
jobs = jobs.Shifts.Where(s = s.StartTime > JobSearch.StartTime) //JobSearch.StartTime is a form variable.
Anyone have any suggestions?
Step 1: create associations so you can have the joins hidden behind EntitySet properties.
http://msdn.microsoft.com/en-us/library/bb629295.aspx
Step 2: construct your filters. You have 3 queryables and the possibility of filter interaction. Specify the innermost filter first so that the outer filters may make use of them.
Here are all jobs (unfiltered). Each job has only the shifts with 3 open positions. Each shift has those open positions.
Expression<Func<Position, bool>> PositionFilterExpression =
p => p.Status == "Open";
Expression<Func<Shift, bool>> ShiftFilterExpression =
s => s.Positions.Where(PositionFilterExpression).Count == 3
Expression<Func<Job, bool>> JobFilterExpression =
j => true
Step 3: put it all together:
List<JobSearchResult> jobs = db.Jobs
.Where(JobFilterExpression)
.Select(j => new JobSearchResult
{
JobNumber = j.Id,
Name = j.JobName,
Latitude = j.LocationLatitude,
Longitude = j.LocationLongitude,
Address = j.AddressLine1,
Shifts = j.Shifts
.Where(ShiftFilterExpression)
.Select(s => new ShiftSearchResult
{
Id = s.Id,
Title = s.ShiftTitle,
StartTime = s.StartTime,
EndTime = s.EndTime,
Positions = s.Positions
.Where(PositionFilterExpression)
.Select(p => new PositionSearchResult
{
Id = position.Id,
Status = position.Status
})
.ToList()
})
.ToList()
})
.ToList();

Categories

Resources