I have a number of instances where I need to return a data list that uses .GroupBy. In addition to dates and integers I also need to return Boolean values, which I cannot seem to do. An example model:
public class HolidayCheckList
{
public DateTime startDate { get; set; }
public DateTime endDate { get; set; }
public int stafferId { get; set; }
public bool startTime { get; set; }
public bool endTime { get; set; }
}
Below is the controller as it is at present:
var model = _db.AnnualLeaves
.Where(r => r.StartDate <= tTE && r.EndDate >= tTS)
.GroupBy(r => r.tBooked)
.Select(m => new HolidayCheckList
{
startDate = m.Max(r => r.StartDate),
endDate = m.Max(r => r.EndDate),
stafferId = m.Min(r => r.StafferId)
});
return View(model.ToList());
This does what I need. However, in addition to the startDate and endDate I need to return the startTime and endTime, which are Boolean values, to the View. I don't know whether I need an aggregate operator I am not aware of, need to include in the .GroupBy statement or maybe need a nested query. No doubt it will be something far simpler.
I did consider changing the data type to an integer, but would like to know if there is a way of doing this "properly".
As an aside, is there a learning resource or documentation for lambda queries? I can find basic information but nothing that details how .GroupBy works, or what the aggregate operators are.
If you're certain that all of the startTime and endTime values will be the same (or you don't care), you could use .First to select the first item in the group's startTime and endTime values:
var model = _db.AnnualLeaves
.Where(r => r.StartDate <= tTE && r.EndDate >= tTS)
.GroupBy(r => r.tBooked)
.Select(m => new HolidayCheckList
{
startDate = m.Max(r => r.StartDate),
endDate = m.Max(r => r.EndDate),
stafferId = m.Min(r => r.StafferId),
startTime = m.Select(r => r.StartTime).First(),
endTime = m.Select(r => r.EndTime).First()
});
A good resource for lambdas is the Lambda Expressions article on MSDN.
The Aggregation Operations looks like a good overview of the various aggregate operations available in LINQ. Unfortunately the examples are in VB.NET though.
Related
Is there a way to search for an item in a list that's nested inside another list based on a property value using LINQ?
Given the follow models below, for a given Order (variable customerOrder), I want to return the earliest order date (Date) where the Day is "Sunday".
models:
public class Order
{
public int Id { get; set; }
public List<OrderLine> OrderLines { get; set; }
}
public class OrderLine
{
public string Description { get; set; }
public List<OrderDate> OrderDates { get; set; }
}
public class OrderDate
{
public DateTime Date { get; set; }
public string Day { get; set; }
}
code:
var dates = new List<DateTime>();
foreach(var a in customerOrder.OrderLines)
{
var orderDate = a.OrderDates.Where(x => x.DateTypeId.Equals("Sunday")).FirstOrDefault();
dates.Add(orderDate.ActualDate);
}
dates.OrderBy(d => d.Date);
return dates.FirstOrDefault();
EDIT
More elegant query
You can use Linq to achieve your result.
Here is a query that would closely mimick your code.
customerOrder.OrderLines
.Select(ol => ol.OrderDates
.Where(x => x.Day.Equals("Sunday"))
.FirstOrDefault())
.Where(d => d != null)
.OrderBy(d => d.Date)
.FirstOrDefault();
which could be more elegantly rewritten as:
customerOrder.OrderLines
.SelectMany(ol => ol.OrderDates)
.OrderBy(d => d.Date)
.FirstOrDefault(d => d.Day == "Sunday");
Here is a Linqpad query with some test data and dump for you to try.
Simply copy and paste in Linqpad.
void Main()
{
var customerOrder = new Order
{
Id = 1,
OrderLines = Enumerable
.Range(0, 10)
.Select(i => new OrderLine
{
Description = $"Line Description {i}",
OrderDates = Enumerable.Range(0, 10)
.Select(j => new OrderDate
{
Date = DateTime.Now.AddDays(i+j),
Day = DateTime.Now.AddDays(i+j).DayOfWeek.ToString()
})
.ToList()
})
.ToList()
}
.Dump();
customerOrder.OrderLines
.SelectMany(ol => ol.OrderDates)
.OrderBy(d => d.Date)
.FirstOrDefault(d => d.Day == "Sunday")
.Dump();
}
// You can define other methods, fields, classes and namespaces here
public class Order
{
public int Id { get; set; }
public List<OrderLine> OrderLines { get; set; }
}
public class OrderLine
{
public string Description { get; set; }
public List<OrderDate> OrderDates { get; set; }
}
public class OrderDate
{
public DateTime Date { get; set; }
public string Day { get; set; }
}
On a side note the OrderDate class is not necessary. The DateTime type has a property DayOfWeek that you can use to test is a Date is a Sunday.
DayOfWeek is an enum so you can simply test MyDate.DayOfWeek == DayOfWeek.Sunday rather than relying on a string for that purpose.
First of all your code will not work as intended.
dates.OrderBy(d => d.Date); doesn't work: OrderBy returns an IEnumerable, it doesn't change the original collection. You should either use List.Sort` or do this:
dates = dates.OrderBy(d => d.Date).ToList()
Secondly, you use FirstOrDefault: it has an overload that accepts predicate to search with; so the Where call is not needed. In addition FirstOrDefault will return null if nothing found. If this is a possible scenario, you should consider checking whether orderDate is null:
var dates = new List<DateTime>();
foreach(var a in customerOrder.OrderLines)
{
var orderDate = a.OrderDates.FirstOrDefault(x => x.DateTypeId.Equals("Sunday"));
if (orderDate is {})
{
dates.Add(orderDate.ActualDate);
}
}
dates = dates.OrderBy(d => d.Date).ToList();
return dates.FirstOrDefault();
That should work fine. But it hard to guess what aspects of behavior of your code samples are intended and what are not. You ask about searching, but say nothing about OrderBy part. Could you clarify this part, please?
Answering the question, if by better you mean more compact way, you can go with something like this:
var result = customerOrder.OrderLines
.SelectMany(a => a.OrderDates)
.OrderBy(d => d.Date)
.FirstOrDefault(x => x.DateTypeId.Equals("Sunday"));
return result;
You shouldn't be bothered with better way now; firstly you should start with at least working way. I suggest you to learn how to do these things both using Linq and without using Linq.
Better is a bit subjective, but you can use the Enumerable.SelectMany extension method to flatten the OrderDate instances into one sequence.
Then you can use the Enumerable.Where extension method to filter the dates that are "Sunday".
Then you can use the Enumerable.Min extension method to get the minimum date.
All of this can be chained together into a single statement.
DateTime earliestSunday = customeOrder
.OrderLines
.SelectMany(ol => ol.OrderDates)
.Where(od => od.Day == "Sunday")
.Min(od => od.Date);
I guess this has been asked before but i don't find any good examples. I have this query.
series = await query
.GroupBy(o => new { o.AddedDate.Date.Month, o.AddedDate.Date.Year })
.Select(g => new DateLineGraphItem
{ Legend = new DateTime(g.Key.Year, g.Key.Month, 1), Number = g.Count() })
.ToListAsync();
Everything is the same all the time except that my DB has different names for all the "AddedDate" column and i'm trying to figure out how i can break this out into it's own method, something like this.
List<DateLineGraphItem> series = GroupByAndSelect("AddedDate")
List<DateLineGraphItem> series = GroupByAndSelect("CreatedDate")
Do i have to use Expressions and Predicates and all that or it is possible to do in some simple manner?
Do i have to use Expressions
It's always better to use expressions (the whole LINQ is based on that concept) than magic strings. The only problem is that there is no out of the box solution for composing expressions from other expressions, so you need to write your own or use 3rd party packages.
From the other side, the nameof operator eliminates most of the string drawbacks. And EF Core provides a handy method called EF.Property for simple scenarios like this.
So if the method contains string propertyName argument which points to direct property of type DateTime, you can simply replace the o.AddedDate with EF.Property<DateTime>(o, propertyName), e.g.
series = await query
.GroupBy(o => new { EF.Property<DateTime>(o, propertyName).Date.Month, EF.Property<DateTime>(o, propertyName).Date.Year })
.Select(g => new DateLineGraphItem
{ Legend = new DateTime(g.Key.Year, g.Key.Month, 1), Number = g.Count() })
.ToListAsync();
I had the same problem.
You may extend your entity class using an interface which contains a DateTime Field, like this:
public class EntityDb1 : IDatedEntity
{
public DateTime AddedDate { get; set; }
public DateTime MyDate => AddedDate;
}
public class EntityDb2 : IDatedEntity
{
public DateTime CreatedDate { get; set; }
public DateTime MyDate => CreatedDate;
}
public interface IDatedEntity
{
DateTime MyDate{ get; }
}
Then execute the query
public class Test
{
public static async Task<IEnumerable<DateLineGraphItem>> ExecuteQuery(IQueryable<IDatedEntity> entityList)
{
return await entityList
.GroupBy(o =>new { o.MyDate.Date.Month, o.MyDate.Date.Year })
.Select(g => new DateLineGraphItem(){ Legend = new DateTime(g.Key.Year, g.Key.Month, 1), Number = g.Count() })
.ToListAsync();
}
}
public class DateLineGraphItem
{
public DateTime Legend { get; set; }
public int Number { get; set; }
}
I have an entity like this:
public class Event
{
public string Code;
public DateTimeOffset DateTime;
}
I want to filter by Code and then group by DateTime.Date. I tried this:
var results = session
.Query<Event>()
.Where(e => e.Code == "123")
.GroupBy(e => e.DateTime.Date)
.ToList();
But I get the following error:
Raven.Client.Exceptions.InvalidQueryException: Field 'Code' isn't neither an aggregation operation nor part of the group by key
Query: from Events group by DateTime.Date where Code = $p0
Parameters: {"p0":"123"}
It can be seen from the resulting query that the where clause is being added after the group by clause, which explains the error.
So how do I perform this query in RavenDB?
EDIT:
The code "123" that I used was just an example. I need it to be a variable that is passed to the query, like this:
var results = session
.Query<Event>()
.Where(e => e.Code == code)
.GroupBy(e => e.DateTime.Date)
.ToList();
To start with, learn about the dynamic aggregation query syntax in:
https://demo.ravendb.net/demos/auto-indexes/auto-map-reduce-index
But, in your case you need to define a Static Map-Reduce Index to calculate this for you:(Sum up the number of (filtered) documents per unique Date)
i.e.
public class Result
{
public string Date { get; set; }
public int NumberOfDocs { get; set; }
}
Map = events => from event in events
where event.Code == "123"
select new Result
{
Date = event.DateTime.Date
NumberOfDocs = 1
}
Reduce = results => from result in results
group result by result.Date into g
select new Result
{
Date = g.Key,
Count = g.Sum(x => x.NumberOfDocs )
}
==> Learn about Static Map-Reduce Index in:
https://demo.ravendb.net/demos/static-indexes/map-reduce-index
Follow the detailed Walkthrough..
----------
Update:
You can use the following map-reduce index that aggregates the number of documents per Code & Date 'couple', and then you can query with 'Code'
public class Result
{
public string Code { get; set; }
public string Date { get; set; }
public int NumberOfDocs { get; set; }
}
Map = events => from event in events
select new Result
{
Code = event.Code
Date = event.DateTime.Date
NumberOfDocs = 1
}
Reduce = results => from result in results
group result by new
{
result.Code,
result.Date
}
into g
select new Result
{
Code = g.Key.Code
Date = g.Key.DateTime.Date,
NumberOfDocs = g.Sum(x => x.NumberOfDocs )
}
and then query
List<Result> queryResults = session.Query< Result, <Index_Name> >()
.Where(x => x.Code == "some-code-number")
.ToList();
and then you can also do in your code
queryResults.GroupBy(x => x.Date)
I want List (containing HoursCount and date) of record from database for specifies date-range
if for some date, records are not available in database then that date should be in list with count = 0
// Where GetDateWistUsage is...
public List<GraphData> GetDateWiseUsage(DateTime startDate, DateTime endDate)
{
return Set.Where(x => x.Date >= startDate.Date && x.Date < endDate.Date)
.GroupBy(x => x.Date)
.Select(x => new GraphData { Date = x.Key.Date, TotalHoursCount = x.Sum(i => i.TotalHours }).ToList();
}
// Where GraphData is
public class GraphData
{
public DateTime Date { get; set; }
public double TotalHoursCount { get; set; }
}
Here, suppose if we are passing Date 1-Dec-2018 to 20-Dec-2018 and for 15-Dec-2018 no record present in database. In that case, List should also contain 15-Dec-2018 with TotalHoursCount = 0
You should create some dummy data to fill in the gaps. Since you're doing a Sum, you can just create some dummies of your input data.
To do so, I've assumed that Set is List<SourceData> - change this as necessary:
public class SourceData
{
public DateTime Date { get; set; }
public long TotalHours { get; set; }
}
public class GraphData
{
public DateTime Date { get; set; }
public long TotalHoursCount { get; set; }
}
I've then added a method to get a date range (upper bound exclusive):
public static IEnumerable<DateTime> GetDateRange(DateTime startDate, DateTime endDate)
{
return Enumerable.Range(0, (endDate - startDate).Days).Select(d => startDate.AddDays(d));
}
Finally, I've updated your GetDataWiseUsage method to generate the date range and convert it into dummy source data:
public static List<GraphData> GetDateWiseUsage(DateTime startDate, DateTime endDate)
{
return Set.Where(x => x.Date >= startDate.Date && x.Date < endDate.Date)
.Concat(GetDateRange(startDate, endDate).Select(date => new SourceData() { Date = date, TotalHours = 0 }))
.GroupBy(x => x.Date.Date)
.Select(x => new GraphData { Date = x.Key, TotalHoursCount = x.Sum(i => i.TotalHours) }).ToList();
}
I've changed your code to group by x.Date.Date rather than x.Date, as it seems that you want a separate grouping per day, rather than per unique time. The group by will not group the dummy data with the real data, and factor it into the sum.
You could add the dummy data afterwards but it seems easier/less work to do it beforehand.
Updated with other relevant classes
public class TrendItem
{
public string ItemTitle { get; set; }
public IEnumerable<string> ItemValues { get; set; }
}
public class TrendValue
{
public int Id { get; set; }
public TrendResultType TrendType { get; set; }
public string Value { get; set; }
public Trend Trend { get; set; } // contains DateRecorded property
}
Please see the function below that leverages on EF Core (2.1):
public async Task<List<TrendItem>> GetTrends(
int companyId,
TrendResultType type,
DateTimeOffset startDate,
DateTimeOffset endDate,
RatingResultGroup group
)
{
var data = _dataContext.TrendValues.Where(rr =>
rr.Trend.CompanyId == companyId &&
rr.TrendType == type &&
rr.Trend.DateRecorded >= startDate &&
rr.Trend.DateRecorded <= endDate);
return await data.GroupBy(rr => new { rr.Trend.DateRecorded.Year, rr.Trend.DateRecorded.Month })
.Select(g => new TrendItem() { ItemTitle = $"{g.Key.Year}-{g.Key.Month}", ItemValues = g.Select(rr => rr.Value) })
.ToListAsync();
}
I'm getting problems, specifically with the portion g.Select(rr => rr.Value), where I intended to select a collection of values (strings).
Whenever I try to change that to something else like g.Sum(rr => int.Parse(rr.Value)), it works fine. It's the retrieval of the collection that seems to be a problem.
I always get ArgumentException: Argument types do not match.
Is this due to the async function?
Hmm, I'd start by looking to simplify the data retrieval into an anonymous type to perform the grouping, then transform into the view model. It may be that EF is getting a bit confused working with the grouped items values.
It looks like you want to group by date (year-month) then have a list of the values in each group:
var query = _dataContext.TrendValues
.Where(rr =>
rr.Trend.CompanyId == companyId
&& rr.TrendType == type
&& rr.Trend.DateRecorded >= startDate
&& rr.Trend.DateRecorded <= endDate)
.Select(rr => new
{
TrendYear = rr.Trend.DateRecorded.Year,
TrendMonth = rr.Trend.DateRecorded.Month,
rr.Value
}).ToListAsync();
var data = query.GroupBy(rr => new { rr.TrendYear, rr.TrendMonth })
.Select(g => new TrendItem() { ItemTitle = $"{g.Key.Year}-{g.Key.Month}", ItemValues = g.ToList().Select(rr => rr.Value).ToList() })
.ToList();
return data;
which could be simplified to:
var query = _dataContext.TrendValues
.Where(rr =>
rr.Trend.CompanyId == companyId &&
rr.TrendType == type &&
rr.Trend.DateRecorded >= startDate &&
rr.Trend.DateRecorded <= endDate)
.Select(rr => new
{
TrendDate = rr.Trend.DateRecorded.Year + "-" + rr.Trend.DateRecorded.Month,
rr.Value
}).ToListAsync();
var data = query.GroupBy(rr => rr.TrendDate)
.Select(g => new TrendItem() { ItemTitle = $"{g.Key}", ItemValues = g.ToList().Select(rr => rr.Value).ToList() })
.ToList();
return data;
The selecting of the TrendDate in the query may need some work if Trend.Year and Trend.Month are numeric fields.