I'm creating a safety tracking HR app, and I'm trying to use LINQ to return the lost hours for each month per safety incident type. The relevant entity/table columns are:
[safety_incident]:
[incident_id]
[incident_date]
[incident_type]
[facility_id]
[safety_hours]:
[safety_hours_id]
[incident_id]
[safety_hours_date]
[lost_hours]
The relationship between safety_incident and safety_hours is 0..n, with safety_hours noting hours lost for an incident on specific dates. I'm trying to return a record/object for each combination of incident type and month, over a year (not necessarily all in the same calendar year), where the lost hours is greater than 0. Without the one-year boundaries or limits on facility, I get what I need from SQL with this:
SELECT (datepart(year,safety_hours_date) * 100 + datepart(month,safety_hours_date)),
inc.incident_type, sum(sh.lost_hours)
FROM [hr].[safety_hours] sh
INNER JOIN [hr].safety_incident inc ON sh.incident_id = inc.incident_id
GROUP BY (datepart(year,safety_hours_date) * 100 + datepart(month,safety_hours_date)),
inc.incident_type
HAVING sum(sh.lost_hours) > 0
ORDER BY (datepart(year,safety_hours_date) * 100 + datepart(month,safety_hours_date)),
inc.incident_type
The closest I can get to an accepted LINQ query is this:
var monthInjuryHours =
from sh in _context.safety_hours
where sh.safety_hours_date >= firstMonth && sh.safety_hours_date < monthAfterLast && sh.lost_hours > 0
join inc in _context.safety_incident on sh.incident_id equals (inc.incident_id) into most
from m in most
where (facID == 0 || m.facility_id == facID)
group m by new
{
m.incident_type,
month = new DateTime(sh.safety_hours_date.Year, sh.safety_hours_date.Month, 1)
} into g
select new
{
injury = g.Key.incident_type,
month = g.Key.month,
hours = g.Sum(h => h.lost_hours) // ERROR, can't access lost_hours
};
Unfortunately, I can't specify lost_hours in the "hours" line, or any properties of safety_hours, only those of safety_incident come up after "h.". Very grateful for any help, thanks...
EDIT: I was able to rearrange the query to switch the order of the two tables and got something that ran:
var monthInjuryHours =
from inc in _context.safety_incident
where (facID == 0 || inc.facility_id == facID) && inc.incident_date < monthAfterLast
join sh in _context.safety_hours on inc.incident_id equals (sh.incident_id) into most
from m in most
where m.safety_hours_date >= firstMonth && m.safety_hours_date < monthAfterLast
&& m.lost_hours > 0
group m by new
{
// Note new aliases
inc.incident_type,
month = new DateTime(m.safety_hours_date.Year, m.safety_hours_date.Month, 1)
} into g
select new
{
injury = g.Key.incident_type,
month = g.Key.month,
hours = g.Sum(h => h.lost_hours)
};
But when I tried iterating through it with a foreach, a NotSupportedException pointing to the "in" keyword told me "Only parameterless constructors and initializers are supported in LINQ to Entities." I swear I've had other queries that returned collections of anonymous types but whatever...
Well, for now, the answer is "F(orget) LINQ". Thanks to Yuck's inspiring answer to this topic, a ToList() after the Where() allowed this syntax to get exactly the results I was after:
var monthInjuryHours = _context.safety_incident.Join(_context.safety_hours,
inc => inc.incident_id, sh => sh.incident_id, (inc, sh) => new { Inc = inc, Hours = sh })
.Where(j => j.Hours.lost_hours > 0 && (facID == 0 || j.Inc.facility_id == facID)
&& j.Inc.incident_date < monthAfterLast
&& j.Hours.safety_hours_date >= firstMonth
&& j.Hours.safety_hours_date < monthAfterLast).ToList() //Fixes things
.GroupBy(x => new { Month = new DateTime(x.Hours.safety_hours_date.Year,
x.Hours.safety_hours_date.Month, 1), x.Inc.incident_type })
.Select(g => new { g.Key.Month, g.Key.incident_type,
LostHours = g.Sum(x => x.Hours.lost_hours) })
.OrderBy(h => h.Month).ThenBy(h => h.incident_type);
I know this wasn't the prettiest or simplest example, but I hope it helps someone.
Related
I'm creating a production dashboard to shows the number of widgets produced by each work center in intervals of one hour. There are 'trigger' and 'target' values for widget production over the course of each hour. My query works...however it doesn't show a particular interval hour if no widgets were produced in that hour. I'm trying to do a sort of left outer join on the interval target table to capture all possible intervals but haven't been able to show intervals that have a zero widget count.
dbvm.widgetSummary = from wc in db.workCenters
from w in db.widgets.Where(w => w.createdByID == wc.ID && w.packDate >= beginDate && w.packDate <= endDate)
from d in db.departments.Where(d => d.ID == wc.deptID && wc.display == true)
from g in db.goals.Where(g => g.workCenterID == wc.ID && g.hour == w.packDate.Value.Hour)
group g by new { wc.ID, wc.friendlyName, g.hour, g.trigger, g.target, d.dept } into n
select new WidgetSummary()
{
createdByID = n.Key.ID,
friendlyName = n.Key.friendlyName,
hour = n.Key.hour,
actualWidgets = n.Count(),
triggerWidgets = n.Key.trigger,
targetWidgets = n.Key.target,
variance = (n.Count() - n.Key.target),
dept = n.Key.dept
};
return View(dbvm);
Well you can do a left outer join.
from wc in db.workCenters
join jw in db.widgets.Where(w => w.packDate >= beginDate && w.packDate <= endDate) on wc.ID equals jw.createdByID into wgj
from w in wgj.DefaultIfEmpty() ...
But why don't you have wc.Widgets and wc.Departments and wc.Goals. Seams to me your domain model is not all that Object oriented and you are just using flat tables without relationships.
Also if you are using EF you might want to look in to perf optimization
I'm having an issue with a linq subquery return invalid data when adding in datetime checks as part of the where clause.
This is the original query and it is returning 0; because the result set is null
var subquery =
(from item in g
from e in item.Entry
where e.Type == 1
&& e.EntryType == 2
&& item.StartDate >= priorMonthStartOfDay
&& item.EndDate <= startOfDayQueryParam
select e.Amount).Sum() ?? 0M;
I modified the query to see what the data was; here is that query and the resulting dataset.
var subquery =
(from item in g
from e in item.Entry
where e.Type == 1
&& e.EntryType == 2
select new
{
Amount = e.Amount,
SD = item.StartDate,
ED = item.EndDate,
QD = priorMonthStartOfDay
};
So then I added in the start date comparison and the results are below. The priorMonthStartOfDay is a DateTime with a value of 12/1/2015 12:00:00 AM
var subquery =
(from item in g
from e in item.Entry
where e.Type == 1
&& e.EntryType == 2
&& item.StartDate >= priorMonthStartOfDay
select new
{
Amount = e.Amount,
SD = item.StartDate,
ED = item.EndDate,
QD = priorMonthStartOfDay
};
Why is the date comparison not behaving as I would expected? Given the value of priorMonthStartOfDay, I would expect the result set to be the same for the last two queries. I'm guessing it has something to do with the time equal comparison because if I subtract a second from the priorMonthStartOfDay then the result sets match up again.
The only logical explanation could be that your priorMonthStartOfDay and/or startOfDayQueryParam variables contain time part not shown in the debugger. Note that by default milliseconds part is not shown, not to mention ticks.
To be 100% sure you are comparing against dates, change the date part of the criteria to
&& item.StartDate >= priorMonthStartOfDay.Date
&& item.EndDate <= startOfDayQueryParam.Date
I am trying to write a query which returns the average time spent on a job in a ticketing system.
There are multiple time logs in each job so I need to group by the SO Number and then somehow get an average of all of the results.
The current query returns a list of every service order and the total minutes spent to the job.
How do I make this show me the average minutes spent on each job?
from so in TblServiceOrders
from sologs in TblSOLogs.Where(x => x.SONumber == so.SONumber).DefaultIfEmpty()
where so.DateClosed >= new DateTime(2013,07,01)
where so.DateClosed <= new DateTime(2013,07,02)
where sologs.ElapsedHours != 0 || sologs.ElapsedMinutes != 0
group new { sologs.ElapsedHours, sologs.ElapsedMinutes } by so into g
select new {
g.Key.SONumber,
elapsed = g.Average (x => (x.ElapsedHours == null ? 0 : x.ElapsedHours * 60) + (x.ElapsedMinutes == null ? 0 : x.ElapsedMinutes))
}
==EDIT==
This looks like it is getting close but it is giving me an average of every time log and not an average of the total time logs in each SO.
Please help?
from so in TblServiceOrders
join sologs in TblSOLogs on so.SONumber equals sologs.SONumber
where so.DateClosed >= new DateTime(2013,07,01)
where so.DateClosed <= new DateTime(2013,07,03)
where sologs.ElapsedHours != 0 || sologs.ElapsedMinutes != 0
group sologs.SONumber by sologs into g
group new {g.Key.ElapsedHours, g.Key.ElapsedMinutes} by "Total" into t
select t.Average (x => (x.ElapsedHours == null ? 0 : x.ElapsedHours * 60) + (x.ElapsedMinutes == null ? 0 : x.ElapsedMinutes))
I've worked it out for the most part, Hopefully this can help others.
The first part of my question was getting all the results into the same group without a distinct value to Group By. This was achieved by declaring a string in the Group By such as "Total".
For Example:
group tblName.FieldName by "Example" into group1
For the second part I needed to perform a Group By on the Sum of another Group By. Below is an example of declaring two new values generated from the Sum of a Group By:
group new { tblName.FieldName1, tblName.FieldName2} by tblName into group1
group new { SumField1 = g.Sum (x => x.FieldName1), SumField2 = g.Sum (x => x.FieldName2) } by "Totals" into newgroup2
Here is a full example of the code I ended up writing, It works well in LINQPad but I am still working on implementation into a Visual Studio C# DataGridView.
from so in TblServiceOrders
join sologs in TblSOLogs on so.SONumber equals sologs.SONumber
where so.DateClosed >= new DateTime(2014,01,17)
where so.DateClosed <= new DateTime(2014,01,17)
where sologs.ElapsedHours != 0 || sologs.ElapsedMinutes != 0
group new {sologs.ElapsedHours, sologs.ElapsedMinutes} by sologs.SONumber into g
group new {hours = g.Sum (x => x.ElapsedHours), mins = g.Sum (x => x.ElapsedMinutes)} by "Totals" into t
select new {
Average = t.Average (x => (x.hours * 60) + x.mins),
Count = t.Count ()
}
It compiles normal but when I try to iterate through result of the LINQ query I 've got such exception The group by operation contains an expression that cannot be translated
The query is
var query0 = from c in dc.Prices
where Convert.ToDateTime(c.data).CompareTo(left) >= 0
&& Convert.ToDateTime(c.data).CompareTo(right) <= 0
&& c.idsticker.Equals(x)
group c by new { ((DateTime)c.data).Year, ((DateTime)c.data).Month }
into groupMonthAvg
select new
{
years = groupMonthAvg.Key.Year,
months = groupMonthAvg.Key.Month,
prices = groupMonthAvg.Average(i => i.value)
};
What expression in group by function is wrong?
Try this:
var query0 = from c in dc.Prices
let date = Convert.ToDateTime(c.data)
where date.CompareTo(left) >= 0 && date.CompareTo(right) <= 0 && c.idsticker.Equals(x)
group c by new { date.Year, date.Month } into groupMonthAvg
select new
{
years = groupMonthAvg.Key.Year,
months = groupMonthAvg.Key.Month,
prices = groupMonthAvg.Average(i => i.value)
};
I'm guessing this is because the data column in Price is a String or some other type other than DateTime.
Try changing the (DateTime)c.data) cast to a Convert.ToDateTime(c.data) instead.
I am not sure if the Linq-SQL translator supports casts.
If possible change the underlying data type in the database to a DateTime if it stores a Date Time value.
I have the following code:
var allWorkorders =
(from wo in context.WORKORDERs
join wot in context.WORKORDERTYPEs on wo.wot_oi equals wot.wotyoi
join pri in context.PRIORITies on wo.prio_oi equals pri.priooi
join s in context.SITEs on wo.BEparn_oi equals s.siteoi
where wo.audt_created_dttm.Value.Year >= now.Year - 3 && wo.audt_created_dttm.Value.Year >= 2006
&& wo.audt_created_dttm < timeframe && (s.id == "NM" || s.id == "TH") &&
(wo.clsdt_date ?? new DateTime(3000, 01, 01)) < DateTime.Now
group pri by new {s.id, pri.prioid, MonthNum = (wo.clsdt_date ?? new DateTime(3000, 01, 01)).Year * 100 +
(wo.clsdt_date ?? new DateTime(3000, 01, 01)).Month} into groupItem
orderby groupItem.Key.MonthNum, groupItem.Key.id
select new {groupItem.Key.id, groupItem.Key.prioid, groupItem.Key.MonthNum, Unit = groupItem.Count()});
allWorkorders.GroupBy(x => new { x.id, x.MonthNum }).Select(x => new {x.Key.id, x.Key.MonthNum,
Denominator = x.Sum(y => y.Unit), Numerator = x.Where(y => SqlMethods.Like(y.prioid, "1%") ||
SqlMethods.Like(y.prioid, "6%")).Sum(y => y.Unit), Data_Indicator = DATA_INDICATOR,
Budgeted = budgetedPlannedOutageHrs, Industry_Benchmark = INDUSTRY_BENCHMARK,
Comments = comments, Executive_Comments = executiveComments,
Fleet_Exec_Comments = fleetExecComments}).ToList();
I want to create a for loop:
for (int counter = 0; counter < allWorkorders.Count; counter++)
{
var item = allWorkorders[counter];
......
However, I get the following error: " '<' cannot be applied to operands of type 'int' and 'method group'"
So even though I have allWorkorders going to ToList() it's not being recognized as a list.
What am I doing wrong? I have done this in the past, the biggest difference being that in the past cases my ToList was at the end of the select statement.
It is trying to use the LINQ extension method method Count() rather than the List<T>.Count
The reason it is doing this is you are not assigning the results of ToList() to anything. This whole statement is basically ignored because you are not using the return value
allWorkorders.GroupBy(x => new { x.id, x.MonthNum }).Select(x => new {x.Key.id, x.Key.MonthNum,
Denominator = x.Sum(y => y.Unit), Numerator = x.Where(y => SqlMethods.Like(y.prioid, "1%") ||
SqlMethods.Like(y.prioid, "6%")).Sum(y => y.Unit), Data_Indicator = DATA_INDICATOR,
Budgeted = budgetedPlannedOutageHrs, Industry_Benchmark = INDUSTRY_BENCHMARK,
Comments = comments, Executive_Comments = executiveComments,
Fleet_Exec_Comments = fleetExecComments}).ToList();
You didn't assign the second line (the one with ToList()) on it to anything. You ended the assignment of allWorkOrders with: "Unit = groupItem.Count()});"
Dropping on ToList() will make it return a list, but since you didn't assign it to anything it immediately goes out of scope and you lose it.
ToList() returns a result. You need something like
var newList = allWorkorders.GroupBy(x => ...).Select(x => ...).ToList();
You can either use a foreach loop instead or add the () to Count, for (int counter = 0; counter < allWorkorders.Count(); counter++). In this case Count is not a property but a Linq extension method that you're calling on an IEnumerable, which is why it complains about a method group when you give it just .Count instead of calling the method .Count().
Also, you have two separate statements there and don't appear to be storing the part which you are performing the .ToList() against anywhere.