C# Foreach loop doesn't work as expected - asp.net-mvc-3

I have a list of posts, and I have a list of opinions, 4 opinions users can vote for each post.
It's like:
POST1
Vote (opinion A | 10 votes), vote (opinion B | 2 votes), vote (opinion C | 1 votes), vote (opinion D | 0 votes)
POST2
Vote (opinion A | 3 votes), vote (opinion B | 4 votes), vote (opinion C | 5 votes), vote (opinion D | 7 votes)
For each post any user posts in his or her Diary, other users can vote 1 opinion.
In my controller I have this line to get the Diary Posts:
var diaryPosts = (from d in db.DiaryPosts
join e in db.EstadosDeAlma
on d.EstadosDeAlmaID equals e.ID
join u in db.User
on d.UserID equals u.ID
orderby d.ID descending
select new DiaryPostsSet
{
PostID = d.ID,
EstadoDeAlmaID = e.ID,
EstadoDeAlma = e.Title,
Author = u.Nickname,
Thumbnail = u.Thumbnail,
AuthorComment = d.Content,
Time = d.UpdateTime }).Take(6).ToList();
And I try to take for each DiaryPost, its opinions votes, and here is my problem. I have it like this:
List<ImpressionsSet> impressions = new List<ImpressionsSet>();
foreach (var item in diaryPosts)
{
impressions = (from i in db.Impressions
select new ImpressionsSet
{
ImpressionID = i.ID,
ImpressionTitle = i.Impression,
UrlSlug = i.UrlSlug,
DiaryPostID = item.PostID,
ImpressionNum = i.DiaryImpressions.Count(d => d.DiaryPostsID == item.PostID)
}).ToList();
}
But 'impressions' var gets only the last loop. I don't know how to solve that, cuz I had only a few experience with arrays and lists in C#4.0. I don't know if I could use that like impressions[n], I tried and it doesn't work.
I also need a sample code on how I would control the results of this loop in my Razor View.
I have this now:
#foreach (var imp in ViewBag.ImpressionsList)
{
if (item.PostID == imp.DiaryPostID)
{
<td>
#{ string cbName = imp.UrlSlug; }
#{ string impression = imp.ImpressionTitle; }
#{ string value = imp.DiaryPostID + "|" + imp.ImpressionID + "|" + Session["id"].ToString(); }
<a class="voto" href="javascript:;" onclick="PostImpressions('#value')">#impression</a>
</td>
<td>
<a class="num" href="javascript:;" onclick="PostImpressions('#value')">#imp.ImpressionNum</a>
</td>
}
}
But I'm not sure if it would really work, cuz I dont have the results for the 2 posts in my impressions var, it just get the last result.
Could anyone help me?

instead of:
impressions = ...
Do
impressions.AddRange( (from i in db.Impressions
select new ImpressionsSet
{
ImpressionID = i.ID,
ImpressionTitle = i.Impression,
UrlSlug = i.UrlSlug,
DiaryPostID = item.PostID,
ImpressionNum = i.DiaryImpressions.Count(d => d.DiaryPostsID == item.PostID)
}).ToList() );

Related

LINQ Left Outer Join Multiple Tables with Group Count and Row Concatenation

Can someone help with below please? I simplified the table/column names, etc. I search everywhere but the answers I get are incomplete solutions for the results I want to achieve below. New to LINQ so please be kind. :-)
TABLES
Parent (ParentId, ParentName, ParentOccupation)
Child (ChildId, ChildName, OtherField, ParentId)
GrandChild (GrandChildId, GrandChildName, OtherField, ChildId)
Parent
+----------+------------+------------------+
| ParentId | ParentName | ParentOccupation |
+----------+------------+------------------+
| 1 | Mary | Teacher |
| 2 | Anne | Doctor |
| 3 | Michael | Farmer |
| 4 | Elizabeth | Police |
| 5 | Andrew | Fireman |
+----------+------------+------------------+
Child
+---------+-----------+-------------+----------+
| ChildId | ChildName | OtherField | ParentId |
+---------+-----------+-------------+----------+
| 1 | Ashley | [SomeValue] | 1 |
| 2 | Brooke | [SomeValue] | 1 |
| 3 | Ashton | [SomeValue] | 3 |
| 4 | Emma | [SomeValue] | 4 |
+---------+-----------+-------------+----------+
GrandChild
+--------------+----------------+-------------+---------+
| GrandChildId | GrandChildName | OtherField | ChildId |
+--------------+----------------+-------------+---------+
| 1 | Andrew | [SomeValue] | 1 |
| 2 | Isabelle | [SomeValue] | 2 |
| 3 | Lucas | [SomeValue] | 2 |
| 4 | Matthew | [SomeValue] | 4 |
+--------------+----------------+-------------+---------+
EXPECTED RESULTS
+----------+------------+------------------+-----------------------+-------------------------+
| ParentId | ParentName | ParentOccupation | NumberOfGrandChildren | NamesOfGrandChildren |
+----------+------------+------------------+-----------------------+-------------------------+
| 1 | Mary | Teacher | 3 | Andrew, Isabelle, Lucas |
| 2 | Anne | Doctor | 0 | |
| 3 | Michael | Farmer | 0 | |
| 4 | Elizabeth | Police | 1 | Matthew |
| 5 | Andrew | Fireman | 0 | |
+----------+------------+------------------+-----------------------+-------------------------+
WHAT I HAVE DONE SO FAR
LEFT OUTER JOINS - getting all the columns but no aggregates
var result1 = (from p in Parent
join c in Child on p.ParentId equals c.ParentId into pcj
from pc in pcj.DefaultIfEmpty()
join g in GrandChild on pc.ChildId equals g.ChildId into cgj
from cg in cgj.DefaultIfEmpty()
where [some criteria]
select new
{
ParentId = p.ParentId,
ParentName = p.ParentName,
ChildId = pc.ChildId,
ChildName = pc.ChildName,
GrandChildId = cg.GrandChildId,
GrandChildName = cg.GrandChildName
});
COUNTS - contain the aggregate but not all parent columns are there. Also returns 1 in the count, instead of 0.
var result2 = (from p in Parent
join c in Child on p.ParentId equals c.ParentId into pcj
from pc in pcj.DefaultIfEmpty()
join g in GrandChild on pc.ChildId equals g.ChildId into cgj
from cg in cgj.DefaultIfEmpty()
where [some criteria]
group new { p } by new { p.ParentId } into r
select new
{
ParentId = r.Key.Id,
NumberOfGrandChildren = r.Count()
});
CONCATENATE COMMA SEPARATED ROW VALUES (for names of grandchildren) - have not attempted yet until I solve the count above, but open for solutions please.
How can I combine and achieve the results above? Any help is appreciated! Thanks in advance.
Assuming you are using EF, and you have navigation properties set up, then your query would look like this:
var result = context.Parents
.Select(p => new {
p.ParentId,
p.ParentName,
p.ParentOccupation,
NumberOfGrandChildren = p.Children
.SelectMany(c => c.GrandChildren)
.Count(),
NamesOfGrandChildren = string.Join(", ", p.Children
.SelectMany(c => c.GrandChildren)
.Select(g => g.GrandChildName))
}).ToList();
EDIT
New comments posted by the author of the question show that the Linq query involves EF Core. My original answer assumed it was a local query (Linq to Object). In fact, it rather seems to be an interpreted query (Linq to Entities).
See linq to entities vs linq to objects - are they the same? for explanations about the distinction between Linq to object and Linq to entities.
In that case, Robert McKee's answer is more to the point.
For curiosity's sake, Linqpad shows that this query:
Parents
.Select(p => new
{
ParentId = p.Id,
ParentName = p.Name,
ParentOccupation = p.Occupation,
GrandChildrenCount = p.Children
.SelectMany(c => c.GrandChildren)
.Count(),
GranchildrenNames = string.Join(", ", p.Children
.SelectMany(c => c.GrandChildren)
.Select(gc => gc.Name))
});
will be translated to the following SQL query:
SELECT "p"."Id", "p"."Name", "p"."Occupation", (
SELECT COUNT(*)
FROM "Children" AS "c"
INNER JOIN "GrandChildren" AS "g" ON "c"."Id" = "g"."ChildId"
WHERE "p"."Id" = "c"."ParentId"), "t"."Name", "t"."Id", "t"."Id0"
FROM "Parents" AS "p"
LEFT JOIN (
SELECT "g0"."Name", "c0"."Id", "g0"."Id" AS "Id0", "c0"."ParentId"
FROM "Children" AS "c0"
INNER JOIN "GrandChildren" AS "g0" ON "c0"."Id" = "g0"."ChildId"
) AS "t" ON "p"."Id" = "t"."ParentId"
ORDER BY "p"."Id", "t"."Id", "t"."Id0"
(Using Sqlite, and a custom EFCore context containing Entity Classes with navigation properties)
ORIGINAL ANSWER - assuming Linq to object
Here is a way you could construct your query.
var Result = Parents
// Stage 1: for each parent, get its Chidren Ids
.Select(p => new
{
Parent = p,
ChildrenIds = Children
.Where(c => c.ParentId == p.Id)
.Select(c => c.Id)
.ToList()
})
// Stage 2: for each parent, get its Grandchildren, by using the childrenIds list constructed before
.Select(p => new
{
p.Parent,
GrandChildren = Grandchildren
.Where(gc => p.ChildrenIds.Contains(gc.ChildId))
.ToList()
})
// Stage 3: for each parent, count the grandchildren, and get their names
.Select(p => new
{
ParentId = p.Parent.Id,
ParentName = p.Parent.Name,
ParentOccupation = p.Parent.Occupation,
NumberOfGrandChildren = p.GrandChildren.Count(),
GranchildrenNames = string.Join(", ", p.GrandChildren.Select(gc => gc.Name))
});
And here is a full working LinqPad script, with random data generation, so you can try it:
void Main()
{
var rnd = new Random();
var Parents = Enumerable
.Range(0, 10)
.Select(i => new Parent
{
Id = i,
Name = $"Parent-{i}",
Occupation = $"Occupation{i}"
})
.ToList();
var Children = Enumerable
.Range(0,15)
.Select(i => new Child
{
Id = i,
Name = $"Child{i}",
ParentId = rnd.Next(0, 10)
})
.ToList();
var GrandChildren = Enumerable
.Range(0, 25)
.Select(i => new GrandChildren
{
Id = i,
Name = $"GrandChild{i}",
ChildId = rnd.Next(0, 15)
})
.ToList();
var Result = Parents
// Stage 1: for each parent, get its Chidren Ids
.Select(p => new
{
Parent = p,
ChildrenIds = Children
.Where(c => c.ParentId == p.Id)
.Select(c => c.Id)
.ToList()
})
// Stage 2: for each parent, get its Grandchildren, by using the childrenIds list constructed before
.Select(p => new
{
p.Parent,
GrandChildren = GrandChildren
.Where(gc => p.ChildrenIds.Contains(gc.ChildId))
.ToList()
})
// Stage 3: for each parent, count the grandchildren, and get their names
.Select(p => new
{
ParentId = p.Parent.Id,
ParentName = p.Parent.Name,
ParentOccupation = p.Parent.Occupation,
NumberOfGrandChildren = p.GrandChildren.Count(),
GranchildrenNames = string.Join(", ", p.GrandChildren.Select(gc => gc.Name))
})
.Dump();
}
// You can define other methods, fields, classes and namespaces here
public class Parent
{
public int Id { get; set; }
public string Name { get; set; }
public string Occupation { get; set; }
}
public class Child
{
public int Id { get; set; }
public string Name { get; set; }
public int ParentId { get; set; }
}
public class GrandChildren
{
public int Id { get; set; }
public string Name { get; set; }
public int ChildId { get; set; }
}
And here is a set of results:
// Parents
0 Parent-0 Occupation0
1 Parent-1 Occupation1
2 Parent-2 Occupation2
3 Parent-3 Occupation3
4 Parent-4 Occupation4
5 Parent-5 Occupation5
6 Parent-6 Occupation6
7 Parent-7 Occupation7
8 Parent-8 Occupation8
9 Parent-9 Occupation9
// Children
0 Child0 1
1 Child1 5
2 Child2 8
3 Child3 6
4 Child4 9
5 Child5 3
6 Child6 0
7 Child7 4
8 Child8 9
9 Child9 7
10 Child10 8
11 Child11 2
12 Child12 7
13 Child13 7
14 Child14 8
// GrandChildren
0 GrandChild0 7
1 GrandChild1 11
2 GrandChild2 11
3 GrandChild3 14
4 GrandChild4 6
5 GrandChild5 0
6 GrandChild6 11
7 GrandChild7 6
8 GrandChild8 0
9 GrandChild9 12
10 GrandChild10 9
11 GrandChild11 7
12 GrandChild12 0
13 GrandChild13 3
14 GrandChild14 11
15 GrandChild15 9
16 GrandChild16 2
17 GrandChild17 12
18 GrandChild18 12
19 GrandChild19 12
20 GrandChild20 14
21 GrandChild21 12
22 GrandChild22 11
23 GrandChild23 14
24 GrandChild24 12
// Result
0 Parent-0 Occupation0 2 GrandChild4, GrandChild7
1 Parent-1 Occupation1 3 GrandChild5, GrandChild8, GrandChild12
2 Parent-2 Occupation2 5 GrandChild1, GrandChild2, GrandChild6, GrandChild14, GrandChild22
3 Parent-3 Occupation3 0
4 Parent-4 Occupation4 2 GrandChild0, GrandChild11
5 Parent-5 Occupation5 0
6 Parent-6 Occupation6 1 GrandChild13
7 Parent-7 Occupation7 8 GrandChild9, GrandChild10, GrandChild15, GrandChild17, GrandChild18, GrandChild19, GrandChild21, GrandChild24
8 Parent-8 Occupation8 4 GrandChild3, GrandChild16, GrandChild20, GrandChild23
9 Parent-9 Occupation9 0

Linq query averages

I can run this in Linqpad and it works fine but in VS when i run it the result throws errors because of the Avg, Max and Min statements. Can anyone advise how i need to change this to get the desired result?
tickets = from t in dbContext.TblOmTasks
join o in dbContext.TblOms on t.OMID equals o.OMID
join ls in dbContext.LkpStatusBasics on t.OMTaskStatus equals ls.ID
where t.OMID == SiteId
where ls.Status.Contains(status)
group t by new { Y = t.Created.Value.Date.Year, M = t.Created.Value.Date.Month } into grp
orderby grp.Key.M
select new TBS
{
Month = new DateTime(grp.Key.Y, grp.Key.M, 1).ToString("MMM", CultureInfo.InvariantCulture)
,Avg = grp.Average(g => Convert.ToInt32((g.Updated.HasValue ? g.Updated - g.Created : DateTime.Now - g.Created).Value.Days))
,Max = grp.Max(g => Convert.ToInt32((g.Updated.HasValue ? g.Updated - g.Created : DateTime.Now - g.Created).Value.Days))
,Min = grp.Min(g => Convert.ToInt32((g.Updated.HasValue ? g.Updated - g.Created : DateTime.Now - g.Created).Value.Days))
};
Looks like EF Core still can not translate Timestamp.Days. So use appropriate functions.
tickets =
from t in dbContext.TblOmTasks
join o in dbContext.TblOms on t.OMID equals o.OMID
join ls in dbContext.LkpStatusBasics on t.OMTaskStatus equals ls.ID
where t.OMID == SiteId
where ls.Status.Contains(status)
group t by new { Y = t.Created.Value.Date.Year, M = t.Created.Value.Date.Month } into grp
orderby grp.Key.M
select new TBS
{
Month = new DateTime(grp.Key.Y, grp.Key.M, 1).ToString("MMM", CultureInfo.InvariantCulture),
Avg = grp.Average(g => EF.Functions.DateDiffDay(g.Created, g.Updated ?? DateTime.Now),
Max = grp.Max(g => EF.Functions.DateDiffDay(g.Created, g.Updated ?? DateTime.Now),
Min = grp.Min(g => EF.Functions.DateDiffDay(g.Created, g.Updated ?? DateTime.Now)
};

How do I optimize this LINQ query? It runs localhost but it doesn't run on Azure

I have this LINQ query and am getting results I need. However it takes 5-6 seconds to show results on localhost, and I can't even run this on Azure.
I'm new to LINQ, and I'm sure that I'm doing something inefficient.
Could someone direct me to optimize?
var joblist = (from t in db.Tracking
group t by t.JobNumber into j
let id = j.Max(x => x.ScanDate)
select new
{
jn = j.Key,
ti = j.FirstOrDefault(y => y.ScanDate == id).TrackingId,
sd = j.FirstOrDefault(y => y.ScanDate == id).ScanDate,
lc = j.FirstOrDefault(y => y.ScanDate == id).LocationId
}).Where(z => z.lc == lid).Where(z => z.jn != null);
jfilter = (from tr in joblist
join lc in db.Location on tr.lc equals lc.LocationId
join lt in db.LocType on lc.LocationType equals lt.LocationType
select new ScanMod
{
TrackingId = tr.ti,
LocationName = lc.LocationName,
JobNumber = tr.jn,
LocationTypeName = lt.LocationTypeName,
ScanDate = tr.sd,
StoneId = ""
}).OrderByDescending(z => z.ScanDate);
UPDATE:
This query runs on Azure(s1) but it takes 30 seconds. This table has 500,000 rows and I assume that OrderByDescending or FirstOrDefault is killing it...
var joblist = db.Tracking
.GroupBy(j => j.JobNumber)
.Select(g => g.OrderByDescending(j => j.ScanDate).FirstOrDefault());
jfilter = (from tr in joblist
join lc in db.Location on tr.LocationId equals lc.LocationId
join lt in db.LocType on lc.LocationType equals lt.LocationType
where tr.LocationId == lid
select new ScanMod
{
TrackingId = tr.TrackingId,
LocationName = lc.LocationName,
JobNumber = tr.JobNumber,
LocationTypeName = lt.LocationTypeName,
ScanDate = tr.ScanDate,
StoneId = ""
}).OrderByDescending(z => z.ScanDate);

LINQ - Previous Record

All:
Lets say I have the following table:
RevisionID, Project_ID, Count, Changed_Date
1 2 4 01/01/2016: 01:02:01
2 2 7 01/01/2016: 01:03:01
3 2 8 01/01/2016: 01:04:01
4 2 3 01/01/2016: 01:05:01
5 2 15 01/01/2016: 01:06:01
I am ordering the records based on Updated_Date. A user comes into my site and edits record (RevisionID = 3). For various reasons, using LINQ (with entity framework), I need to get the previous record in the table, which would be RevisionID = 2 so I can perform calculations on "Count". If user went to edit record (RevisionID = 4), I would need to select RevisionID = 3.
I currently have the following:
var x = _db.RevisionHistory
.Where(t => t.Project_ID == input.Project_ID)
.OrderBy(t => t.Changed_Date);
This works in finding the records based on the Project_ID, but how then do I select the record before?
I am trying to do the following, but in one LINQ statement, if possible.
var itemList = from t in _db.RevisionHistory
where t.Project_ID == input.Project_ID
orderby t.Changed_Date
select t;
int h = 0;
foreach (var entry in itemList)
{
if (entry.Revision_ID == input.Revision_ID)
{
break;
}
h = entry.Revision_ID;
}
var previousEntry = _db.RevisionHistory.Find(h);
Here is the correct single query equivalent of your code:
var previousEntry = (
from r1 in db.RevisionHistory
where r1.Project_ID == input.Project_ID && r1.Revision_ID == input.Revision_ID
from r2 in db.RevisionHistory
where r2.Project_ID == r1.Project_ID && r2.Changed_Date < r1.Changed_Date
orderby r2.Changed_Date descending
select r2
).FirstOrDefault();
which generates the following SQL query:
SELECT TOP (1)
[Project1].[Revision_ID] AS [Revision_ID],
[Project1].[Project_ID] AS [Project_ID],
[Project1].[Count] AS [Count],
[Project1].[Changed_Date] AS [Changed_Date]
FROM ( SELECT
[Extent2].[Revision_ID] AS [Revision_ID],
[Extent2].[Project_ID] AS [Project_ID],
[Extent2].[Count] AS [Count],
[Extent2].[Changed_Date] AS [Changed_Date]
FROM [dbo].[RevisionHistories] AS [Extent1]
INNER JOIN [dbo].[RevisionHistories] AS [Extent2] ON [Extent2].[Project_ID] = [Extent1].[Project_ID]
WHERE ([Extent1].[Project_ID] = #p__linq__0) AND ([Extent1].[Revision_ID] = #p__linq__1) AND ([Extent2].[Changed_Date] < [Extent1].[Changed_Date])
) AS [Project1]
ORDER BY [Project1].[Changed_Date] DESC
hope I understood what you want.
Try:
var x = _db.RevisionHistory
.FirstOrDefault(t => t.Project_ID == input.Project_ID && t.Revision_ID == input.Revision_ID -1)
Or, based on what you wrote, but edited:
_db.RevisionHistory
.Where(t => t.Project_ID == input.Project_ID)
.OrderBy(t => t.Changed_Date)
.TakeWhile(t => t.Revision_ID != input.Revision_ID)
.Last()

LINQ and 2 datatables

I have 2 datatables in a dataset. One table has a list called CostTypes. Just an Id and Description field.
The other datatable is the master table and has many records and one of the columns is the cost type. There will be cost types that are not reference in this datatable. There is another column in this databale called cost.
What I am trying to do is get a summary by cost type with a total of the cost. But I want ALL cost types listed any values not in the master table will be zero.
CostType table
Id, Description
1,Marketing
2,Sales
3,Production
4,Service
Master table
Id, Cost, CostTypeId
1,10,1
2,120,1
3,40,3
So I would like to see a result in a datable (if possible) so I can bind to datagridview
Marketing 130
Sales 0
Production 40
Service 0
Thanks for the help everyone, this is what I came up from the answers - Can anyone suggest any improvements???
Also how can I convert the result in query1 into a datable???
var query1 =
from rowCT in costTypes.AsEnumerable()
from rowSTD in stdRates.AsEnumerable()
.Where( d => d.Field<int?>( "CostTypeId" ) == rowCT.Field<int?>( "CostTypeId" ) )
.DefaultIfEmpty()
group new { row0 = rowCT, row1 = rowSTD }
by rowCT.Field<string>( "Description" ) into g
select new
{
g.Key,
Cost = g.Sum( x => x.row1 == null ? 0 : x.row1.Field<decimal>( "Cost" ) ),
TotalCost = g.Sum( x => x.row1 == null ? 0 : x.row1.Field<decimal>( "TotalCost" ) ),
TotalHours = g.Sum( x => x.row1 == null ? 0 : x.row1.Field<decimal>( "TotalHours" ) ),
TotalLabourCost = g.Sum( x => x.row1 == null ? 0 : x.row1.Field<decimal>( "TotalLabourCost" ) )
}
;
Maybe something like this:
Test data:
DataTable dt=new DataTable();
dt.Columns.Add("Id",typeof(int));
dt.Columns.Add("Description",typeof(string));
dt.Rows.Add(1,"Marketing");
dt.Rows.Add(2,"Sales");
dt.Rows.Add(3,"Production");
dt.Rows.Add(4,"Service");
DataTable dt2=new DataTable();
dt2.Columns.Add("Id",typeof(int));
dt2.Columns.Add("Cost",typeof(int));
dt2.Columns.Add("CostTypeId",typeof(int));
dt2.Rows.Add(1,10,1);
dt2.Rows.Add(2,120,1);
dt2.Rows.Add(3,40,1);
Linq query
var query=(
from row in dt.AsEnumerable()
from row1 in dt2.AsEnumerable()
.Where (d =>d.Field<int>("Id")==row.Field<int>("Id") )
.DefaultIfEmpty()
group new{row,row1}
by row.Field<string>("Description") into g
select new
{
g.Key,
Cost=g.Sum (x =>x.row1==null?0:x.row1.Field<int>("Cost"))
}
);
Result
Key Cost
Marketing 10
Sales 120
Production 40
Service 0
You can use the Sum extension method to compute the cost. It will return 0 if the collection is empty which is exactly what you want:
var costTypes = new DataTable("CostTypes");
costTypes.Columns.Add("Id", typeof(Int32));
costTypes.Columns.Add("Description", typeof(String));
costTypes.Rows.Add(1, "Marketing");
costTypes.Rows.Add(2, "Sales");
costTypes.Rows.Add(3, "Production");
costTypes.Rows.Add(4, "Service");
var costEntries = new DataTable("CostEntries");
costEntries.Columns.Add("Id", typeof(Int32));
costEntries.Columns.Add("Cost", typeof(Int32));
costEntries.Columns.Add("CostTypeId", typeof(Int32));
costEntries.Rows.Add(1, 10, 1);
costEntries.Rows.Add(2, 120, 1);
costEntries.Rows.Add(3, 40, 3);
var costs = costTypes
.Rows
.Cast<DataRow>()
.Select(
dr => new {
Id = dr.Field<Int32>("Id"),
Description = dr.Field<String>("Description")
}
)
.Select(
ct => new {
ct.Description,
TotalCost = costEntries
.Rows
.Cast<DataRow>()
.Where(ce => ce.Field<Int32>("CostTypeId") == ct.Id)
.Sum(ce => ce.Field<Int32>("Cost"))
}
);
The result is:
Description|TotalCost
-----------+---------
Marketing | 130
Sales | 0
Production | 40
Service | 0
You can create a new DataSet quite simply:
var costsDataTable = new DataTable("Costs");
costsDataTable.Columns.Add("Description", typeof(String));
costsDataTable.Columns.Add("TotalCost", typeof(Int32));
foreach (var cost in costs)
costsDataTable.Rows.Add(cost.Description, cost.TotalCost);
If the linear search performed by the Where in the code above is a concern you can improve the performance by creating a lookup table in advance:
var costEntriesLookup = costEntries
.Rows
.Cast<DataRow>()
.Select(
ce => new {
Cost = ce.Field<Int32>("Cost"),
CostTypeId = ce.Field<Int32>("CostTypeId")
}
)
.ToLookup(ce => ce.CostTypeId, ce => ce.Cost);
var costs = costTypes
.Rows
.Cast<DataRow>()
.Select(
dr => new {
Id = dr.Field<Int32>("Id"),
Description = dr.Field<String>("Description")
}
)
.Select(
ct => new {
ct.Description,
TotalCost = costEntriesLookup.Contains(ct.Id)
? costEntriesLookup[ct.Id].Sum()
: 0
}
);
I came up with a simpler bit of linq than others seemed to use. Thanks to Martin Liversage for the code to create the input data.
var costTypes = new DataTable("CostTypes");
costTypes.Columns.Add("Id", typeof(Int32));
costTypes.Columns.Add("Description", typeof(String));
costTypes.Rows.Add(1, "Marketing");
costTypes.Rows.Add(2, "Sales");
costTypes.Rows.Add(3, "Production");
costTypes.Rows.Add(4, "Service");
var costEntries = new DataTable("CostEntries");
costEntries.Columns.Add("Id", typeof(Int32));
costEntries.Columns.Add("Cost", typeof(Int32));
costEntries.Columns.Add("CostTypeId", typeof(Int32));
costEntries.Rows.Add(1, 10, 1);
costEntries.Rows.Add(2, 120, 1);
costEntries.Rows.Add(3, 40, 3);
var cte = costTypes.Rows.Cast<DataRow>();
var cee = costEntries.Rows.Cast<DataRow>();
var output = cte.Select(
ct => new {
Description = ct["Description"],
Sum = cee.Where(ce=>ce["CostTypeId"].Equals(ct["Id"])).Sum(ce=>(int)ce["Cost"])
}
);
This may lose efficiency on larger tables since each cost type will search the cost entry table whereas using grouping I suspect you only need one pass over the table. Personally I'd prefer the (to my mind) simpler looking code. It will depend on your use case though.

Resources