Displaying equivalent to SQL pivot table using Linq against Entity Framework - c#

I've been struggling for a few days to display something like Pivoting a dynamic table on SQL, using Linq. I don't actually want to create a pivoted table, I just want to display the results as if it was a pivoted table.
I have two entities:
Category:
public int Id { get; set; }
public string Name { get; set; }
public string Icon { get; set; }
public ICollection<StaticEvent> StaticEvents { get; set; }
StaticEvent:
public int Id { get; set; }
public int CategoryId {get; set; }
public Category Category { get; set; }
public DateTimeOffset Time {get; set; }
[MaxLength(500)]
public string Comment {get; set;}
I'm trying to generate a table showing the COUNT of STATIC EVENTS per CATEGORY PER YEARMONTH. So the rows would be the YEARMONTH, the columns would be the Categories and the values would be the COUNT.
I got to the point where I can count the STATIC EVENTS per YEARMONTH:
var query = _context.Categories
.SelectMany(c => c.StaticEvents )
.GroupBy(c =>
new {
Year = c.Time.Year,
Month = c.Time.Month
})
.Select( x=> new {YearMonth = x.Key, Count = x.Count()})
.ToList();
foreach (var x in query)
{Console.WriteLine($"Month = {x.YearMonth.Year},{x.YearMonth.Month}, , Count = " + x.Count);}
but I'm lost about what to do from here.

If Categories is unknown values which are stored in database, it is not possible to do that via LINQ without dynamic query creation.
There is query which do the job when you known categories on the compile time. Since you have not specified EF version, I have emulated Count with Sum:
var query = _context.Categories
.SelectMany(c => c.StaticEvents)
.GroupBy(c =>
new {
Year = c.Time.Year,
Month = c.Time.Month
})
.Select(x => new {
YearMonth = x.Key,
Cat1 = x.Sum(z => z.CategoryId == 1 ? 1 : 0),
Cat2 = x.Sum(z => z.CategoryId == 2 ? 1 : 0),
Cat3 = x.Sum(z => z.CategoryId == 3 ? 1 : 0),
})
.ToList();

Related

How can I make EF Core 3 translate the group by foreign key the same way efcore7 does it?

I have the following entities and a database context
public class Item
{
public int Id { get; set; }
public int? ReceiptId { get; set; }
public int ItemTypeId { get; set; }
}
public class ItemType
{
public int Id { get; set; }
public string Name { get; set; }
public List<Item> Items { get; set; }
}
public class Receipt
{
public int Id { get; set; }
public string ReceiptInfo { get; set; }
public List<Item> Items { get; set; }
}
I'm trying to get a the list of receipts, but instead of containing the items they contain, I want them to have the itemType and the amount of items for each. I have written the following linq query, which works:
var result = _databaseContext.Receipts.Select(r => new
{
r.Id,
r.ReceiptInfo,
ItemInfo = r.Items.GroupBy(item => item.ItemTypeId)
.Select(group => new
{
IdItemType = group.Key,
AmountOfItems = group.Count(),
}).ToList()
});
With EF Core 7, it is translated to the following SQL query:
SELECT [r].[Id], [r].[ReceiptInfo], [t].[IdItemType], [t].[AmountOfItems]
FROM [Receipts] AS [r]
OUTER APPLY
(SELECT [i].[ItemTypeId] AS [IdItemType], COUNT(*) AS [AmountOfItems]
FROM [Items] AS [i]
WHERE [r].[Id] = [i].[ReceiptId]
GROUP BY [i].[ItemTypeId]) AS [t]
ORDER BY [r].[Id]
Yet, I need to do this in an older project which doesn't support a version older than 3.1 for EF Core.
There it translates the query differently and I get this error
Column 'Receipts.Id' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause
In case of EF Core 3.1, you have to postprocess loaded detail items on the client side:
var rawData = _databaseContext.Receipts.Select(r => new
{
r.Id,
r.ReceiptInfo,
RawItemInfo = r.Items.Select(item => new
{
IdItemType = item.ItemTypeId
}).ToList()
})
.ToList();
var result = rawData
.Select(r => new
{
r.Id,
r.ReceiptInfo,
ItemInfo = r.RawItemInfo
.GroupBy(item => item.ItemTypeId)
.Select(group => new
{
IdItemType = group.Key,
AmountOfItems = group.Count(),
}).ToList()
});
As you see, GroupBy support has improved drastically in EFC 7. EFC 3 only supports GroupBy with aggregates at the query root.
Therefore, to make it run in EFC 3 you need to force the query into the supported shape. To get the same grouping level, the query starts at Items and groups + aggregates once over three elements instead of two:
var result = _databaseContext.Items
.GroupBy(item => new { item.ReceiptId, item.Receipt.ReceiptInfo, item.ItemTypeId })
.Select(group => new
{
Id = group.Key.ReceiptId,
ReceiptInfo = group.Key.ReceiptInfo,
IdItemType = group.Key.ItemTypeId,
NrOfItems = group.Count(),
})
That returns the same data as the original query and does the reduction of data (aggregate) in the database. To get the same result shape, it needs some post-processing in-memory (i.e. after calling AsEnumerable()):
.AsEnumerable()
.GroupBy(x => new { x.Id, x.Receipt.ReceiptInfo })
.Select(g => new
{
g.Key.Id,
g.Key.ReceiptInfo,
ItemInfo = g.Select(x => new { x.IdItemType, x.NrOfItems })
});
This requires adding a navigation property Item.Receipt.

Query from a many to many relationship table

I have two tables Documents and Group like below. I joined the two tables creating a DocumentsGroup using code first.
Documents Table:
public class Documents
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Group> Groups { get; set;
}
Groups Table:
public class Group
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Documents> Documents { get; set; }
}
Here is the DocumentsGroup table. This is the junction table and it does not have a model rather just showing how its looking.
{
public int DocumentsId { get; set; }
public int GroupsId{ get; set; }
}
I am trying to get all the documents which belong to one group from the junction table. I have the Group ID so am trying to get all the documents which belong to that ID like below:
int groupId = 4;
var documents = _database.Groups.Where(d => d.Id == groupId).Include(i => i.Documents).ToList();
I tried that but am not getting all the documents belonging to that group. Is there anything am doing wrong?
Use the following query:
int groupId = 4;
var query =
from g in _database.Groups
from d in g.Documents
where g.Id == groupId
select d;
var documents = query.ToList();
Or via method chain syntax:
int groupId = 4;
var documents = _database.Groups
.Where(g => g.Id == groupId)
.SelectMany(g => g.Documents)
.ToList();
Try reach the the Documents through the mapping table
int groupId = 4;
var documents = _database.DocumentsGroup
.Where(x => x.GroupId == groupId)
.Include(x => x.Documents)
.Select(x => new Documents
{
Id = x.Documents.Id,
// add all props you need
})
.ToList();
But if you don't have the mapping table just create it or you can try:
int groupId = 4;
var documents = _database.Groups
.Include(x => x.Documents)
.Where(x => x.Id == groupId )
.ToList();
The query you have written will return a result set of groups, not the documents. If "Group" table's "Id" column is unique, you should write this as:
var group = dbContext.Groups.Include(g => g.Documents).FirstOrDefault(g => g.Id == 4); //Given group id is 4
if (group != null) {
var documents = group.Documents.ToList(); // Here you should get the desired Documents, given that the the tables are correctly configured
}

How to calculate sum for specific property without grouping main list data

I'm fetching Invoices from database and I want to return all invoices without grouping them!
I don't want to group them since If there are 100 invoices I want to return all of them 100, considering that I want to get Sum of Amount.
So it is perfectly fine to repeat same Total for multiple invoices if their sum is the same, so basically I want to calculate sum of Amount of each invoice item and group by CompanyId, PackageId, BankId, PayMethod only if it's possible?
-- Read code comments --
var result = await _dbContext.Invoices
.Where(p => p.CheckDate >= startDate && p.CheckDate <= endDate)
.Select(p => new DemoDto()
{
CompanyId = p.CompanyId,
Title = p.Title,
Price = p.Price
Total = p.Sum(p => p.Amount).ToString(), // Can I sum here and group by fields I mentioned above? without grouping all data set because I want to keep all 100 records if I received all 100 from database
})
.ToListAsync();
This query obliviously doesn't work because it says
Invoice does not contain definition for Sum and no accessible method..
DemoDto looks like this:
public class DemoDto
{
public string CompanyId {get;set;}
public string Title {get;set;}
public decimal Price {get;set;}
public decimal Amount {get;set;}
}
Invoice class looks like this:
public class Invoice
{
public string CompanyId { get; set; }
public int PackageId {get; set;}
public int BankId {get;set;}
public int PayMethod {get;set;}
public string Title { get; set; }
public decimal Price { get; set; }
public decimal Amount { get; set; }
}
what I'm missing here?
How can I achieve this?
Thanks guys
Cheers
Fetch all the invoices from the database:
var invoices = await _dbContext.Invoices
.Where(p => p.CheckDate >= startDate && p.CheckDate <= endDate)
.ToListAsync();
Group the in-memory results using Linq-To-Object:
var result = invoices?
.GroupBy(p => new { p.CompanyId, p.PackageId, p.BankId, p.PayMethod })
.SelectMany(x => x.Select(y =>
new DemoDto
{
CompanyId = y.CompanyId,
Title = y.Title,
Price = y.Price,
Total = x.Sum(z => z.Price)
}))
.ToList();
If you want to perform the grouping in the database for some reason, you should execute a raw SQL query or a stored procedure rather than relying on the ORM to generate some magic (and most probably inefficient) query for you.

Create list with data from grouped Linq query plus related data from another table

I have two entities witch are related by DataID.
What I need is, to get a list, or two lists to pass data to WPF form to display.
public class Journal
{
[Key]
public int ID {get; set;}
public int DataID {get; set;}
[ForeignKey("DataID")]
public virtual JournalData JournalData { get; set; }
}
Public class JournalData
{
[Key]
public int DataID {get; set;}
public string Field1 { get; set; }
public string Field2 { get; set; }
}
First of all I created a lists
List<Journal>
List<JournalData>
When I'v tried to get data from Journal grouped by DataID
List<Journal> result = query
.GroupBy(t => t.DataID)
.Select(g => new { DataID = g.Key})
.ToList()
.Select(t => new Journal() { DatatID = t.DataID })
.ToList();
I have only DataID and now I want to add data from JournalData where DataID =t.DataID
Could You please help me? Mabe the is a way to get related data through relationship?
You probably need to include JournalData in your query
you will need to add following namespace
using System.Data.Entity;
Something like this (not tested)
List<Journal> result = query
.Include(t => t.JournalData)
.GroupBy(t => t.DataID)
.Select(g => new { DataID = g.Key, JornalData = g.JournalData})
.ToList()
.Select(t => new Journal() { DatatID = t.DataID, JournalData = t.JournalData })
.ToList();
EDIT
var result = query.Include(t => t.JournalData)
.GroupBy(g => g.DataID)
.Select(g => new { DataID = g.Key, JournalData = g.Select(j => j.JournalData) })
.ToList();
EDIT2
if you want field1, field2 next to DataID something like this
var result = journals.GroupBy(g => g.DataID)
.Select(g => new
{
DataID = g.Key,
Fields = g.Select(j => new { Field1 = j.JournalData.Field1, Field2 = j.JournalData.Field2})
})
.ToList();

How to concatenate two IEnumerable's and project them into a grouping

I have two IEnumerable<StatusGroup> where StatusGroup like
public class StatusGroup
{
public DateTime ExamDate{ get; set; }
public int SuccessCount{ get; set; }
public int FailCount{ get; set; }
}
I want to merge the two IEnumerable, so the result will have just one record for each date and the sum of counters properties should be calculated
for example
List1={Date= 01/01/2016 , Success=10, Fail=0}
List2={Date= 01/01/2016 , Success=0, Fail=3}
the result should be
List2={Date= 01/01/2016 , Success=10, Fail=3}
You can do Concat/Union, and then group by Date. Code will look approximately like this:
var list3 = List1.Concat(List2)
.GroupBy(x => x.Date)
.Select(grouping =>
new StatusGroup
{
Date = grouping.Key,
SuccessCount = grouping.Sum(x => x.SuccessCount),
FailCount = grouping.Sum(x => x.FailCount)
});

Categories

Resources