Group by in linq + select - c#

say I have this data
1 757f27a2-e997-44f8-b2c2-6c0fd6ee2c2f 2 3
2 757f27a2-e997-44f8-b2c2-6c0fd6ee2c2f 3 1
3 757f27a2-e997-44f8-b2c2-6c0fd6ee2c2f 2 2
column 1 // pk
column 2 // userId
column 3 // courseId
column 4 // permissionId
I have this class
class CoursePermissions
{
public string Prefix { get; set; }
public bool OwnerPermission { get; set; } // permissionId 1
public bool AddPermission { get; set; } // permissionId 2
public bool EditPermission { get; set; } // permissionId 3
}
I want to group all the 3 rows by courseId(or Prefix) and then take that information and make a class out Of it
So the end result would be
List<CoursePermissions> permissions = new List<CoursePermissions>();
CoursePermissions a = new CoursePermissions
{
Prefix = "comp101";
OwnerPermission = false,
AddPermission = true,
EditPermission = true
};
CoursePermissions b = new CoursePermissions
{
Prefix = "comp102";
OwnerPermission = true,
AddPermission = false,
EditPermission = false
};
permissions.Add(a);
permissions.Add(b);
So the above is how the object would look if I took all the row data from the db and manually made it the way I wanted it too look. Of course I need to do it somehow as a query.
In my example I have 2 students. They both belong to the same course. Student 1has edit and Add permission for Comp101 but only owner permissions for comp102.
I want to get all the rows back for Comp101 and put it into CoursePermissions. Then I want to get all the rows back for Comp102 and put it into CoursePermissions. Then store all these in a collection and use them.
The only thing I can do is something like this
var list = session.Query<PermissionLevel>().Where(u => u.Student.StudentId == studentId).ToList();
IEnumerable<IGrouping<string, PermissionLevel>> test = list.GroupBy(x => x.Course.Prefix);
foreach (var t in test)
{
CoursePermissions c = new CoursePermissions();
foreach (var permissionLevel in t)
{
if (permissionLevel.PermissionLevelId == 1)
{
c.OwnerPermission = true;
}
}
}
It would nice if I could get rid of the nest for each loop and do it all when the data comes from the query.

Here's an approach that I think is quite functional.
First set up a dictionary of actions that will set the appropriate course permission given a permission level id.
var setPermission = new Dictionary<int, Action<CoursePermissions>>()
{
{ 1, cps => cps.OwnerPermission = true },
{ 2, cps => cps.AddPermission = true },
{ 3, cps => cps.EditPermission = true },
};
Now create a function that will turn the course prefix and a list of permission level ids into a new CoursePermissions object.
Func<string, IEnumerable<int>, CoursePermissions>
buildCoursePermission = (prefix, permissionLevelIds) =>
{
var cps = new CoursePermissions() { Prefix = prefix };
foreach (var permissionLevelId in permissionLevelIds)
{
setPermission[permissionLevelId](cps);
}
return cps;
};
Now all you have left is a simple query that turns your list of permission levels into a list of course permissions.
var coursePermissionsList =
(from pl in list
group pl.PermissionLevelId by pl.Course.Prefix into gcpls
select buildCoursePermission(gcpls.Key, gcpls)).ToList();
How does that work for you?

Related

Unable to manupulate the list

I am having below class:
public class statistics
{
public int Type { get; set; }
public string Title { get; set; }
public bool Flag { get; set; }
}
Above is list of statistics class so it contains records like this:
1st record : Type = 1
Title = "abc,ttt"
flag= true
2nd Records : Type = 1
Title = "xyz"
flag= true
3rd Records : Type = 1
Title = "lmn,ggg"
flag= true
So here I would like to manipulate my statistics variable and so my
statistics variable should contains 5 records like below:
1st record : Type = 1
Title = "abc"
flag= true
2nd record : Type = 1
Title = "ttt"
flag= true
3rd Records : Type =
Title = "xyz"
flag= true
4th Records : Type = 1
Title = "lmn"
flag= true
5th Records : Type = 1
Title = "ggg"
flag= true
So as you can see that I would like to have a separate record if title contains comma-separated records.
For eg:1st record : Type = 1
Title = "abc,ttt"
flag= true
abc and ttt should split into two records as title contains comma-separated records.
This is how I am trying but unable to do it:
statistics = statistics.Select(o => o.Title.Split(',')).
Select(
t => new statistics
{
Type = t. // not working
}
).ToList();
It seems that you're looking for Split (to turn single comma separated Type into several items) and SelectMany (to flatten the collection):
List<statistics> source = .....;
var result = source
.SelectMany(item => item.Title //split title
.Split(',')
.Select(title => new statistics() {
Type = item.Type,
Title = title,
Flag = item.Flag }))
.ToList(); // finally, materialize into list
You need something like this:
var result = statistics.Select(s => s.Title.Split(',')
.Select(x => new statistics {Type = s.Type, Flag = s.Flag, Title = x}))
.SelectMany(s=>s)
.ToList();
With this output:
var result = statistics.SelectMany(s => s.Title.Split(new char[] { ',' }).
Select(t => new statistics() { Title = t, Flag = s.Flag, Type = s.Type }));
A bit nicer with the query syntax, but don't use the same name statistics for both the class and list:
var result = (from s in statistics
from a in s.Title.Split(',')
select new statistics(){ Type = s.Type, Title = a, Flag = s.Flag }).ToList();

Add duplicates together in List

First question :)
I have a List<Materiau> (where Materiau implements IComparable<Materiau>), and I would like to remove all duplicates and add them together
(if two Materiau is the same (using the comparator), merge it to the first and remove the second from the list)
A Materiau contains an ID and a quantity, when I merge two Materiau using += or +, it keeps the same ID, and the quantity is added
I cannot control the input of the list.
I would like something like this:
List<Materiau> materiaux = getList().mergeDuplicates();
Thank you for your time :)
Check out Linq! Specifically the GroupBy method.
I don't know how familiar you are with sql, but Linq lets you query collections similarly to how sql works.
It's a bit in depth to explain of you are totally unfamiliar, but Code Project has a wonderful example
To sum it up:
Imagine we have this
List<Product> prodList = new List<Product>
{
new Product
{
ID = 1,
Quantity = 1
},
new Product
{
ID = 2,
Quantity = 2
},
new Product
{
ID = 3,
Quantity = 7
},
new Product
{
ID = 4,
Quantity = 3
}
};
and we wanted to group all the duplicate products, and sum their quantities.
We can do this:
var groupedProducts = prodList.GroupBy(item => item.ID)
and then select the values out of the grouping, with the aggregates as needed
var results = groupedProducts.Select( i => new Product
{
ID = i.Key, // this is what we Grouped By above
Quantity = i.Sum(prod => prod.Quantity) // we want to sum up all the quantities in this grouping
});
and boom! we have a list of aggregated products
Lets say you have a class
class Foo
{
public int Id { get; set; }
public int Value { get; set; }
}
and a bunch of them inside a list
var foocollection = new List<Foo> {
new Foo { Id = 1, Value = 1, },
new Foo { Id = 2, Value = 1, },
new Foo { Id = 2, Value = 1, },
};
then you can group them and build the aggregate on each group
var foogrouped = foocollection
.GroupBy( f => f.Id )
.Select( g => new Foo { Id = g.Key, Value = g.Aggregate( 0, ( a, f ) => a + f.Value ) } )
.ToList();
List<Materiau> distinctList = getList().Distinct(EqualityComparer<Materiau>.Default).ToList();

Raven returning wrong document in OrderByDescending Statement

I have 50,000 documents in my raven database, but when I I run this query the Id of the latestProfile object is returned as 9999 (the first id in the db is 0, so this is the ten thousandth item).
//find the profile with the highest ID now existing in the collection
var latestProfile = session.Query<SiteProfile>()
.Customize(c => c.WaitForNonStaleResults())
.OrderByDescending(p => p.Id)
.FirstOrDefault();
//lastProfile.Id is 9999 here
//See how many items there are in the collection. This returns 50,000
var count = session.Query<SiteProfile>()
.Customize(c => c.WaitForNonStaleResults()).Count();
My guess is that Raven is paging before my OrderByDescending statement, but
The default page size is 10, and even the max is 1024
All the Parts of this are either IRavenQueryable or IQueryable
It is also not a stale index as I have tested this with WaitForNonStaleResults()
My expected result here is the most recent id I added (50,000) to be the item returned here, but yet it is not.
Why not? This looks like a bug in Raven to me.
EDIT:
Ok, so I now know exactly why, but it still looks like a bug. Here is a list of the items from that same list actualised by a ToArray()
{ Id = 9999 },
{ Id = 9998 },
{ Id = 9997 },
{ Id = 9996 },
{ Id = 9995 },
{ Id = 9994 },
{ Id = 9993 },
{ Id = 9992 },
{ Id = 9991 },
{ Id = 9990 },
{ Id = 999 }, //<-- Whoops! This is text order not int order
{ Id = 9989 },
So even though my Id column is an integer because Raven stores it internally as a string it is ordering by that representation. Clearly Ravens Queryable implementation is resolving the ordering before checking types
I have read that you can define sort order to use integer sorting on defined indexes but really, this should not matter. In a strongly typed language integers should be sorted as integers.
Is there a way to make this Id ordering correct? Do I have actually have to resort to creating a special index on the id column just to get integers ordered correctly?
UPDATE 2:
I am now using an index as follows:
public SiteProfiles_ByProfileId()
{
Map = profiles => from profile in profiles
select new
{
profile.Id
};
Sort(x => x.Id, SortOptions.Int);
}
To try and force it to understand integers. I can see that my index is called via the Raven server console as follows:
Request # 249: GET - 3 ms - Bede.Profiles - 200 - /indexes/SiteProfiles/ByProfileId?&pageSize=1&sort=-__document_id&operationHeadersHash=-1789353429
Query:
Time: 3 ms
Index: SiteProfiles/ByProfileId
Results: 1 returned out of 20,000 total.
but still it comes back with string ordered results. I have seen advice not to use integers as the id, but that would cause massive issues on this project as there are 3rd parties referencing the current ids (in the old service this is designed to replace).
UPDATE 3: I have specific unit test that shows the issue. it appears to work fine for any integer property except for the Id.
[TestMethod]
public void Test_IndexAllowsCorrectIntSortingWhenNotId()
{
using (var store = new EmbeddableDocumentStore() {RunInMemory = true})
{
store.Initialize();
IndexCreation.CreateIndexes(typeof(MyFakeProfiles_ByProfileId).Assembly, store);
using (var session = store.OpenSession())
{
var profiles = new List<MyFakeProfile>()
{
new MyFakeProfile() { Id=80, Age = 80, FirstName = "Grandpa", LastName = "Joe"},
new MyFakeProfile() { Id=9, Age = 9,FirstName = "Jonny", LastName = "Boy"},
new MyFakeProfile() { Id=22, Age = 22, FirstName = "John", LastName = "Smith"}
};
foreach (var myFakeProfile in profiles)
{
session.Store(myFakeProfile, "MyFakeProfiles/" + myFakeProfile.Id);
}
session.SaveChanges();
var oldestPerson = session.Query<MyFakeProfile>().Customize(c => c.WaitForNonStaleResults())
.OrderByDescending(p => p.Age).FirstOrDefault();
var youngestPerson = session.Query<MyFakeProfile>().Customize(c => c.WaitForNonStaleResults())
.OrderBy(p => p.Age).FirstOrDefault();
var highestId = session.Query<MyFakeProfile>("MyFakeProfiles/ByProfileId").Customize(c => c.WaitForNonStaleResults())
.OrderByDescending(p => p.Id).FirstOrDefault();
var lowestId = session.Query<MyFakeProfile>("MyFakeProfiles/ByProfileId").Customize(c => c.WaitForNonStaleResults())
.OrderBy(p => p.Id).FirstOrDefault();
//sanity checks for ordering in Raven
Assert.AreEqual(80,oldestPerson.Age); //succeeds
Assert.AreEqual(9, youngestPerson.Age);//succeeds
Assert.AreEqual(80, highestId.Id);//fails
Assert.AreEqual(9, lowestId.Id);//fails
}
}
}
private void PopulateTestValues(IDocumentSession session)
{
var profiles = new List<MyFakeProfile>()
{
new MyFakeProfile() { Id=80, Age = 80, FirstName = "Grandpa", LastName = "Joe"},
new MyFakeProfile() { Id=9, Age = 9,FirstName = "Jonny", LastName = "Boy"},
new MyFakeProfile() { Id=22, Age = 22, FirstName = "John", LastName = "Smith"}
};
foreach (var myFakeProfile in profiles)
{
session.Store(myFakeProfile, "MyFakeProfiles/" + myFakeProfile.Id);
}
}
}
public class MyFakeProfile
{
public int Id { get; set; }
public int Age { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class MyFakeProfiles_ByProfileId : AbstractIndexCreationTask<MyFakeProfile>
{
// The index name generated by this is going to be SiteProfiles/ByProfileId
public MyFakeProfiles_ByProfileId()
{
Map = profiles => from profile in profiles
select new
{
profile.Id
};
Sort(x => (int)x.Id, SortOptions.Int);
}
}
You need to specify the type of the field on the index, see http://ravendb.net/docs/2.5/client-api/querying/static-indexes/customizing-results-order
Side note, IDs in RavenDB are always strings. You seem to be trying to use integer IDs - don't do that.
You can provide multiple Sort field, as you have only defined it for Id:
public SiteProfiles_ByProfileId()
{
Map = profiles => from profile in profiles
select new
{
profile.Id
};
Sort(x => x.Id, SortOptions.Int);
Sort(x => x.Age, SortOptions.Int);
}
BUT ... I am unsure of the effects of applying a sort on a field that isn't mapped.
You may have to extend the mapping to select both fields, like this:
public SiteProfiles_ByProfileId()
{
Map = profiles => from profile in profiles
select new
{
profile.Id,
profile.Age
};
Sort(x => x.Id, SortOptions.Int);
Sort(x => x.Age, SortOptions.Int);
}

Count of flattened parent child association in LINQ

I'm trying to get a count of parents with no children plus parents children. As I write this I realize it is better explained with code.. So, here it goes:
With these example types:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public string Description { get; set; }
}
And this data:
var customers = new List<Customer>
{
new Customer
{
Id = 2,
Name = "Jane Doe"
},
new Customer
{
Id = 1,
Name = "John Doe",
Orders = new List<Order>
{
new Order { Id = 342, Description = "Ordered a ball" },
new Order { Id = 345, Description = "Ordered a bat" }
}
}
};
// I'm trying to get a count of customer orders added with customers with no orders
// In the above data, I would expect a count of 3 as detailed below
//
// CId Name OId
// ---- -------- ----
// 2 Jane Doe
// 1 John Doe 342
// 1 John Doe 345
int customerAndOrdersCount = {linq call here}; // equals 3
I am trying to get a count of 3 back.
Thank you in advance for your help.
-Jessy Houle
ADDED AFTER:
I was truly impressed with all the great (and quick) answers. For others coming to this question, looking for a few options, here is a Unit Test with a few of the working examples from below.
[TestMethod]
public void TestSolutions()
{
var customers = GetCustomers(); // data from above
var count1 = customers.Select(customer => customer.Orders).Sum(orders => (orders != null) ? orders.Count() : 1);
var count2 = (from c in customers from o in (c.Orders ?? Enumerable.Empty<Order>() ).DefaultIfEmpty() select c).Count();
var count3 = customers.Sum(c => c.Orders == null ? 1 : c.Orders.Count());
var count4 = customers.Sum(c => c.Orders==null ? 1 : Math.Max(1, c.Orders.Count()));
Assert.AreEqual(3, count1);
Assert.AreEqual(3, count2);
Assert.AreEqual(3, count3);
Assert.AreEqual(3, count4);
}
Again, thank you all for your help!
How about
int customerAndOrdersCount = customers.Sum(c => c.Orders==null ? 1 : Math.Max(1, c.Orders.Count()));
If you would initialize that Order property with an empty list instead of a null, you could do:
int count =
(
from c in customers
from o in c.Orders.DefaultIfEmpty()
select c
).Count();
If you decide to keep the uninitialized property around, then instead do:
int count =
(
from c in customers
from o in (c.Orders ?? Enumerable.Empty<Order>() ).DefaultIfEmpty()
select c
).Count();
customers
.Select(customer => customer.Order)
.Sum(orders => (orders != null) ? orders.Count() : 1)
This works if you want to count "no orders" as 1 and count the orders otherwise:
int customerOrders = customers.Sum(c => c.Orders == null ? 1 : c.Orders.Count());
By the way, the question is very exemplary.
You probabbly searching for something like this:
customers.GroupBy(customer=>customer). //group by object iyself
Select(c=> //select
new
{
ID = c.Key.Id,
Name = c.Key.Name,
Count = (c.Key.Orders!=null)? c.Key.Orders.Count():0
}
);
var orderFreeCustomers = customers.Where(c=>c.Orders== null || c.Orders.Any()==false);
var totalOrders = customers.Where (c => c.Orders !=null).
Aggregate (0,(v,e)=>(v+e.Orders.Count) );
Result is the sum of those two values

Looping through a Linq Result Set with Multiple Joins

I have a complex LINQ query like
var results = from obj1 in context.ProcessBases
join obj2 in context.InspectorArticles
on obj1.ID equals obj2.ProcessBaseID
join obj3 in context.InspectorSamples
on obj2.ID equals obj3.InspectorArticleID
where obj1.ID == _processBaseID
select new {obj1, obj2, obj3,};
Now this result set will have only ONE ProcessBases, each ProcessBase will have MULTIPLE InspectorArticles and each InspectorArticle will have MULTIPLE InspectorSamples. So when I loop through the result set, I want to loop through each InspectorArticle, and then loop through each InspectorSample that belongs to that InspectorArticle, something like:
foreach (InspectorArticle _iart in results.First().obj2)
{
...
foreach (InspectorSample _isamp in results.First().obj3)
{
...
}
}
But since I've called a .First() on my result set obviously I get this exception:
foreach statement cannot operate on variables of type x because x does
not contain a public definition for 'GetEnumerator'
So how can I loop through each instance of InspectorArticle, and then loop through the number of InspectorSamples for that article?
Thanks.
Since you are joining the records together, this statement is not correct:
Now this result set will have only ONE ProcessBases, each ProcessBase
will have MULTIPLE InspectorArticles and each InspectorArticle will
have MULTIPLE InspectorSamples.
What you will actually have after you execute your query is an IEnumerable where each object in the IEnumerable contains a reference to a ProcessBase, an InspectorArticle and an InspectorSample. For example, using the code below in LinqPad will yield an IEnumerable with the following contents:
Code:
void Main()
{
var processBases = new List<ProcessBase>();
var inspectorArticles = new List<InspectorArticle>();
var inspectorSamples = new List<InspectorSample>();
processBases.Add(new ProcessBase { ID = 1 });
processBases.Add(new ProcessBase { ID = 2 });
inspectorArticles.Add(new InspectorArticle { ID = 3, ProcessBaseID = 1 });
inspectorArticles.Add(new InspectorArticle { ID = 4, ProcessBaseID = 1 });
inspectorArticles.Add(new InspectorArticle { ID = 5, ProcessBaseID = 2 });
inspectorSamples.Add(new InspectorSample { ID = 6, InspectorArticleID = 3 });
inspectorSamples.Add(new InspectorSample { ID = 7, InspectorArticleID = 3 });
inspectorSamples.Add(new InspectorSample { ID = 8, InspectorArticleID = 3 });
inspectorSamples.Add(new InspectorSample { ID = 9, InspectorArticleID = 4 });
inspectorSamples.Add(new InspectorSample { ID = 10, InspectorArticleID = 5 });
inspectorSamples.Add(new InspectorSample { ID = 11, InspectorArticleID = 5 });
var processBaseID = 1;
var results = from obj1 in processBases
join obj2 in inspectorArticles on obj1.ID equals obj2.ProcessBaseID
join obj3 in inspectorSamples on obj2.ID equals obj3.InspectorArticleID
where obj1.ID == processBaseID
select new { obj1, obj2, obj3 };
Console.WriteLine(results);
}
public class ProcessBase
{
public int ID { get; set; }
}
public class InspectorArticle
{
public int ID { get; set; }
public int ProcessBaseID { get; set; }
}
public class InspectorSample
{
public int ID { get; set; }
public int InspectorArticleID { get; set; }
}
Results:
So, if you want to keep the Linq statement as is and loop through it with multiple foreach statements, you'll need to use something like the code below:
foreach(var article in results.GroupBy(g => g.obj2.ID))
{
Console.WriteLine("Article ID: #{0}", article.Key);
foreach(var sample in results
.Where(s => s.obj3.InspectorArticleID == article.Key)
.Select(s => s.obj3))
{
Console.WriteLine("\tSample ID: #{0}", sample.ID);
}
}
Using this code (and continuing the example from above) should get you the output:
Article ID: #3
Sample ID: #6
Sample ID: #7
Sample ID: #8
Article ID: #4
Sample ID: #9
The disadvantage of this approach is that you're having to enumerate the results list several times. If you know this list will always be small, then that's not such a big deal. If the list will be very large, then you might want to come up with a better way to return the data.
EDIT
To make the code more efficient, you could group by InspectorArticle.ID and then create a Dictionary keyed by the ID, and containing the original InspectorArticle and the associated InspectorSamples.
var articles = results.GroupBy(g => g.obj2.ID)
.ToDictionary(k => k.Key, v => new {
InspectorArticle = v.Select(s => s.obj2).First(),
InspectorSamples = v.Select(s => s.obj3) });
foreach(var article in articles.OrderBy(a => a.Key).Select(kv => kv.Value))
{
Console.WriteLine("Article ID: #{0}", article.InspectorArticle.ID);
foreach(var sample in article.InspectorSamples)
{
Console.WriteLine("\tSample ID: #{0}", sample.ID);
}
}
The code above will yield the same results as my first example, but for longer lists of articles and samples will be more efficient since it will only enumerate the entire list once when building the Dictionary.
Please note that I keyed the Dictionary off of the ID property because I didn't supply an IEqualityComparer to the GroupBy method. If you would rather key by the InspectorArticle object itself, you would need to make sure that two different InspectorArticle instances with the same ID are viewed as equal, which can be done if you create an IEqualityComparer and pass it into the GroupBy method.

Categories