LINQ - Can't add column to result without changing multi-table grouping - c#

Suppose I have a "database" defined as:
// Baked goods vendors
var vendor = new[] {
new { ID = 1, Baker = "Pies R Us", StMnemon = "NY", Items = 8, Rating = 9 },
new { ID = 2, Baker = "Mikes Muffins", StMnemon = "CA", Items = 5, Rating = 9 },
new { ID = 3, Baker = "Best Bakers", StMnemon = "FL", Items = 2, Rating = 5 },
new { ID = 4, Baker = "Marys Baked Treats", StMnemon = "NY", Items = 8, Rating = 7 },
new { ID = 5, Baker = "Cool Cakes", StMnemon = "NY", Items = 4, Rating = 9 },
new { ID = 6, Baker = "Pie Heaven", StMnemon = "CA", Items = 12, Rating = 9 },
new { ID = 7, Baker = "Cakes N More", StMnemon = "GA", Items = 6, Rating = 8 },
new { ID = 8, Baker = "Dream Desserts", StMnemon = "FL", Items = 2, Rating = 7 }
};
// Locations
var location = new[] {
new {ID= 1, State = "New York", Mnemonic = "NY"},
new {ID= 2, State = "Massachusetts", Mnemonic = "MA"},
new {ID= 3, State = "Ohio", Mnemonic = "OH"},
new {ID= 4, State = "California", Mnemonic = "CA"},
new {ID= 5, State = "Florida", Mnemonic = "FL"},
new {ID= 6, State = "Texas", Mnemonic = "TX"},
new {ID= 7, State = "Georgia", Mnemonic = "GA" }
};
I want to build a query that would be the equivalent of the SQL query:
SELECT State, Rating, SUM(Items) AS 'Kinds'
FROM vendor, location
WHERE vendor.StMnemon = location.Mnemonic
GROUP BY State, Rating
Two things of interest in this query are:
The GROUP BY involves multiple tables, and
The result contains a summation of a column not appearing in the grouping criteria.
I've seen the solutions in the posts on grouping by multiple tables and summing columns not in the group-by. The problem is that combining both doesn't really duplicate the relational query.
I try to duplicate it in LINQ with the following code:
var query = from v in vendor
join l in location
on v.StMnemon equals l.Mnemonic
orderby v.Rating ascending, l.State
select new { v, l };
var result = from q in query
group q by new {
s = q.l.State,
r = q.v.Rating
/* ==> */ , i = q.v.Items
} into grp
select new
{
State = grp.Key.s,
Rating = grp.Key.r
/* ==> */ , Kinds = grp.Sum(k => grp.Key.i)
};
This results in:
=================================
State Rating Kinds
Florida 5 2
Florida 7 2
New York 7 8
Georgia 8 6
California 9 5
California 9 12
New York 9 8
New York 9 4
=================================
Whereas, the SQL query given above gives this result:
=========================
State Rating Kinds
Florida 5 2
Florida 7 2
New York 7 8
Georgia 8 6
California 9 17
New York 9 12
=========================
The discrepancy is because there seems to be no place to put additional columns, other than in the grouping criteria, which of course changes the grouped result. Commenting out the two lines indicated by the /* ==> */ comment in the code above will give the same grouping as the SQL result, but of course that removes the summation field that I want to include.
How do we group multiple tables in LINQ and include additional criteria without changing the grouped result?

something like this seems to return the same as the SQL query:
var result = from v in vendor
from l in location
where l.Mnemonic == v.StMnemon
group v by new { l.State, v.Rating } into grp
orderby grp.Key.Rating ascending, grp.Key.State
select new {State = grp.Key.State, Rating = grp.Key.Rating, Kinds = grp.Sum(p=>p.Items)};
foreach (var item in result)
Console.WriteLine("{0}\t{1}\t{2}", item.State, item.Rating, item.Kinds);

You can do an aggregation outside of the group:
var query = from v in vendor
join l in location
on v.StMnemon equals l.Mnemonic
orderby v.Rating ascending, l.State
select new { v, l };
var result = from q in query
group q by new {
s = q.l.State,
r = q.v.Rating
} into grp
select new
{
State = grp.Key.s,
Rating = grp.Key.r,
Kinds = grp.Sum(g => g.Items)
};
Grouping is a little tricky to grasp - it returns an IGrouping that has one property - Key. The actual items in that grouping are returned by the GetEnumerator() function that lets you treat the group as a collection of those items, meaning you can do aggregation on the items within that group.

Related

LINQ inner WHERE - inner collection contains string

Suppose I have the following collection:
ONE
- Banana
- Mango
TWO
- Apple
- Mango
THREE
- Orange
- Pear
I want to get only the collection which has Mango in it, such as:
ONE
- Banana
- Mango
TWO
- Apple
- Mango
The following example still returns a collection with 3 items:
List<Order> list = new List<Order> {
new Order { Id = 1, Name = "ONE", Items = new List<Items> { new Items { Id = 1, Nama = "Banana" }, new Items { Id = 2, Nama = "Mango" } }},
new Order { Id = 1, Name = "TWO", Items = new List<Items> { new Items { Id = 1, Nama = "Orange" }, new Items { Id = 2, Nama = "Mango" } }},
new Order { Id = 1, Name = "THREE", Items = new List<Items> { new Items { Id = 1, Nama = "Pear" }, new Items { Id = 2, Nama = "Chery" } }},
};
var result = list.Where(x => x.Items.Any(y => !y.Nama.Equals("Mango"))).ToList();
You are getting all collections where there is at least one item that is not Mango.
Try removing the "!".
var result = list.Where(x => x.Items.Any(y => y.Nama.Equals("Mango"))).ToList();
You're almost there! Try reading out the logic of your code to make sense of what it's is doing.
Your inner Where clause logic is saying 'Check the List and if Any of the entries does not Equal "Mango", then we keep that list'. With this logic, every entry in your list has a List with an entry that does not equal "Mango".
Reverse your logic so it says 'Check the List and if Any of the entries Equals "Mango", then we keep that list'.
var result = list.Where(x => x.Items.Any(y => y.Nama.Equals("Mango"))).ToList();

Select TOP 1 for each FK in list using Entity Framework

I have a large table where I'm trying to select the top 1 row for each FK in a list.
My table is laid out as:
ChangeId | AssetId | Timestamp
1 1 123
2 2 999
3 1 3478
4 3 344
5 2 1092
Where ChangeId is my PK, AssetId is my FK and Timestamp is the value I'm trying to select.
If I try the following:
var results =
from Asset in _context.Asset
join change in _context.Change on Asset.AssetId equals change.AssetId into potentialChange
from actualChange in potentialChange.OrderByDescending(y => y.ChangeId).Take(1)
select
{
AssetId,
Timestamp
}
Where my expected result would be:
[
{
AssetId: 1,
Timestamp: 3478
},
{
AssetId: 2,
Timestamp: 1092
},
{
AssetId: 3,
Timestamp: 344
}
]
This query flags up the The LINQ expression could not be translated and will be evaluated locally. which is not suitable for a production rollout.
Running a foreach loop and selecting each item out 1 by 1 works, not it's not a performant solution.
Is there a suitable way to achieve the above?
Try to group it by AssetId and take max from each group
var results =
from Asset in _context.Asset
join change in _context.Change on Asset.AssetId equals change.AssetId into potentialChange
group potentialChange by potentialCharge.AssetId into g
select
{
g.Key,
g.Max().Timestamp
}
Use Group By as follows:
List<MyTable> data = new List<MyTable>()
{
new MyTable(){ChangeId = 1, AssetId = 1, Timestamp = 123},
new MyTable(){ChangeId = 2, AssetId = 2, Timestamp = 999},
new MyTable(){ChangeId = 3, AssetId = 1, Timestamp = 123},
new MyTable(){ChangeId = 5, AssetId = 3, Timestamp = 123},
new MyTable(){ChangeId = 5, AssetId = 2, Timestamp = 123},
};
var expectedData = data.OrderByDescending(d => d.Timestamp).GroupBy(d => d.AssetId).Select(g => new
{
AssetId = g.Key,
TimeStamp = g.First().Timestamp
}).ToList();
This will give your expected result.
Try using .First() instead of .Take(1)
LINQ How to take one record and skip rest c#

EF for Top 10 most popular locations c# [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
I have a table tbLocations with the following columns:
int id {PRIMARY_KEY, AUTO_INCREMENT}
int userid {Foreign key with table_users}
varchar(200) location
In this table I have the following rows:
1, 1, New York
2, 1, California
3, 1, Seattle
4, 1, New York
5, 2, Seattle
6, 2, Ontario
7, 3, Chicago
8, 4, Las Vegas
9, 5, New York
I want to create a c# linq query that gets me the top 10 locations, in my case I should get
New York 2
Seattle 2
The issue here is that a location can be duplicate for a user i.e. New York is duplicate for userid 1 so I don't want such duplication to affect the final report.
Like in the final report I have New York =2 and not =3
How can I do this in LINQ?
I really have no idea where to start, I tried by grouping by but that didn't work
Start with this query:
select top 10 count(*) cnt, [location] from
(
select count(*) as dupl, userid, [location]
from tbLocations
group by userid, [location]
) as test
group by [location]
order by cnt desc
This gives these results:
cnt location
2 New York
2 Seattle
1 Ontario
1 California
1 Chicago
1 Las Vegas
Here is my solution:
var locations = new List<Location>
{
new Location{ Id = 1, UserId = 1, Name = "New York" },
new Location{ Id = 2, UserId = 1, Name = "California" },
new Location{ Id = 3, UserId = 1, Name = "Seattle" },
new Location{ Id = 4, UserId = 1, Name = "New York" },
new Location{ Id = 5, UserId = 2, Name = "Seattle" },
new Location{ Id = 6, UserId = 2, Name = "Ontario" },
new Location{ Id = 7, UserId = 3, Name = "Chicago" },
new Location{ Id = 8, UserId = 4, Name = "Las Vegas" },
new Location{ Id = 9, UserId = 5, Name = "New York" },
};
var topLocations = locations
.GroupBy(location => new { location.UserId, location.Name })
.Select(group => group.First())
.GroupBy(location => location.Name)
.Select(group => new { group.Key, Count = group.Count() })
.OrderByDescending(location => location.Count)
.Take(2);
foreach (var item in topLocations)
{
Console.WriteLine($"{item.Key} {item.Count}");
}

SelectMany from grouped element

In my code below I would like to get Invoices with their aggregate InvoiceLine totals and also a list of Tracks associated with each Invoice.
var screenset =
from invs in context.Invoices
join lines in context.InvoiceLines on invs.InvoiceId equals lines.InvoiceId
join tracks in context.Tracks on lines.TrackId equals tracks.TrackId
group new { invs, lines, tracks }
by new
{
invs.InvoiceId,
invs.InvoiceDate,
invs.CustomerId,
invs.Customer.LastName,
invs.Customer.FirstName
} into grp
select new
{
InvoiceId = grp.Key.InvoiceId,
InvoiceDate = grp.Key.InvoiceDate,
CustomerId = grp.Key.CustomerId,
CustomerLastName = grp.Key.LastName,
CustomerFirstName = grp.Key.FirstName,
CustomerFullName = grp.Key.LastName + ", " + grp.Key.FirstName,
TotalQty = grp.Sum(l => l.lines.Quantity),
TotalPrice = grp.Sum(l => l.lines.UnitPrice),
Tracks = grp.SelectMany(t => t.tracks)
};
However, in the last line were I did a SelectMany is giving me an error:
Tracks = grp.SelectMany(t => t.tracks)
Error:
The type arguments cannot be inferred from the usage. Try specifying the type arguments explicitly.
Any ideas why?
Thanks in advance.
Object tracks is a single track and not a List. If you need to use SelectMany, use need to select a list in order to :
Projects each element of a sequence to an IEnumerable and flattens
the resulting sequences into one sequence.
So Change it to:
Tracks = grp.Select(t => t.tracks)
The real usage of SelectMany, is when you have a List of Lists and you want to convert the Lists into a single list. Example:
List<List<int>> listOfLists = new List<List<int>>()
{
new List<int>() { 0, 1, 2, 3, 4 },
new List<int>() { 5, 6, 7, 8, 9 },
new List<int>() { 10, 11, 12, 13, 14 }
};
List<int> selectManyResult = listOfLists.SelectMany(l => l).ToList();
foreach (var r in selectManyResult)
Console.WriteLine(r);
Output:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Find foreign key matching multiple row values using linq

I have a table called conversation that lists the users who are in the same conversation. Eg:
id | conversation | user
1 | 1 | Bob
2 | 1 | Jane
3 | 2 | Tim
4 | 2 | Lily
5 | 2 | Rick
And I have list has some users like so..
List<string> usernames = new List<string>{"Bob","Jane"};
I now want to check when a user wants to start a conversation with other users whether they have previously had a conversation with those other users (exclusively).
Eg. Bob wants to create a new conversation with Jane.(I have Bob and Jane in my username List values to compare if those have been in conversation before?)
As we see they two have a conversation already, I want to get the conversation id belong these two guys.
if my list contains following data like so..
List<string> usernames = new List<string>{"Bob","Jane","Tim"};
This time I am expecting there is no conversation those 3 before.
I want to find out there is no conversation i can create one new for them.
I have trying this with Linq but cannot get any correct result so far.
Thank in advance for your help;
You can group by conversation IDs and match the groups with your list of users:
var previousConversations = userConversations
.GroupBy(uc => uc.Conversation)
.Where(g => g.OrderBy(uc => uc.user).Select(uc => uc.user)
.SequenceEqual(usernames.Sort()));
You could use the SequenceEqual method to compare 2 sequences.
var users1 = new List<String> { "Bob", "Jane" };
var users2 = new List<String> { "Bob", "Jane", "Tim" };
var tableData = new List<YourTable>
{
new YourTable {foreignKey = 1, name = "Bob"},
new YourTable {foreignKey = 1, name = "Jane"},
new YourTable {foreignKey = 2, name = "Tim"},
new YourTable {foreignKey = 2, name = "Lily"},
new YourTable {foreignKey = 2, name = "Rick"},
};
var keyFound = (from t in tableData
group t by t.foreignKey into users
where users.Select(u => u.name).SequenceEqual(users1)
select users.Key).FirstOrDefault();
var keyNull = (from t in tableData
group t by t.foreignKey into users
where users.Select(u => u.name).SequenceEqual(users2)
select users.Key).FirstOrDefault();
Edit:
You're using linq as a means to fetch data from the database, not all operations are supported this way. What we can do is extract the selection in memory and then we can use all operations again.
Depending on your situation this might not be a good idea, usually you want to let sql handle all the query power since he is better at that.
But if you are selecting on a not so big table you can easily pull in memory feel free to do it like this :
var rawData = (from t in tableData
group t by t.foreignKey into users
select users).ToList();
var key = (from d in rawData
where d.Select(u => u.name).SequenceEqual(users2)
select d.Key).FirstOrDefault();
If on the other hand the data is too big and you want it executed on sql side i would consider make a stored procedure for this.
You're basically doing a "these tags" query, which is answered over here:
Select items by tag when searching multiple tags
Applied to your problem, it looks like:
int userCount = myUsers.Count;
List<int> conversationIds = conversationsUsers
.Where(cu => myUsers.Contains(cu.UserName))
.GroupBy(cu => cu.ConversationId)
.Where(g => g.Select(cu => cu.Username).Distinct().Count() == userCount)
.Select(g => g.Key)
(exclusively).
Well, ok... Move the user filtering into the group filtering.
int userCount = myUsers.Count;
List<int> conversationIds = conversationsUsers
.GroupBy(cu => cu.ConversationId)
.Where(g => g.Select(cu => cu.Username).Distinct().Count() == userCount)
.Where(g => g.Select(cu => cu.UserName).All(userName => myUsers.Contains(userName)))
.Select(g => g.Key)
Check with the following linq. It will result in conversation id if any conversation is else empty (enumeration yields no result). You can check your conditions accordingly.
var cust = new List<Table>
{
new Table {Conversation = 1, Id = 1, User = "Bob"},
new Table {Conversation = 1, Id = 2, User = "Jane"},
new Table {Conversation = 2, Id = 3, User = "Tim"},
new Table {Conversation = 2, Id = 4, User = "Lily"},
new Table {Conversation = 2, Id = 5, User = "Rick"}
};
var names = new List<string> { "Rick", "Lily", "Tim" };
var res = from x in cust
group x by x.Conversation
into y
where y.Select(z => z.User).Intersect(names).Count() == names.Count
select y.Key;
Thanks everybody for your help and answers. The following code did the trick and gives me the result what I wanted. Thank you Kristof for providing this code and your effort. I have attached order by clause in second query otherwise it wont gives the right result. Hope this helps someone who need similar stuff.
var users1 = new List<String> { "Bob", "Jane" };
var users2 = new List<String> { "Bob", "Jane", "Tim" };
var tableData = new List<YourTable>
{
new YourTable {foreignKey = 1, name = "Bob"},
new YourTable {foreignKey = 1, name = "Jane"},
new YourTable {foreignKey = 2, name = "Tim"},
new YourTable {foreignKey = 2, name = "Lily"},
new YourTable {foreignKey = 2, name = "Rick"},
};
var rawData = (from t in tableData
group t by t.foreignKey into users
select users).ToList();
var key = (from d in rawData
where d.Select(u => u.name).OrderBy(s=> s).SequenceEqual(users2.OrderBy(s=>s))
select d.Key).FirstOrDefault();

Categories