This is my raw data coming from DB:
PrimaryColumn StudentId StudentName CourseName CourseId CourseDuration
1 1 X Cse1 C1 2
2 1 X Cse2 C2 1
3 1 X Cse3 C3 3
4 2 Y Cse1 C1 2
5 2 Y Cse4 C4 5
Classes from C# end:
public class Student
{
public int StudentId {get; set;}
public string StudentName {get; set}
public List<Course> Courses {get; set;}
}
public class Course
{
public int CourseId {get; set;}
public string CourseName {get; set;}
public int CourseDuration {get; set; }
}
My goal is to fetch the data grouped by Students and the courses they would be taking which is done using List<Course> as a property of Student Class.
So, I thought the goal is pretty forward. So, I went ahead and used GroupBy on the raw data coming from DB to C# hoping to get the result but to no avail.
This is the best I've gotten, so far.
masterData.GroupBy(x => new { x.StudentId, x.StudentName }, (key, group) => new { StudentId = key.StudentId, StudentName = key.StudentName, Courses=group.ToList() }).ToList();
Although this doesn't get me what I hope to fetch. With some minor code workaround post this call, I'm able to achieve the what I need. But, it's irking me everytime that I'm unable to group the raw data properly.
It would be really helpful if anyone could show me the right way to further optimize the above code or do it different way altogether.
This is the way I need the result to be in a List<Student>:
Student:
StudentId: 1
StudentName: X
List<Course> Courses: [0] - { CourseId: C1, CourseName = Cse1, CourseDuration = 2}
[1] - { CourseId: C2, CourseName = Cse2, CourseDuration = 1}
[2] - { CourseId: C3, CourseName = Cse3, CourseDuration = 3}
Student:
StudentId: 2
StudentName: Y
List<Course> Courses: [0] - { CourseId: C1, CourseName = Cse1, CourseDuration = 2}
[1] - { CourseId: C4, CourseName = Cse4, CourseDuration = 5}
Cheers
You can do it like this:
Here full example: dotNetFiddle
List<Student> result = data.GroupBy(x => new { x.StudentID, x.StrudentName },
(key, group) => new Student{
StudentId = key.StudentID,
StudentName = key.StrudentName,
Courses = GetCourses(group)}).ToList();
//You can do this operation with Reflection if you want. If you don't want to write manually the property names.
public static List<Course> GetCourses(IEnumerable<RawData> data)
{
List<Course> course = new List<Course>();
foreach(var item in data)
{
Course c = new Course();
c.CourseDuration = item.CourseDuration;
c.CourseId = item.CourseID;
c.CourseName = item.CourseName;
course.Add(c);
}
return course;
}
What I'd do in your case (but I'm not sure you'll see it as 'properly grouping') is something along the lines of
var groupedStudents = masterData.GroupBy(x => x.StudentId);
foreach (var studentGroup in groupedStudents)
{
// new studentclass
var student = new Student(studentGroup.First().StudentId, studentGroup.First().StudentName);
foreach (var studentRecord in studentGroup)
{
student.Courses.Add(new course(studentRecord.CourseId, studentRecord.CourseName, studentRecord.CourseDuration);
}
// add the student-object to where you want to save them i.e.
this.MyStudentList.Add(student);
}
The problem here is that DB is poorly organized and stays far from normalized state. But you could deal with as if it were properly separated to different tables. This way you extracted Courses, Students and then aggregated them - the code below should give a clue
// get all courses
var courses = masterData.GroupBy(x => x.CourseId).Select(group => group.First())
.Select(x => new Course {CourseId = x.CourseId, CourseName = x.CourseName, ...})
.ToDictionary(x => x.CourseId);
// get all students (with empty courses for now)
var students = masterData.GroupBy(x => x.StudentId).Select(group => group.First())
.Select(x => new Student {StudentId = x.StudentId, ...})
.ToDictionary(x => x.StudentId);
// fill students with courses
foreach(var data in masterData)
{
student[data.StudentId].Courses.Add(courses[data.CourseId])
}
I suppose it is a clear way which could be reused after tables normalization. Alternatively you could try to write a complicated LINQ doing all this staff by a single query
Related
var dict = new Dictionary<ClassA,int>();
I should add the Name and Seat number into the dict but I have to take it from separate classes,
eg
dict.Add(student.name)
dict.Add(class.studentlist.score)
I want it to get an output of
Student Score
Amy 78
Amy 89
Amy 45
Ben 34
.
.
.
as well as one that shows the total score if the student's name repeats more than once
Student Score
Amy (total score)
Ben (total score)
I'm not sure how to go about doing this, or if it is even possible? The name and score comes from different classes so I'm a bit confused.
You cannot have a dictionary with multiple Name keys, name should be unigue. So try to create a list
var students = new List<Student>();
students.Add(new Student { Name = "Amy", Score = 78 });
students.Add(new Student { Name = "Ben", Score = 34 });
students.Add(new Student { Name = "Amy", Score = 89 });
students.Add(new Student { Name = "Amy", Score = 45 });
List<string,int> scores = students.GroupBy(s => s.Name)
.Select(i => new Student { Name = i.Key, Score = i.Sum(x => x.Score) }).ToList();
public class Student
{
public string Name { get; set; }
public int Score { get; set; }
}
UPDATE
#Cleptus suggested to use a Dictionary<string, List> where string is a name, and list is to keep the score. It is a very interesting idea, but I like more a list, since it is more like an relational db and linq is ideal for list collections. I feel Dictionaries as too hierarchical , and the always need an extra step or code to get some information.
but dictionary could be used to keep the result information
Dictionary<string, int> result = students.GroupBy(s => s.Name)
.ToDictionary( i => i.Key, i=> i.Sum(x => x.Score));
var amyResult=result["Amy"];
Assuming your class student is:
public class Student
{
public string Name { get; set; }
}
You could use a Dictionary whose key would be a Student and whose content would be a list/array of scores.
List<Student, List<int>> results = new List<Student, List<int>>();
results.Add(new Student() { Name = "Amy"}, new List<int>() { 78, 89, 45 });
results.Add(new Student() { Name = "Ben"}, new List<int>() { 61 });
And to show the data, you just need to iterate the keys and show the data however you need (either aggregated or individually).
using System.Linq;
....
foreach (Student currentStudent in results.Keys) {
List<int> studentResults = results[currentStudent];
// This would show your first needed output (individual scores)
foreach(int result in studentResults) Console.WriteLine(currentStudent.Name + ": " + result.ToString());
// This would show your second needed output (total scores)
Console.WriteLine(currentStudent.Name + ": " + studentResults.Sum().ToString());
}
The second one takes advantage of IEnumerable.Sum()
This is an already asked question, but that question is to work with 2 attributes only, I need to work with 3 attributes, So I am copy-pasting most of the text.
Let's suppose if we have a class like
class Person {
internal int PersonID;
internal string car ;
internal string friend ;
}
Now I have a list of this class: List persons;
Now this list can have instances multiple same PersonIDs, for ex.
persons[0] = new Person { PersonID = 1, car = "Ferrari" , friend = "Josh" };
persons[1] = new Person { PersonID = 1, car = "BMW" , friend = "Olof" };
persons[2] = new Person { PersonID = 2, car = "Audi" , friend = "Gustaf" };
Is there a way I can group by PersonID and get the list of all the cars and friends he has? For example the expected result would be:
class Result {
int PersonID;
List<string> cars;
List<string> friends;
}
From what I have done so far:
IEnumerable resultsForDisplay = ResultFromSQL_Query.GroupBy(
p => p.PersonId.ToString(),
p => p.car,
(key, g) => new { PersonId = key, car = g.ToList()});
But now I'm stuck at getting the friend's array in resultsForDisplay
Sure, you can perform LINQ queries on the group g as well, like:
IEnumerable<Result> resultsForDisplay = from q in ResultFromSQL_Query
group q by q.PersonID into g
select new Result {PersonID = g.Key,cars = g.Select(x => x.car).ToList(), friends = g.Select(x => x.friend).ToList()};
Or with lambda expressions:
IEnumerable<Result> results = persons.GroupBy(x => x.PersonID)
.Select(g => new Result { PersonID = g.Key, cars = g.Select(x => x.car).ToList(), friends = g.Select(x => x.friend).ToList()};
So you can perform any LINQ query on a group (which thus behaves as an IEnumerable<> on the elements of that grou), like a .Select(..), but also .Sum(..), .Average(..) and other sub queries, aggregates, etc.
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();
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);
}
I have an Entity like this:
public class Category
{
public int classid {get;set;}
public int itemid {get;set;}
public string label {get;set;}
}
So a List produces this JSON (three sizes and three colors
[{"classid":1,"itemid":1,"label":"Small"},
{"classid":1,"itemid":2,"label":"Medium"},
{"classid":1,"itemid":3,"label":"Large"},
{"classid":2,"itemid":1,"label":"Blue"},
{"classid":2,"itemid":2,"label":"Green"},
{"classid":2,"itemid":3,"label":"Red"},
{"classid":3,"itemid":1,"label":"Tee"},
{"classid":3,"itemid":2,"label":"Golf"},
{"classid":3,"itemid":3,"label":"Dress"}]
However the JavaScript client needs something like this myarray[][].label:
[[{"itemid":1,"label":"Small"},
{"itemid":2,"label":"Medium"},
{"itemid":3,"label":"Large"}],
[{"itemid":1,"label":"Blue"},
{"itemid":2,"label":"Green"},
{"itemid":3,"label":"Red"}],
[{"itemid":1,"label":"Tee"},
{"itemid":2,"label":"Golf"},
{"itemid":3,"label":"Dress"}]]
And this is smack dab in the middle of my Linq query.
How would I construct the Linq query to assemble the two dimensional array from the one dimensional array within Linq?
EDIT: Existing Query:
...
CATS = (from myP in myProduct.ProductCategories
select new ProductCategory
{
classid = myP.classid,
itemid = myP.itemid,
label = myP.label
}),
...
EDIT: Getting Closer:
CATS = (from myP in myProduct.ProductCategories
group myP by myP.classid into groups
select new resultClass
{ classid = groups.Key,
opts = groups.Select(x =>
new ProductOption
{ itemid = x.itemid,
label = x.label}) }),
I haven't tested this, but it's familiar territory and should work:
IEnumerable<Category> items = ...;
var groups = items.GroupBy(x => x.classid);
var arrays = groups.Select(x =>
x.Select(y => new { itemid = y.itemid, label = y.label }).ToArray()
).ToArray();