Linq - SelectMany Confusion - c#

From what I understand from the documentation of SelectMany, one could use it to produce a (flattened) sequence of a 1-many relationship.
I have following classes
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string Description { get; set; }
}
I then try to use them using the query expression syntax like so
var customers = new Customer[]
{
new Customer() { Id=1, Name ="A"},
new Customer() { Id=2, Name ="B"},
new Customer() { Id=3, Name ="C"}
};
var orders = new Order[]
{
new Order { Id=1, CustomerId=1, Description="Order 1"},
new Order { Id=2, CustomerId=1, Description="Order 2"},
new Order { Id=3, CustomerId=1, Description="Order 3"},
new Order { Id=4, CustomerId=1, Description="Order 4"},
new Order { Id=5, CustomerId=2, Description="Order 5"},
new Order { Id=6, CustomerId=2, Description="Order 6"},
new Order { Id=7, CustomerId=3, Description="Order 7"},
new Order { Id=8, CustomerId=3, Description="Order 8"},
new Order { Id=9, CustomerId=3, Description="Order 9"}
};
var customerOrders = from c in customers
from o in orders
where o.CustomerId == c.Id
select new
{
CustomerId = c.Id
, OrderDescription = o.Description
};
foreach (var item in customerOrders)
Console.WriteLine(item.CustomerId + ": " + item.OrderDescription);
This gives to what I need.
1: Order 1
1: Order 2
1: Order 3
1: Order 4
2: Order 5
2: Order 6
3: Order 7
3: Order 8
3: Order 9
I assume this translates to using the SelectMany method when not using the query expression syntax?
Either ways, I'm trying to wrap my head around using SelectMany. So even if my above query does not translate to SelectMany, given the two classes and mock data, could someone provide me with a linq query that uses SelectMany?

Here is your query using SelectMany, modeled exactly after your example. Same output!
var customerOrders2 = customers.SelectMany(
c => orders.Where(o => o.CustomerId == c.Id),
(c, o) => new { CustomerId = c.Id, OrderDescription = o.Description });
The first argument maps each customer to a collection of orders (completely analagous to the 'where' clause you already have).
The second argument transforms each matched pair {(c1, o1), (c1, o2) .. (c3, o9)} into a new type, which I've made the same as your example.
So:
arg1 maps each element in the base collection to another collection.
arg2 (optional) transforms each pair into a new type
The resulting collection is flat like you'd expect in your original example.
If you were to omit the second argument, you would end up with a collection of all orders the match up to a customer. It'd be just that, a flat collection of Order objects.
Using it takes a lot of getting used to, I still have trouble wrapping my head around it sometimes. :(

SelectMany() works like Select, but with that extra feature of flattening a collection that is selected. It should be used whenever you want a projection of elements of sub-collections, and don't care about the sub-collection's containing element.
For example, let's say your domain looked like this:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
class Order
{
public int Id { get; set; }
public Customer Customer { get; set; }
public string Description { get; set; }
}
To get the same list you wanted, your Linq would look something like this:
var customerOrders = Customers
.SelectMany(c=>c.Orders)
.Select(o=> new { CustomerId = o.Customer.Id,
OrderDescription = o.Description });
... which will produce the same result without needing the flat collection of Orders. The SelectMany takes each Customer's Orders collection and iterates through that to produce an IEnumerable<Order> from an IEnumerable<Customer>.

Though this is an old question, I thought I would improve the excellent answers a little:
SelectMany returns a list (which may be empty) for each element of the controlling list. Each element in these result lists are enumerated into the expressions' output sequence and so are concatenated into the result. Hence, a' list -> b' list[] -> concatenate -> b' list.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using System.Diagnostics;
namespace Nop.Plugin.Misc.WebServices.Test
{
[TestClass]
public class TestBase
{
[TestMethod]
public void TestMethod1()
{ //See result in TestExplorer - test output
var a = new int[]{7,8};
var b = new int[]
{12,23,343,6464,232,75676,213,1232,544,86,97867,43};
Func<int, int, bool> numberHasDigit =
(number
, digit) =>
( number.ToString().Contains(digit.ToString()) );
Debug.WriteLine("Unfiltered: All elements of 'b' for each element of 'a'");
foreach(var l in a.SelectMany(aa => b))
Debug.WriteLine(l);
Debug.WriteLine(string.Empty);
Debug.WriteLine("Filtered:" +
"All elements of 'b' for each element of 'a' filtered by the 'a' element");
foreach(var l in a.SelectMany(aa => b.Where(bb => numberHasDigit(bb, aa))))
Debug.WriteLine(l);
}
}
}

Here is another option using SelectMany
var customerOrders = customers.SelectMany(
c => orders.Where(o => o.CustomerId == c.Id)
.Select(p => new {CustomerId = c.Id, OrderDescription = p.Description}));
If you use the Entity Framework or LINQ to Sql and you have an association (relationship) between the entities, then you can do so:
var customerOrders = customers.SelectMany(
c => c.orders
.Select(p => new {CustomerId = c.Id, OrderDescription = p.Description}));

Related

Using Linq to Get a List of Objects to a Distinct Grouped List

So I have a list of this object returned in a working API at the moment.
public class ShoppingListItemDto
{
public int ShoppingListItemId { get; set; }
public string Name { get; set; }
public string Category { get; set; }
}
I want to get it into a grouped list object. Something like this:
public class GroupedShoppingListItemDto
{
public string Category { get; set; }
public List<ShoppingListItemDto> ShoppingListItemDto { get; set; }
}
I found this post and put together the below, but I can't seem to get it to pull in the nested objects.
var list = _context.ShoppingListItems
.GroupBy(sli => new { sli.Category })
.Select(sli => new GroupedShoppingListItemDto { }).ToList();
For example, I'd like the linq query to return something like this:
{
"category":"produce",
"items": [
{
"id":1,
"name":"lettuce",
"category":"produce"
},
{
"id":4,
"name":"cucumber",
"category":"produce"
}
],
"category":"meat",
"items": [
{
"id":2,
"name":"chicken",
"category":"meat"
},
{
"id":3,
"name":"steak",
"category":"meat"
}
]
}
You need to ToList on Select query.
var newList = list.GroupBy(x => x.Category)
.Select(x => new GroupedShoppingListItemDto { Category = x.Key, ShoppingListItemDto = x.ToList() });
It's should works
So you have a sequence of ShoppingListItems and every ShoppingListItem has a Category, which is a string. Some ShoppingListItems have the same Category.
You want to make groups of ShoppingListItems that have the same Category. You are right, for this you need to use one of the overloads of Enumerable.GroupBy.
You want to have a specific output format. In that case, use the GroupBy with a parameter ResultSelector:
var result = _context.ShoppingListItems.GroupBy(
// parameter KeySelector: make groups of ShopplingListitems with same Category
shoppingListItem => shoppingListItem.Category,
// parameter ResultSelector: for every category, and all ShoppingListItems
// with this category make one new GroupedShoppingListItemDto
(category, shoppingListItemsWithThisCategory) => new GroupedShoppingListItemDto
{
Category = category,
ShoppingListItems = shoppingListItemsWithThisCategory.ToList(),
})
// execute the query:
.ToList();
Note, that every ShoppingListItem in your GroupedShoppingListItemDto will have a Category of which you already know the value: it equals GroupedShoppingListItemDto.Category. If you have a 1000 ShoppingListItems with category "meat", you will transfer this same value more than a 1000 times, which is quite a waste of processing power.
Therefore, if you will only use the result locally within this procedure, consider using anonymous types:
var result = _context.ShoppingListItems.GroupBy(shoppingListItem => shoppingListItem.Category,
(category, shoppingListItemsWithThisCategory) => new
{
Category = category,
ShoppingListItems = shoppingListItemsWithThisCategory.Select(shoppingListItem => new
{
// Select only the properties that you plan to use:
Id = shoppingListItem.Id,
Name = shoppingListItem.Name,
...
// No need to select, you already know the value
// Category = shoppingListItem.Category
})

How To group by a list of list and merge it into single list without repetition in C#?

I have a class named studentdetails and a property named students which is the list of studentdetails.
public class studentdetails
{
public int SubjectId {get ; set; }
public int studentId { get; set; }
public int ClassId { get; set; }
}
List<studentdetails> students = new List<studentdetails>()
{
new studentdetails() { studentId = 1, SubjectId = 1, ClassId = 1 },
new studentdetails() { studentId = 2, SubjectId = 2, ClassId = 1 },
new studentdetails() { studentId = 3, SubjectId = 1, ClassId = 2 },
new studentdetails() { studentId = 1, SubjectId = 3, ClassId = 2 },
new studentdetails() { studentId = 1, SubjectId = 3, ClassId = 1 }
};
I have to create a list of class which contains list of student(which contains a list of sbject as property ) as a property without repeating class,student and subject if its already exist.
example:
public class Class
{
public int ClassId { get; set; }
public Lits<student> students { get; set; }
}
public class student
{
public int StudentId { get; set; }
public Lits<Subject> subjects { get; set; }
}
public class Subject
{
public int SubjectId { get; set; }
}
For Example:
With refer to above studentdetails
class(1) -student(1) -subject(1)
-subject(3)
-student(2) -subject(2)
class(2) -student(3) -subject(1)
-student(1) -subject(3)
The next approach can be used to solve the problem:
List<Class> classes = students
// This GroupBy creates groups by ClassId:
// (ClassId) -> (List of Students).
.GroupBy(s => s.ClassId)
.Select(c => new Class
{
ClassId = c.Key,
Students = c
// This GroupBy for given Class creates groups by StudentId:
// (StudentId) -> (List of Subjects).
.GroupBy(s => s.StudentId)
.Select(s => new Student
{
StudentId = s.Key,
Subjects = s
// This GroupBy for given Class and Student removes
// duplicate values of SubjectId. If you can guarantee
// that for given Class and Student will not be duplicate
// values of SubjectId then you can remove this GroupBy.
// If you remove this GroupBy then you need to change
// expression inside Select to the following:
// new Subject { SubjectId = t.SubjectId }.
.GroupBy(t => t.SubjectId)
.Select(t => new Subject { SubjectId = t.Key })
.ToList()
}).ToList()
}).ToList();
Here is complete sample that shows this approach.
#Thaks asked in the comment:
If there is class name, student name and aсtivity name along with ids.
Then how can I map accordingly to the instance?
If you need additionally to map properties other then id then you should use the next overload of GroupBy method: GroupBy(keySelector, comparer). Using this method we can use instances of StudentDetails class as keys and specify comparer for them.
At first we should create comparer, a class that implements interface IEqualityComparer. In our sample we can use a single comparer class to perform all three GroupBy operations, because all our GroupBy operations are performed using int Id property. Typically each GroupBy operation uses its own comparer class because most of the time different GroupBy operations are performed using different keys (different data types, different number of groupping properties). Here is how we can implement our comparer:
// For demo I simplified implementation of the Equals and GetHashCode
// methods by excluding null checks. In the documentation of
// IEqualityComparer you can find implementation with null checks.
public class ComparerById<T> : IEqualityComparer<T>
{
private readonly Func<T, int> _keySelector;
public ComparerById(Func<T, int> keySelector) => _keySelector = keySelector;
public bool Equals(T x, T y) => _keySelector(x) == _keySelector(y);
public int GetHashCode(T obj) => _keySelector(obj);
}
And then using this comparer we can perform required GroupBy:
List<Class> classes = students
// Now key of each group has type StudentDetails, therefore later we
// will be able to use properties of StudentDetails such as ClassName.
// Here to compare keys of type StudentDetails we use comparer:
// new ComparerById<StudentDetails>(s => s.ClassId);
// It means that we create groups by ClassId.
.GroupBy(s => s, new ComparerById<StudentDetails>(s => s.ClassId))
.Select(c => new Class
{
ClassId = c.Key.ClassId,
ClassName = c.Key.ClassName,
Students = c
// Here we create groups by StudentId.
.GroupBy(s => s, new ComparerById<StudentDetails>(s => s.StudentId))
.Select(s => new Student
{
StudentId = s.Key.StudentId,
StudentName = s.Key.StudentName,
Subjects = s
// Here we create groups by SubjectId.
.GroupBy(t => t, new ComparerById<StudentDetails>(t => t.SubjectId))
.Select(t => new Subject {SubjectId = t.Key.SubjectId, SubjectName = t.Key.SubjectName})
.ToList()
}).ToList()
}).ToList();
Here is complete sample that shows this approach.

Building a new list of Object with combination of other lists of Objects

I have two lists of Client Object:
public class Client
{
public int ClientID { get; set; }
public string Name { get; set; }
public string DCCode { get; set; }
public string CountryName { get; set; }
}
my list A hold 4 items ClientID , and name fields are populated,
My List B Hold the same 4 items but there is no name and hold the ClientID DCCode, and CountryName
i need to either Update list A DCCode and Countryname with corresponding values in list B
or create List C which it hold 4 items with complete value of list A and B together
Like :
List C L: item 1 : ClientID : 1, Name: XXYY, DCCode :4, CountryName: UK
I can do this using for loop, but i have been trying to use LINQ
i have tried the following codes but i could not find the correct way to get the result i want
Solution 1
Clients1.Where(i => Clients2.Any(a=> i.CLinetID == a.CLinetID))
Solution 2:
Clients1.Concat(Clients1).Concat(Clients2).ToList();
Any help would be welcomed
As you have the ClientID field populated in both lists join them by that property and project a new object populated with all fields:
var result = from c1 in Clients1
join c2 in Clients2 on c1.ClientID equals c2.ClientID
select new Client { ClientID = c1.ClientID, Name = c1.Name, DCCode = c2.DCCode, CountryName = c2.CountryName };
This will create the third list. You can also update the items of Clients1 likewise:
foreach (var c1 in Clients1)
{
var c2 = Clients2.FirstOrDefault(i => i.ClientID == c1.ClientID);
if(c2 != null)
{
c1.DCCode = c2.DCCode;
c1.CountryName = c2.CountryName;
}
}
For updating entities in first list you can create dictionary from second list - that will allow you to quickly find corresponding entity with O(1):
var clientsByID = listB.ToDictionary(c => c.ClientID);
foreach(var clientA in listA)
{
var clientB = clientsByID[clientA.ClientID];
clientA.DCCode = clientB.DCCode;
clientA.CountryName = clientB.CountryName;
}
You can also join two lists on ClientID property and produce new list from results (if enumerable is good for you, then I would go with query syntax instead):
var listC = listA.Join(listB,
a => a.ClientID,
b => b.ClientID,
(a,b) => new Client {
ClientID = a.ClientID,
Name = a.Name,
DCCode = b.DCCode,
CountryName = b.CountryName
}).ToList();

Another Q about Linq grouping

I using Linq (together with EF) in order to access my database. I have object "Job", which contains several properties, some of them are "complex". My goal is to group jobs by these properties, and to get a count for each group.
Here my objects (simplified):
public class Job
{
[Key]
public int Id
{
get;
set;
}
[Required]
public Salary Salary
{
get;
set;
}
[Required]
public ICollection<Category> Categories
{
get;
set;
}
}
"Category" is a complex class, and looks like this:
public class Category
{
[Key]
public int Id
{
get;
set;
}
public Industry Industry //Example: Software
{
get;
set;
}
public Field Field //Example: .NET
{
get;
set;
}
public Position Position //Example: Developer
{
get;
set;
}
}
Industry, Field, Position and Salary classes contains just "int" id and "string" name.
I need to group list of Jobs by Industry, Field, Position and Salary and to get a count of each group. This is how I doing it right now:
var IndustryGroupsQuery = from t in Jobs.SelectMany(p => p.Categories)
group t by new { t.Industry} into g
select new
{
Tag = g.Key.Industry,
Count = g.Count()
};
var FieldsGroupsQuery = from t in Jobs.SelectMany(p => p.Categories)
group t by new { t.Field} into g
select new
{
Tag = g.Key.Field,
Count = g.Count()
};
var PositionsGroupsQuery = from t in Jobs.SelectMany(p => p.Categories)
group t by new { t.Position} into g
select new
{
Tag = g.Key.Position,
Count = g.Count()
};
Jobs.GroupBy(job => job.Salary)
.Select(group => new
{
Tag = group.Key,
Count = group.Count()
}))
This is works fine, but I wondering is it possible to improve somehow its performance.
Q1: I think, that probably one single query will perform better that four. Is it possible to combine these queries into one single query?
Q2: When I asking Linq to group by "Industry", how exactly it able to distinguish between one Industry to another? Is it implicitly comparing records' keys? Is it will be faster if I explicitly tell to linq which property to group by (e.g. "id") ?
Thanks!
Answer in reverse order:
Q2:
When you group by an object instead of a base type, it uses the standard equality comparer (obj x == obj y) which does a simple reference comparison (http://msdn.microsoft.com/en-us/library/bsc2ak47(v=vs.110).aspx). If that suits, it works, otherwise you can implement a custom equality comparer (How to implement IEqualityComparer to return distinct values?)
Q1:
If you wanted sub-groups of the groups, then you can do it in a single query. If you just want the counts for each, then you are doing it exactly the right way.
You can user conditional GROUP BY.
You can define a variable to tell the query which column to use for grouping. You can define an ENUM for GROUP BY columns.
int groupByCol = 1; //Change the value of this field according to the field you want to group by
var GenericGroupsQuery = from t in Jobs
group t by new { GroupCol = ( groupByCol == 1 ? t.Industry:(groupByCol == 2 ? t.Field:(groupByCol == 3 ? t.Position : t.Job)))} into g
select new
{
Tag = g.Key,
Count = g.Count()
};

Why is this linq expression not working?

I'm using LINQ to Entities.
I have a table called Student; it has ID and Name as it's columns. ID is a primary key.
I'd like to be able select the name of the Student and get the amount of Students with the same Name.
So for example I'd have this as my table data.
ID Name
1 Bob
2 Will
3 Bob
After performing the query I'd return a List of Student objects looking like this.
Name Quantity
Bob 2
Will 1
I guess it is kind of similar to how the Tags page of stackoverflow works; It has the name and the quantity.
Anyways, I created a partial class called Student.cs in which I added a Quantity property like this.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace MySite.Models
{
public partial class Student
{
private int _quantity;
public int Quantity
{
get { return _quantity; }
set { _quantity = value; }
}
}
}
I came up with this but I'm getting an error..
public IQueryable<Student> FindStudentsDistinctWithQuantity()
{
/*SELECT Name, COUNT(Name) AS Quantity
FROM Student
GROUP BY Name*/
var students= (from s in db.Students
group s by s.Name into g
select new {Name = g.Key, Quantity = g.Count()});
return students;
}
The error i'm getting says something like Can't convert from type Anonymous to the Student list. Does it have something to do with it not recognizing the quantity field I added in the partial class?
Thanks!
Change your Student type to look like this:
public partial class Student
{
public Int32 Quantity { get; set; }
public String Name { get; set; }
}
And your query to look like this:
var students = from s in db.Students
group s by s.Name into g
select new Student {
Name = g.Key,
Quantity = g.Count() };
Your method returns an IQueryable<Student> but you are currently returning an IQueryable<T> of a projected anonymous type.
You need to refactor your Student type to have a Name property of type String and then project new instances of your Student type from the expression so that the return type of your expression will match the return type of your method.
Your function returns Student
public IQueryable<Student> FindStudentsDistinctWithQuantity(){ ... }
But your Linq query returns a new type that contains a Name and an Int (count)
>>> select new {Name = g.Key, Quantity = g.Count()});
y-try select new Student{Name = g.Key, Quantity = g.Count()}
The method's return value ties the "students" collection to IQueryable<Student> but... the Linq expression is creating an IQueryable<some anonymous type>, and there is no conversion between the two.
You may get a baby step further by modifying your select part to be:
select new Student() {....}
Hopefully this helps,
Tyler
The select new keywords are causing the form of the data to change, which means the LINQ query will not return an IQueryable<Student>, but rather an anonymous type containing the "Name" and "Quantity" properties. If you change it to return a concrete type rather than an anonymous one you will be able to retrieve the data in the form you want.
public class StudentGrouping {
public string Name { get; set; }
public int Quantity { get; set; }
}
public IQueryable<StudentGrouping> FindStudentsDistinctWithQuantity()
{
/*SELECT Name, COUNT(Name) AS Quantity
FROM Student
GROUP BY Name*/
var students= (from s in db.Students
group s by s.Name into g
select new StudentGrouping {
Name = g.Key,
Quantity = g.Count()
}).AsQueryable();
return students;
}
The problem is that you are not returning students - you are trying to return an anonymous type from your function. This is not allowed.
Create a class to represent your result and use new MyClass { ... } instead of new { ... } in your query, and change the method to return IQueryable<MyClass> instead of IQueryable<Student>.
You could for example make a class called StudentNameAndResults.
class StudentNameAndResults
{
public string Name { get; set; }
public int Quantity { get; set; }
}
Alternatively, you could just return the result as a Dictionary or an IEnumarable of IGrouping. For example:
public IDictionary<string, int> FindStudentsDistinctWithQuantity()
{
Database db = new Database();
var students= (from s in db.Students
group s by s.Name into g
select new {Name = g.Key, Quantity = g.Count()});
return students.ToDictionary(s => s.Name, s => s.Quantity);
}
Also, the property you created is using the verbose syntax from pre-C# 3.0 days. Now you can use auto-implemented properties if you don't need any special logic:
public int Quantity { get; set; }
var students= (from s in db.Students
group s by s.Name into g
select new {Name = g.Key, Quantity = g.Count()});
This is an anonymous type, not IQueryable<Student>. Either you need to return System.Object, or you need to return IQueryable<Student> as follows...
return from s in db.Students
group s by s.Name into g
select new Student{Name = g.Key, Quantity = g.Count()};
Where Student defines the properties used in the initializtion.
You are doing a projection in your linq query. If you'd hover the cursor over var students inside vs you'll see its a collection of an anonymous type.
If you want to return an IQueryabley<Student> you need to do:
var students= from s in db.Students
group s by s.Name into g
select s.Key;
There is no way outside methods can know about the anonymous type you have created in your previous example, so you won't be able to return a typed collection.
With the method I suggested you will still be able to do a projection on the return value of your method later on, since IQueryable is composable until the first enumeration:
var students = FindStudentsDistinctWithQuantity();
var namesAndQunatity = from s in students select new {s.Name, s.Quantity};

Categories

Resources