Creating Nested Groupings using Linq Extension Methods - c#

I am building an application that has a diary feature, and within that object there are a list of appointments. For display purposes I have a requirement to create a nested groupings in the following format
Heading 1
Sub-heading 1
Data row 1
Data row 2
Sub-heading 2
Data row 1
Data row 2
Heading 2
Sub-heading 1
Data row 1
Data row 2
where heading is the office that the appointment is in, and the sub-heading is the room that this is in. This will make more sense when looking at the following models;
public class Diary
{
public int Id {get; set; }
public DateTime DiaryDate {get; set; }
List<Appointment> Appointments {get; set;}
}
public class Appointment
{
public int Id {get; set;}
public int DiaryId {get; set;}
public DateTime StartTime {get; set;}
public DateTime EndTime {get; set; }
public string Attendees {get; set; }
public Office Office {get; set; }
public Room Room {get; set; }
}
Office is an enum with office locations in, as is Room with rooms in it.
Currently I have solved this problem using this code:
Appointments.GroupBy(k => k.Office, k => k)
.ToDictionary(k => k.Key,
k => k.ToList().GroupBy(sk => sk.Room)
.ToDictionary(mk => mk.Key, mk => mk.ToList()));
I am writing the diary to a view model where the appointments is of type
public Dictionary<Office, Dictionary<Room, List<Appointment>>> Appointments { get; set; }
Essentially, my long group by statement is grouping the the data by Office and then using the resultSelector overload to project the original object. Then, I am turning that IGrouping result into a Dictionary, where the key is office and value is list of type Appointment. From that I am then grouping each list of type Appointment by Room, creating a dictionary within a dictionary, which produces the type Dictionary with Key Office, and then dictionary with key Room and list of Appointment as the value to each key.
This code produces the desired result, but I think it is difficult to read, more difficult to understand in the view when looping and probably quite inefficient.
Can somebody offer some advice as how I can simply the way I achieve my desired result?

You're actually building up a lookup, a dictionary with multiple values per key. Just use that.
Your query simply becomes:
var query = appointments.OrderBy(a => a.Office).ThenBy(a => a.Room)
.GroupBy(a => a.Office)
.ToDictionary(g => g.Key, g => g.ToLookup(a => a.Room));

Related

SQL to LINQ Conversion Involving Max()

I wanted to retrieve the top 5 merchants with the latest order and the grand total amount. However, I can't seem to get the Grand total amount. I think the issue lies within LastOrderGrandTotal= x.FirstOrDefault().GrandTotal. The code below syntax is in LINQ method syntax
var list = _ApplicationDbContext.OrderTransaction
.Where(x => x.Status != 0)
.GroupBy(x => x.MerchantUserId)
.Select(x => new DashboardActiveMerchantStore { MerchantUserId = x.Key, CreatedDate = x.Max(c => c.CreatedDate), LastOrderGrandTotal= x.FirstOrDefault().GrandTotal })
.OrderByDescending(x => x.CreatedDate)
.Take(5)
.ToList();
For further clarification the SQL syntax for the query above is
select TOP(5) MerchantUserId,MAX(CreatedDate),GrandTotal from OrderTransaction
group by MerchantUserId
order by CreatedDate desc
I have a class to store the data retrieve from the LINQ syntax
public class DashboardActiveMerchantStore
{
public string MerchantName { get; set; }
public double LastOrderGrandTotal { get; set; }
public Guid MerchantUserId { get; set; }
public DateTime CreatedDate { get; set; }
}
As you are using entity framework, a totally different, more natural approach is possible. You can use the virtual ICollection<...>.
I wanted to retrieve the top 5 merchants with the latest order and the grand total amount.
The classes in entity framework
It seems there is a one-to-many relation between Merchants and OrderTransactions: every Merchant has zero or more OrderTransactions, and every OrderTransaction is a transaction of exactly when Merchant, namely the Merchant that the foreign key MechantId refers to.
If you've followed the entity framework conventions, you will have classes similar to:
public class Merchant
{
public Guid Id {get; set;} // primary key
public string Name {get; set;}
...
// Every Merchant has zero or more TransactionOrders (one-to-many)
public virtual ICollection<TransactionOrder> TransactionOrders {get; set;}
}
public class TransactionOrder
{
public Guid Id {get; set;} // primary key
public DateTime CreatedDate {get; set;}
public int Status {get; set;}
public double GrandTotal {get; set;}
...
// every TransactionOrder is an order of a Merchant, using foreign key
public Guid MerchantId {get; set;}
public virtual Merchant Merchant {get; set;}
}
And of course the DbContext:
public class OrderDbContext : DbContext
{
public DbSet<Merchant> Merchants {get; set;}
public DbSet<TransactionOrder> TransactionOrders {get; set;}
}
This is all that entity framework needs to detect the tables, the columns of the tables and the one-to-many relation between the table. Only if you deviate from the conventions, like you want to use different property names, or different table names, you need attributes or fluent API.
In entity framework the columns of a table are represented by non-virtual properties. The virtual properties represent the relations between the tables (one-to-many, many-to-many)
Get top 5 Merchants
If you want some information about "merchants that ... with some information about some of their OrderTransactions" you start at dbContext.Merchants;
If you want some information about "OrderTransactions that ..., each with some information about their Merchant", you start at dbContext.OrderTransactions.
I want to query the top 5 merchants with the latest order and the grand total amount
So you want Merchants. From these Merchants you want at least their Id, and their Name, and some information about their OrderTransactions.
Not all OrderTransactions, only information about their last OrderTransaction with a non-zero Status.
From this last OrderTransaction you want the CreatedDate and the GrandTotal.
Now that you've got Merchants with their last non-zero Status Order, you don't want all these Merchants, you only want the five Merchants with the newest CreatedDate.
I hope the above is your requirement. It is not what your SQL said, but your SQL didn't fetch Merchants, it fetched groups of TransactionOrders.
var result = dbContext.Merchants
.Select(merchant => new
{
Id = merchant.Id,
Name = merchant.Name,
// from Every Merchant, get the Date and Total of their last
// non-zero status Order
// or null if there is no such order at all
LastOrder = merchant.OrderTransactions
.Where(orderTransaction => orderTransaction.Status != 0)
.OrderByDescending(orderTransaction => oderTransaction.CreatedDate)
.Select(orderTransaction => new
{
CreatedDate = orderTransaction.CreatedDate,
GrandTotal = orderTransaction.GrandTotal,
})
.FirstOrDefault(),
})
Entity Framework knows your one-to-many relation. Because you use the virtual ICollection<...> it will automatically create the (Group-)Join for you.
Now you don't want all Merchants, you don't want Merchants without LastOrder. They didn't have any Order with a non-zero Status. From the remaining Merchants you only want the five Merchants with the newest LastOrder.CreatedDate.
So remove all Merchants that have no LastOrders, order by descending LastOrder.CreatedDate and take the top 5.
Continuing the LINQ:
.Where(merchant => merchant.LastOrder != null) // at leas one Order with non-zero Status
.OrderbyDescending(merchant => merchant.LastOrder.CreatedDate)
.Take(5).
You will have "Merchants (Id and Name) with their LastOrder (CreatedData and GrandTotal)", if you want, add an extra Select to convert this into five DashboardActiveMerchantStores
I would rewrite the query like this. You may not even need FirstOrDefault because with GroupBy I believe there is always at least a record.
var result = data
.Where(x => x.Status != 0)
.GroupBy(x => x.MerchantUserId)
.Select(q => new
{
q.Key,
Orders = q.OrderByDescending(q => q.CreatedDate),
})
.Select(x => new DashboardActiveMerchantStore {
MerchantUserId = x.Key,
CreatedDate = x.Orders.FirstOrDefault().CreatedDate,
LastOrderGrandTotal = x.Orders.FirstOrDefault().GrandTotal,
})
.Take(5)
.ToList();
var lastFiveProducts = products.OrderByDescending(p => p.LastOrderGrandTotal).Take(5)

Way to create a tree from two lists with a matching ID

Let`s say I have the following two models :
public class Order
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public string Street { get; set; }
public IList<Appointment> Appointments { get; set; }
}
public class Appointment
{
public Guid Id {get; set;}
public DateTime StartDate {get; set;}
public DateTime EndDate {get; set;}
public Guid OrderId { get; set; }
}
I have a list with Order items (all with the list of Appointments empty) and a list with all appointments and I don`t know how to match elements from the two lists in order to obtain the Order object with all corresponding appointments (based on OrderId).
How can I do that in an efficient manner ? I don`t want to iterate through all orders, and assign the corresponding appointments..
First, make a dictionary of Appointment by OrderId:
var appointmentByOrderId = appointments
.GroupBy(a => a.OrderId)
.ToDictionary(g => g.Key, g => g.ToList());
After that walk through the list of Orders, and set Appointments to the list from the dictionary:
foreach (var order in orders) {
List<Appointment> appointmentList;
if (appointmentByOrderId.TryGetValue(order.Id, out appointmentList) {
order.Appointments = appointmentList;
} else {
order.Appointments = new List<Appointment>();
}
}
The else branch is optional. You can drop it if you do not want to set an empty list to orders with empty appointment lists.
One way would be to "group join" the two lists and project a new collection:
var neworders =
from o in orders
join a in appointments
on o.Id equals a.OrderId
into g
select new Order
{
Id = o.Id,
Name = o.Name,
Location = o.Location,
Street = o.Street,
Appointments = g.ToList()
};
Obviously this creates new Order objects - if that is not desirable another option would be to loop through all of the orders and "attach" the matching appointments - using ToLookup to pre-group them:
var groups = appointments.ToLookup(a => a.OrderId);
foreach(var o in orders)
o.Appointments = groups[o.Id].ToList();
Build a Dictionary<Guid, Order>, and populate it with all of your orders. Then go through the list of appointments and add to the proper order. For example (assuming you have a list called Orders and a list called Appointments):
var OrdersDictionary = Orders.ToDictionary(o => o.Id, o => o);
foreach (var appt in Appointments)
{
OrdersDictionary[appt.OrderId].Appointments.Add(appt);
}
There's probably a LINQ way to do that rollup all in a single statement, but I didn't take the time to puzzle it out.

EF & LINQ: Get object from database cell

I use Entity Framework Code First and I have three tables (for example):
public class Motorbike()
{
public int Id {get; set;}
public string Producent {get; set;}
public Engine Motor {get; set;}
public Tire Tires {get; set;}
}
public class Engine()
{
public int Id {get; set;}
public int Power {get; set;}
public decimal Price {get;set;}
}
public class Tire()
{
public int Id {get; set;}
public int Size {get; set;}
public decimal Price {get; set;}
}
It's just example, in fact it's more complicated.
Entity Frmaework generates table for me, but tables Motorbike has column: Id, Power, Engine_Id (where storing only number - id engine, not whole object) and Tire_Id (where storing only number - id tire, not whole object).
I know how to insert data - just create new Motorbike object, save to his fields data (for Engine and Tire fields I save whole objects not only id) and use .Add() method from my context.
But how to get data for row where motorbike id is (for example) 1?
I've tried something like this:
List<Motorbike> motorbikes= new List<Motorbike>();
var list = _context.Motorbike.Where(p => p.Id == 1);
motorbikes.AddRange(list);
but always I've got null for Engine and Tire fields (fields Id and Producent are fill properly).
Use Include to load related entities like:
var list = _context.Motorbike
.Include(m=> m.Engine)
.Include(m=> m.Tire)
.Where(p => p.Id == 1);
See: Entity Framework - Loading Related Entities
You're looking for the Include() method.
List<Motorbike> motorbikes = _context.Motorbike
.Include(p => p.Engine)
.Include(p => p.Tire)
.Where(p => p.Id == 1)
.ToList();

LINQ to Entities can't compile a store expression

I use Entity Framework 6.0.1 and I have next problem:
I have the next db structure:
public class User
{
int Id { get; set;}
string E-mail {get; set;}
string Name {get; set;}
...
}
class House
{
string Id {get; set;}
string Name { get; set; }
string Street { get; set; }
. . .
IQueryable<User> Users { get; set; }
}
Every House may be linked to many Users. Any User may be linked to many Houses.
I need to create a query in which to get a list of houses to which is attached a particular user
I know only user Id.
I wrote the next statement:
var houses = this.context.Houses
.Where(house => house.Users.Any(i => i.Id == my_searched_user_id));
but I get error: LINQ to Entities does not recognize the method 'System.String ElementAtOrDefault[String](System.Collections.Generic.IEnumerable`1[System.String], Int32)' method, and this method cannot be translated into a store expression.
I change it to
var houses = this.context.Houses
.Where(house => house.Users.**ToList()**.Any(i => i.Id == my_searched_user_id));
but without any luck :(
You added ToList in wrong place. Try next code:
var houses = this.context.Houses
.Where(house => house.Users.Any(i => i.Id == my_searched_user_id))
.ToList();
ToList will cast IQueryable to IEnumerable, so the query will be executed.
You should not define your Users collection as an IQueryable. It should instead be an ICollection:
public virtual ICollection<User> Users {get;set}
I'm surprised that even compiles... but still, that's not really your problem. Your problem is in code you have not shown. You need to show all the code because something else is causing this error, something to do with indexing a string variable.

Using Linq to order multiple lists from multiple tables

At the moment, I have multiple tables in my Database with slightly varying columns to define different "history" elements for an item.
So I have my item table;
int ItemId {get;set}
string Name {get;set}
Location Loc {get;set}
int Quantity {get;set}
I can do a few things to these items like Move, Increase Quantity, Decrease Quantity, Book to a Customer, "Pick" an item, things like that. So I have made multiple "History Tables" as they have different values to save E.g
public class MoveHistory
{
public int MoveHistoryId { get; set; }
public DateTime Date { get; set; }
public Item Item { get; set; }
public virtual Location Location1Id { get; set; }
public virtual Location Location2Id { get; set; }
}
public class PickingHistory
{
public int PickingHistoryId { get; set; }
public DateTime Date { get; set; }
public Item Item { get; set; }
public int WorksOrderCode { get; set; }
}
This is fine apart from where I want to show a complete history for an item displayed in a list;
Item 123 was moved on 23/02/2013 from Location1 to Location2
Item 123 was picked on 24/02/2013 from work order 421
I am using Entity Framework, .NET 4.5, WPF, and querying using Linq but cannot figure a way of taking these lists of history elements, and ordering them out one by one based on their date.
I can think of messy ways, like one single history table with columns used if required. Or even create a third list containing the date and what list it came from, then cycle through that list picking the corresponding contents from the corresponding list. However, I feel there must be a better way!
Any help would be appreciated.
If you implement a GetDescription() method on your history items (even as an extension method), you can do this:
db.PickingHistory.Where(ph => ph.Item.ItemId == 123)
.Select(ph => new { Time = ph.Date, Description = ph.GetDescription() })
.Concat(db.MoveHistory.Where(mh => mh.ItemId == 123)
.Select(mh => new { Time = mh.Date, Description = mh.GetDescription() })
.OrderByDescending(e => e.Time).Select(e => e.Description);
The problem you are facing is that you're trying to use your database model as a display model and obviously are failing. You need to create a new class that represents your history grid and then populate it from your various queries. From your example output the display model may be:
public class HistoryRow{
public DateTime EventDate { get; set; }
public string ItemName { get; set; }
public string Action { get; set; }
public string Detail { get; set; }
}
You then load the data into this display model:
var historyRows = new List<HistoryRow>();
var pickingRows = _db.PickingHistory.Select(ph => new HistoryRow{
EventDate = ph.Date,
ItemName = ph.Item.Name,
Action = "picked",
Detail = "from works order " + ph.WorksOrderCode);
historyRows.AddRange(pickingRows);
var movingRows = _db.MoveHistory.Select(mh => new HistoryRow{
EventDate = mh.Date,
ItemName = ph.Item.Name,
Action = "moved",
Detail = "from location " + mh.Location1Id + " to location " + mh.Location2Id);
historyRows.AddRange(movingRows );
You can repeatedly add the rows from various tables to get a big list of the HistoryRow actions and then order that list and display the values as you wish.
foreach(var historyRow in historyRows)
{
var rowAsString = historyRow.ItemName + " was " + historyRow.Action.....;
Console.WriteLine(rowAsString);
}
If you are implementing this in order to provide some sort of undo/redo history, then I think that you're going about it in the wrong way. Normally, you would have one collection of ICommand objects with associated parameter values, eg. you store the operations that have occurred. You would then be able to filter this collection for each item individually.
If you're not trying to implement some sort of undo/redo history, then I have misunderstood your question and you can ignore this.

Categories