Linq DefaultValues on Join - c#

I'm trying to get the total download count of my app for the last 30 days on different devices, i succeeded of returning the right query, by grouping by the number of days and joinning with an enumerable with the last thirty days. However I'm not able to format the output as i want. Let me share the query first with the presentation in LinqPad
var last_days = (from idx in Enumerable.Range(1, (DateTime.Now - DateTime.Now.AddDays(-30)).Days)
select new { day = DateTime.Now.AddDays(-30).AddDays(idx).Date});
var orders = (from od in Orders
group od by EntityFunctions.AddSeconds((DateTime?)new DateTime(1970,1,1,0,0,0,0), (int?)od.Created) into g
select new {
day = g.Key ,
web = g.Where( q => q.Source == "web").Count(),
ios = g.Where( q => q.Source == "ios").Count(),
android = g.Where( q => q.Source == "android").Count(),
total = g.Count()
}).OrderByDescending(q => q.day).Take(31);
var days=
(from d in last_days
join od in orders on d.day equals od.day into x
from od in x.DefaultIfEmpty()
select x );
days.Dump();
This is the result I get
Now, I want to format the final output to an IEnumerable of 5 columns(day, web, ios, android, total) regardless whether it was empty or not. So instead of the empty O sign, I get the date, and the web = ios = android = total = 0. How can I do this?
So on day without any downloads, I still get an entry with the date and platforms to 0.

This is hardly the most elegant solution, but something like this should work:
var days = last_days.Select(d =>
orders.DefaultIfEmpty(new {
day = d,
web = 0,
ios = 0,
android = 0,
total = 0
}).FirstOrDefault(od =>
od.day == d.Date));
The basic idea is to tell the generator what to fall back on, in each case, if an appropriate order entry cannot be found.
In retrospect, it's probably easier to start from a blank slate. What about something more like:
var last_30_days =
from idx in Enumerable.Range(1, 30)
orderby idx descending
select DateTime.Now.AddDays(idx - 30).Date;
var orders =
from date in last_30_days
let datesOrders = Orders.Where(order => order.Created == date)
select new Info()
{
Date = date,
Web = datesOrders.Where(q => q.Source == "web").Count(),
iOS = datesOrders.Where(q => q.Source == "ios").Count(),
Android = datesOrders.Where(q => q.Source == "android").Count(),
Total = datesOrders.Count()
};

What about selecting new DaySum { ord = (ord == null ? 0 : ord.Count) };
Instead of just
Select x

Got it right, generated sql is okay in complexity, was a bit rusty on linq i guess
var last_days = (from idx in Enumerable.Range(1, (DateTime.Now - DateTime.Now.AddDays(-31)).Days)
select new { day = DateTime.Now.AddDays(-31).AddDays(idx).Date});
var orders = (from od in Orders
where od.ServiceProviderID == 2
group od by new DateTime(1970,1,1,0,0,0,0).AddSeconds(od.Created).Date into g
select new {
day = (DateTime)g.Key ,
web = g.Where( q => q.Source == "web").Count(),
ios = g.Where( q => q.Source == "ios").Count(),
android = g.Where( q => q.Source == "android").Count(),
total = g.Count()
}).OrderByDescending(q => q.day).Take(32);
var days = (from d in last_days
join od in orders on d.day.Date equals od.day.Date into x
from od in x.DefaultIfEmpty()
select new {
day = d.day ,
web = (od == null) ? 0: od.web,
ios = (od == null) ? 0: od.ios,
android = (od == null) ? 0: od.android,
total = (od == null) ? 0 : od.total
} );
days.Dump();

Related

Linq query: inner join with a count() group by subquery

We have some Machines having Devices attached. Not all the machines have devices atached and the devices cand be moved between machines. The devices generate Errors and we need to count those errors occured in the past day.
We have four tables: Machines (with Id and Code), Devices (with Id and Code), a pairing table DevicesMachines (Id, IdMachine, IdDevice, From datetime, To datetime) and Errors(Id, IdDevice, Moment datetime, Description).
The working SQL query is this:
Select m.Id, m.Code,
Coalesce(d.Code, 'NA') As DeviceCode,
Coalesce(Err.ErrorCnt,0) As ErrorCnt
From Machines As m
Left Outer Join (Select IdMachine, IdDevice From DevicesMachines as dm
Where GetDate() Between dm.From And dm.To) As dm on m.Id=dm.IdMachine
Left Outer Join Devices As d on dm.IdDevice=d.Id
Left outer join
( Select IdMachine, Count(Id) As ErrorCnt From Errors as er
Where er.Moment >= DateAdd(day,-1,GetUtcDate())
Group By IdMachine) As Err
On m.Id=Err.IdMachine
I have tried many syntaxes, one of which is below:
using ( DataContextM dcMachines = new dataContextM())
{
IEnumerable<MachineRow> lstM =
from m in dcMachines.Machines
from dm in dcMachines.DevicesMachines.Where(dm => (dm.IdMachine == m.Id) && (dm.From <= DateTime.Now) && (dm.To >= DateTime.Now)).DefaultIfEmpty()
from d in dcMachines.Devices.Where(d => d.Id == dm.IdDevice).DefaultIfEmpty()
from er in dcMachines.Errors
.Where(er => (er.Moment >= DateTime.Now) && (er.Moment <= DateTime.Now.AddDays(-1)))
.GroupBy(er => er.IdMachine)
.Select(er => new { IdMachine = er.Key, ErrorCnt = er.Count() })
.Where(er=> er.IdMachine==m.Id).DefaultIfEmpty()
select new MachineRow
{
Id = amId,
Code = m.Code,
DeviceCode = (d == null) ? "NA" : d.DeviceCode,
IdDevice = (d == null) ? 0: d.Id,
ErrorCnt = (er == null) ? 0 : er.ErrorCnt
};
}
I failed to find the right Linq syntax and I need your help.
Thank you,
Daniel
Based on the SQL you provided, I created what I think is the equivalent EF LINQ expression:
using (var dcMachines = new DataContextM())
{
var now = DateTime.Now;
var utcYesterday = DateTime.UtcNow.AddDays(-1);
var devicesMachinesQuery =
from dm in dcMachines.DevicesMachines
where dm.From <= now && dm.To >= now
join d in dcMachines.Devices on dm.IdDevice equals d.Id into dItems
from d in dItems.DefaultIfEmpty()
select new
{
dm.IdMachine,
dm.IdDevice,
DeviceCode = d != null ? d.Code : "NA"
};
var errorsQuery =
from err in dcMachines.Errors
where err.Moment >= utcYesterday
select err;
IEnumerable<MachineRow> lstM =
from m in dcMachines.Machines
join dm in devicesMachinesQuery on m.Id equals dm.IdMachine into dmItems
from dm in dmItems.DefaultIfEmpty()
select new MachineRow
{
Id = m.Id,
Code = m.Code,
DeviceCode = dm != null ? dm.DeviceCode : "NA",
IdDevice = dm != null ? dm.IdDevice : 0,
ErrorCnt = (
from err in errorsQuery
where err.IdMachine == m.Id
select err.Id
)
.Count()
};
}
I made some tests in memory and it seems to yield the same behavior as your provided SQL query.

Entity framework filtering data by time

I am so sorry from the question, but I can not take a period from a DateTime. for exemple: If I have date "10.10.2016 7:00", 10.10.2016 10:00", I need to take only the rows with the time between "6:00" and "8:00". Next is my code by return an error : "can not use TimeOfDay ",help me please
ds.TrafficJamMorning = (from row in orderQuery
where row.AcceptedTime.TimeOfDay >= new TimeSpan(6, 30, 0) &&
row.AcceptedTime.TimeOfDay <= new TimeSpan(9, 30, 0)
group row by row.AcceptedTime.Date
into grp
select new TrafficJamPeriodInfo
{
CurrentDateTime = grp.Key,
ReceptionCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.Reception),
InternetCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.Internet),
ExchangeSystemCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.ExchangeSystem)
}).ToList();
TimeOfDay Is not supported by the linq provider and it does not know how to parse it into sql. Use instead DbFunctions.CreateTime:
Also instantiate the timespans before the linq query so you do not instantiate a new object every time
var startTime = new TimeSpan(6, 30, 0);
var endTime = new TimeSpan(9, 30, 0);
var result = (from row in orderQuery
let time = DbFunctions.CreateTime(row.AcceptedTime.Hour, row.AcceptedTime.Minute, row.AcceptedTime.Second)
where time >= startTime &&
time <= endTime
group row by DbFunctions.TruncateTime(row.AcceptedTime) into grp
select new TrafficJamPeriodInfo
{
CurrentDateTime = grp.Key,
ReceptionCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.Reception),
InternetCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.Internet),
ExchangeSystemCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.ExchangeSystem)
}).ToList();
Looking again at the question - If all you want to check is that it is between 2 hours then use the Hour property (This won't be nice to write if you want to check for example Hour and Minues and in that case I'd go for my first suggestion):
var result = (from row in orderQuery
where row.AcceptedTime.Hour >= 6
row.AcceptedTime.Hour < 8
group row by DbFunctions.TruncateTime(row.AcceptedTime) into grp
select new TrafficJamPeriodInfo
{
CurrentDateTime = grp.Key,
ReceptionCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.Reception),
InternetCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.Internet),
ExchangeSystemCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.ExchangeSystem)
}).ToList();
I use the following where clause on my IQueryable:
var query = dbContext.GetAllItems().AsQueryable();
//... other filters
if(MusBeBetween6and8){
query = query.Where(item => item.AcceptedTime.Hour > 6 && item.AcceptedTime.Hour < 8);
}
//... other filters
return query.ToList();
Hope it helps. This also works for Oracle + Odac.
ds.TrafficJamMorning = (from row in orderQuery
where
DbFunctions.DiffMinutes( DbFunctions.TruncateTime(row.AcceptedTime), row.AcceptedTime) >= 6 * 60 + 30 &&
DbFunctions.DiffMinutes( DbFunctions.TruncateTime(row.AcceptedTime), row.AcceptedTime) <= 9 * 60 + 30
group row by DbFunctions.TruncateTime(row.AcceptedTime)
into grp
select new TrafficJamPeriodInfo
{
CurrentDateTime = grp.Key,
ReceptionCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.Reception),
InternetCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.Internet),
ExchangeSystemCount = grp.Count(r => r.OrderOriginId == (int)OrderOrigin.ExchangeSystem)
}).ToList();
I had a similar problem.
You can compare the date parts instead.
where row.Year > s.Year && r.Month > s.Month && row.Day > s.Day

LINQ left outer join on date not working

I have this collection of GroupedResult
IQueryable<GroupedResult> groupedResult = from p in db.Departure
group p by new { p.Terminal, p.DepartureDate, p.DepartureDate.Month } into g
select new GroupedResult
{
terminal = g.Key.Terminal.Code,
date = g.Key.DepartureDate,
distance = g.Count(),
month = g.Key.Month
};
The problem is that the collection I get has some dates missing. For example db.Departure contains Feb. 1, 2, 4, and 6. I also wanted to show GroupedResult for Feb. 3 and 5 so I use the following code to create a collection of dates starting from a particular start date:
var dates = new List<DateTime>();
for (var dt = date; dt <= DateTime.Now; dt = dt.AddDays(1))
{
dates.Add(dt);
}
And then join dates with groupedResult
var result = from p in groupedResult.ToList()
from q in dates.Where(r => r == p.date).DefaultIfEmpty()
select p;
The result I get is same as the one with groupedResult. How can I also show the entries with no date data?
This is because you are selecting P at the very end.....you have to create new anonymous class
var result = from q in dates
join p in groupedResult.ToList() on q equals p.date into joinedResult
from r in joinedResult.DefaultIfEmpty()
select new
{
terminal = r==null?null:r.terminal,
date = q,
distance = r==null?null:r.distance,
month = r==null?null:r.month
};
var result = from p in groupedResult.ToList()
from q in dates.Where(r => r == p.date).DefaultIfEmpty()
select p;
You've got the left join the wrong way around. You're selecting all items from your groupedResult and then ask LINQ to get all dates - or none if there is none - that match a date that already exists in the groupedResult.
var result = from q in dates
from p in groupedResult.Where(r => r.date == q).DefaultIfEmpty()
select new { Date = q, Items = p };
This works, because you select all dates and search for matching grouped items instead.

How do I get the max of each group?

Consider the following LINQ statement:
var posts = db.Posts
.Where(p => p.Votes.Count > 0 && p.User.Confirmed)
.Select(p => new
{
PostId = p.PostId,
Votes = p.Votes.Count(),
Hours = EntityFunctions.DiffHours(DateTime.UtcNow, p.Timestamp)
})
.Select(p1 => new
{
PostId = p1.PostId,
Votes = p1.Votes,
Group = p1.Hours <= 24 ? 24 :
p1.Hours <= 168 ? 168 :
p1.Hours <= 720 ? 720 : 0
})
.Where(p2 => p2.Group != 0);
It successfully groups a listing of posts into their respective groups: 24 hours, 168 hours, and 720 hours.
However, now I need to get the PostId that has the Max Votes for each group. How do I do that?
var postIds = posts.OrderByDescending(x => x.PostId).GroupBy(x => x.Group)
.Select(x => x.First().PostId);
Or, for a bit more clarity (IMHO), and (I think) less speed:
var postIds = posts.GroupBy(x => x.Group).Select(g => g.Max(p => p.PostId));
The former has the benefit that if you want the post, and not just the PostId, you have that available more easily.
I was looking at this but kind of slow. It's a little different syntax so I'll post it anyway
var groups = (from p in posts
group p by p.Group into g
select new
{
Id = g.Max(p => p.Id),
Group = g.Key
}).ToList();
var bestPosts = (from p in posts
join j in groups on new {p.Group, p.Votes} equals new {j.Group, j.Votes}
select p).ToList();
Groups according to "GroupByField" and selects the max.
var query = from o in _context.Objects
group o by o.GroupByField
into group
select new
{
maxParameter = (from o in group orderby o.OrderByField select o).Last()
};
and then in order to select the original (max) objects
var largest = query.Select(q => q.maxParameter).ToList();

LINQ query to group records by month within a period

I am looking for some help on adapting the following LINQ query to return all dates within the next 6 months, even those where no records fall within the given month.
var maxDate = DateTime.Now.AddMonths(6);
var orders = (from ord in db.Items
where (ord.Expiry >= DateTime.Now && ord.Expiry <= maxDate)
group ord by new
{
ord.Expiry.Value.Year,
ord.Expiry.Value.Month
}
into g
select new ExpiriesOwnedModel
{
Month = g.Select(n => n.Expiry.Value.Month).First(),
Quantity = g.Count()
}).ToList();
I'd really appreciate any assistance or pointers on how best to implement this.
I'm not sure how well it'll interact with your database, but I'd do this as with a join:
var firstDaysOfMonths = Enumerable.Range(0, 7).Select(i =>
new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1).AddMonths(i));
var orders = firstDaysOfMonths.GroupJoin(
db.Items,
fd => fd,
ord => new DateTime(ord.Expiry.Value.Year, ord.Expiry.Value.Month, 1),
(fd, ords) => new { Month = fd.Month, Quantity = ords.Count() });
Note you may end up with an extra month where before you didn't (on the first day of the month?)
Stolen from Rawling's answer, if you prefer query syntax for group joins (I do):
var orders =
from month in Enumerable.Range(0, 7)
.Select(i => new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1).AddMonths(i))
join ord in db.Items
on month equals new DateTime(ord.Expiry.Value.Year, ord.Expiry.Value.Month, 1)
into ords
select new { month.Month, Quantity = ords.Count() };
Alternative if it does not play nice with the database:
var rawGroups = db.Items.Where(item.Expiry >= DateTime.Now && ord.Expiry <= maxDate)
.GroupBy(item => new
{
item.Expiry.Value.Year,
item.Expiry.Value.Month
}, g => new ExpiriesOwnedModel()
{
Month = g.Key.Month,
Quantity = g.Count()
}).ToDictionary(model => model.Month);
var result = Enumerable.Range(DateTime.Now.Month,6)
.Select(i => i > 12 ? i - 12 , i)
.Select(i => rawGroups.Keys.Contains(i) ?
rawGroups[i] :
new ExpiriesOwnedModel()
{ Month = i , Quantity = 0 });

Categories

Resources