How to use List.Distinct to populate a custom class? - c#

I have the following two classes (in C#)
public class courseList
{
public string MajorName { get; set; }
public string MajorNameID { get; set; }
public string CourseID { get; set; }
public string CourseName { get; set; }
}
public class CourceNames
{
[DataMember]
public string CourseID { get; set; }
[DataMember]
public string CourseName { get; set; }
}
public class Courses
{
[DataMember]
public string MajorNameID { get; set; }
[DataMember]
public string MajorName { get; set; }
[DataMember]
public List<CourceNames> CourseNames { get; set; }
public Courses()
{
Course = new List<CourceNames>();
}
}
I am reading two tables from MYSQL database using SQLreader to
List<courseList> courseList
class.
MY result record is as follows :
MajorNameID MajorName CourseName CourseID
100000 Physics Thermodynamic PHY101
100000 Physics Quantum PHY200
100000 Physics Relativity PHY300
200000 Chemistry Gases CHM300
200000 Chemistry Oreganic CHM500
200000 Chemistry Inroganic CHM120
300000 Mathematics Pure MAT100
300000 Mathematics Applied MAT300
As u could see, I want to populate Courses class. I am not sure how I could do this using Linq.
I recently learnt the following method but it's not working correctly.
List<Courses> courses = courseList .GroupBy(
d => new { d.MajorNameID , d.MajorName },
d => d.MajorName,
(key, g) => new courses
{
MajorNameID = key.MajorNameID,
MajorName = key.MajorName,
CourseNames = g.Distinct().ToList()
}
).ToList();
I get the following error :
> Severity Code Description Project File Line Suppression State
> Error CS0029 Cannot implicitly convert type
> 'System.Collections.Generic.List<string>' to
> 'System.Collections.Generic.List<CourceNames>'...........
I do not fully understand the above. Could someone help me how I could set it up correctly?
I want to load couses per each major in the list.

Try simplifying your group by to just grouping by key and using Select expression on each item in group.
This final trick is that you have to cast the course names to the correct type. The compiler should give you enough hints, but Linq GroupBy is VERY different to MySQL/TSQL GroupBy statements so they can be tricky to master at first.
List<Courses> courses = courseList.GroupBy(
d => new { d.MajorNameID, d.MajorName }
).Select(g => new Courses
{
MajorNameID = g.Key.MajorNameID,
MajorName = g.Key.MajorName,
CourseNames = g.Distinct().Select(c =>
new CourceNames { CourseID = c.CourseID, CourseName = c.CourseName }).ToList()
}
).ToList();
[UPDATE: I don't normally use element projection syntax, so I had to compile this to check]
Using the same GroupBy overload as your question (element projection), this is the same query, note that the element selector expression now selects the elements, the original post has a key expression in there instead:
List<Courses> courses2 = courseList.GroupBy(
d => new { d.MajorNameID, d.MajorName }, // key selector
c => new CourceNames { CourseID = c.CourseID, CourseName = c.CourseName }, // element selector
(key, g) => new Courses
{
MajorNameID = key.MajorNameID,
MajorName = key.MajorName,
CourseNames = g.Distinct().ToList()
}
).ToList();

The lambda variable g is a group, you cannot directly do a .Distinct on it
List<Courses> courses = courseList .GroupBy(
d => new { d.MajorNameID , d.MajorName },
d => d.MajorName,
(key, g) => new Courses
{
MajorNameID = key.MajorNameID,
MajorName = key.MajorName,
CourseNames = g.Distinct().ToList()
}).ToList();
You need to project the course name from the group g and then do a .Distinct on it. Which will be like:
List<Courses> courses = courseList .GroupBy(
d => new { d.MajorNameID , d.MajorName },
d => d.MajorName,
(key, g) => new Courses
{
MajorNameID = key.MajorNameID,
MajorName = key.MajorName,
CourseNames = g.Select(x => x.CourseName).Distinct().ToList()
}).ToList();

Related

LINQ getting field from another collection

I have 3 tables and I'm trying to get a combined result with a sum of one field of them.
I'm working with C#, .NET, Entity Framework 7 and SQL Server.
I need to get the city's Name of each result, but I store the idCity
Brand table:
public byte IdBrand { get; set; }
public string Name { get; set; } = null!;
Bundles table:
public int IdBundle { get; set; }
public short IdCity{ get; set; }
public short IdBrand { get; set; }
public decimal? Volume { get; set; }
Cities:
public short IdCity { get; set; }
public string Name { get; set; } = null!;
I've tried this linq query and got almost the result I want but the city field is failing and I got stuck...
var volume = context.Bundles
.GroupBy(city => city.IdCity)
.Select(cad => new
{
CITY = context.Cities.Local.ToList().ElementAt(cad.Key)!.Name,
BRAND1 = cad.Where(c => c.IdBrand == 1).Sum(c => c.Volume),
BRAND2 = cad.Where(c => c.IdBrand == 19).Sum(c => c.Volume)
}).ToList();
I get this result that I expect but the CITY is not correct, I think because the cad.Key is not the same than Cities Index
I also tried:
context.Cities.ToList()
.Where(i => context.Bundles.Any(a=> i.IdCity == a.IdCity))
.Select(x=> x.Name)
CITY
BRAND1
BRAND2
LONDON
10.2
12
MOSCOU
11.4
1
PARIS
9.1
0.4
I guess that the cad.Key is not what I need to use to get the ElementAt Cities but how can I get the city .Name from another table in the Select? Or what is the best way to perform this query?
Try the following query, it should have better performance:
var query =
from b in context.Bundles
group b by b.IdCity into g
select new
{
IdCity = g.Key,
BRAND1 = g.Sum(c => c.IdBrand == 1 ? c.Volume : 0),
BRAND2 = g.Sum(c => c.IdBrand == 19 ? c.Volume : 0)
} into agg
join city in context.Cities on agg.IdCity equals city.Id
select new
{
CITY = city.Name,
BRAND1 = agg.BRAND1,
BRAND2 = agg.BRAND2
};

linq group by to generate nested POCO

I have four tables joined to produce data something like below:
Name Grade CardID Date Class Listen Read Write
Jane Doe A 1001 2020-10-01 Period 1 - Spanish 500 500 500
John Doe B+ 1002 2010-10-02 Pereiod 2 - English 1000 1000 1000
Jane Doe A 1001 2020-10-01 Period 3 - Englsih 500 1000 1000
How do I convert the above data into a nested form like below using LINQ group by? This is a .NET CORE WEB API project and uses DTO objects projections from the LINQ query data.
[
{
"cardId": 1001,
"studentName": "Jane Doe",
"grade": "A",
"evaluationDate": "2020-10-01T00:00:00",
"Period 1 - Spanish": {
"Listen": 1000,
"Read": 500,
"Write": 500
},
"Period 3 - English": {
"Listen": 1000,
"Read": 500,
"Write": 1000
}
},
{
"cardId": 1002,
"studentName": "John Doe",
"grade": "B+",
"evaluationDate": "2010-10-01T00:00:00",
"Period 2 - English": {
"Listen": 500,
"Read": 500,
"Write": 1000
}
}
]
Below I have two viewModel classes which I am using to generate the nested POCO data stracture to be returned from the query. If I don't use GroupBy, I can generate a simple unnested POCO but I don't want to repeat the response data as separate object. This is for a .NET core web api project .
I feel like I am close, but the group by in LINQ is throwing me off...
public class PointCardViewModel
{
public int CardId { get; set; }
public string StudentName { get; set; }
public string Grade { get; set; }
public DateTime EvaluationDate { get; set; }
public IEnumerable<LineItemViewModel> LineItems { get; set; }
}
public class LineItemViewModel
{
public string ClassPeriod { get; set; }
public int Listen { get; set; }
public int Read { get; set; }
public int Write { get; set; }
}
((from s in db.Students
join dc in db.DailyCards on s.StudentId equals dc.StudentId
join dcli in db.DailyCardLineItems on dc.CardId equals dcli.CardId
join dcob in db.DailyCardOtherBehaviors on dc.CardId equals dcob.CardId
select new
{
s.StudentName,
s.StudentGrade,
dc.CardId,
dc.CardDate,
dcli.ClassParticipationPoints,
dcli.AssignmentCompletionPoints,
dcli.BonusHomeworkPoints,
dcli.ClassPeriod
})
.GroupBy(x => x.CardId)
.Select(g => new PointCardViewModel()
{
CardId = g.Key,
StudentName = g.Select(c => c.StudentName).First(),
Grade = g.Select(c => c.StudentGrade).First(),
EvaluationDate = x.CardDate,
LineItems = g.Select(y => new LineItemViewModel()
{
//Class
//Read
//Listen
//Write
})
}).toList()
Update:
After understanding multiple group By in lINQ, my .NET Core WEB API is still complaining about bad request and doesn't return the nested JSON. I did update the LineItems prop to be IDictionary type with the decorator. Interestingly, if I comment out the DTO portion of LineItems and set it to null, the response comes back fine. Can you help what the issue is here?
public async Task<List<PointCardViewModel>> GetPointCards()
{
var queryPointCards =
((from s in db.Students
join dc in db.DailyCards on s.StudentId equals dc.StudentId
join dcli in db.DailyCardLineItems on dc.CardId equals dcli.CardId
join dcob in db.DailyCardOtherBehaviors on dc.CardId equals dcob.CardId
select new
{
s.StudentName,
s.StudentGrade,
dc.CardId,
dc.CardDate,
dcli.ClassParticipationPoints,
dcli.AssignmentCompletionPoints,
dcli.BonusHomeworkPoints,
dcli.ClassPeriod,
dcob.PersonalAppearancePoints,
dcob.LunchPoints,
dcob.RecessOtherPoints,
dcob.AmHomeroomPoints,
dcob.PmHomeroomPoints
})
.GroupBy(x => new {
x.CardId,
x.StudentGrade,
x.StudentName,
x.CardDate,
x.PersonalAppearancePoints,
x.LunchPoints,
x.RecessOtherPoints,
x.AmHomeroomPoints,
x.PmHomeroomPoints
})
.Select(x => new PointCardViewModel
{
CardId = x.Key.CardId,
StudentName = x.Key.StudentName,
Grade = x.Key.StudentGrade,
EvaluationDate = x.Key.CardDate,
PersonalAppearancePoints = x.Key.PersonalAppearancePoints,
LunchPoints = x.Key.LunchPoints,
RecessOtherPoints = x.Key.RecessOtherPoints,
AMHomeRoomPoints = x.Key.AmHomeroomPoints,
PMHomeRoomPoints = x.Key.PmHomeroomPoints,
LineItems = null
//x.Select(c => new LineItemViewModel
//{
// ClassPeriod = c.ClassPeriod,
// ClassParticipationPoints = c.ClassParticipationPoints,
// AssignmentCompletionPoints = c.AssignmentCompletionPoints,
// BonusHomeworkPoints = c.BonusHomeworkPoints
//}).ToDictionary(key => key.ClassPeriod, value => (object)value)
}
)
).ToListAsync();
if (db != null)
{
return await queryPointCards;
}
return null;
}
You could achieve this with a slight change in your query and resultant Data structure. For example
Changing your Data Structures as
public class PointCardViewModel
{
public int CardId { get; set; }
public string StudentName { get; set; }
public string Grade { get; set; }
public DateTime EvaluationDate { get; set; }
[JsonExtensionData]
public IDictionary<string, object> LineItems { get; set; } //Change Here
}
public class LineItemViewModel
{
public string ClassPeriod { get; set; }
public int Listen { get; set; }
public int Read { get; set; }
public int Write { get; set; }
}
Note that the LineItems has been converted to a Dictionary and decorated with JsonExtensionDataAttribute.
And now you could Change your Group By Query as
.GroupBy(x=> new {x.Name,x.Grade,x.CardID,x.Date})
.Select(x=> new PointCardViewModel
{
CardId=x.Key.CardID,
StudentName = x.Key.Name,
Grade = x.Key.Grade,
EvaluationDate = x.Key.Date,
LineItems = x.Select(c=> new LineItemViewModel
{
ClassPeriod = c.Class,
Listen = c.Listen,
Read = c.Read,
Write = c.Write
}).ToDictionary(key=>key.ClassPeriod,value=>(object)value)
});
Serializing the resultant data would give the required Json
Demo Code
Change the Group by and Select as below:
var result=((from s in db.Students
join dc in db.DailyCards on s.StudentId equals dc.StudentId
join dcli in db.DailyCardLineItems on dc.CardId equals dcli.CardId
join dcob in db.DailyCardOtherBehaviors on dc.CardId equals dcob.CardId
select new
{
s.StudentName,
s.StudentGrade,
dc.CardId,
dc.CardDate,
dcli.ClassParticipationPoints,
dcli.AssignmentCompletionPoints,
dcli.BonusHomeworkPoints,
dcli.ClassPeriod
})
.GroupBy(x => new { x.StudentName, x.CardId, x.StudentGrade, x.CardDate})
.Select(g => new PointCardViewModel()
{
CardId =g.Key.CardId,
StudentName = g.Key.StudentName,
Grade = g.Key.StudentGrade,
EvaluationDate = g.Key.CardDate,
LineItems = g.Select(y => new LineItemViewModel
{
Class=y.Class,
Read=y.ClassParticipationPoints,
Listen=y.AssignmentCompletionPoints,
Write=y.BonusHomeworkPoints
})
}).toList()

LINQ select all items of all subcollections that contain a string

I'm using jqueryui autocomplete to assist user in an item selection. I'm having trouble selecting the correct items from the objects' subcollections.
Object structure (simplified) is
public class TargetType
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<SubCategory> SubCategories { get; set; }
public TargetType()
{
SubCategories = new HashSet<SubCategory>();
}
}
public class SubCategory
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<SubTargetType> SubTargetTypes { get; set; }
public SubCategory()
{
SubTargetTypes = new HashSet<SubTargetType>();
}
}
Currently I'm doing this with nested foreach loops, but is there a better way?
Current code:
List<SubTargetResponse> result = new List<SubTargetResponse>();
foreach (SubCategory sc in myTargetType.SubCategories)
{
foreach (SubTargetType stt in sc.SubTargetTypes)
{
if (stt.Name.ToLower().Contains(type.ToLower()))
{
result.Add(new SubTargetResponse {
Id = stt.Id,
CategoryId = sc.Id,
Name = stt.Name });
}
}
}
You can do using Linq like this
var result = myTargetType.SubCategories
.SelectMany(sc => sc.SubTargetTypes)
.Where(stt => stt.Name.ToLower().Contains(type.ToLower()))
.Select(stt => new SubTargetResponse {
Id = stt.Id,
CategoryId = sc.Id,
Name = stt.Name });
The above query doesn't work. The following should work, but I'd not recommend that as that'd not be faster or more readable.
var result = myTargetType.SubCategories
.Select(sc => new Tuple<int, IEnumerable<SubTargetType>>
(sc.Id,
sc.SubTargetTypes.Where(stt => stt.Name.ToLower().Contains(type.ToLower()))))
.SelectMany(tpl => tpl.Item2.Select(stt => new SubTargetResponse {
Id = stt.Id,
CategoryId = tpl.Item1,
Name = stt.Name }));
Actually there are 2 different questions.
LINQ select all items of all subcollections that contain a string
Solutions:
(A) LINQ syntax:
var result =
(from sc in myTargetType.SubCategories
from stt in sc.SubTargetTypes.Where(t => t.Name.ToLower().Contains(type.ToLower()))
select new SubTargetResponse
{
Id = stt.Id,
CategoryId = sc.Id,
Name = stt.Name
})
.ToList();
(B) Method syntax:
var result =
myTargetType.SubCategories.SelectMany(
sc => sc.SubTargetTypes.Where(stt => stt.Name.ToLower().Contains(type.ToLower())),
(sc, stt) => new SubTargetResponse
{
Id = stt.Id,
CategoryId = sc.Id,
Name = stt.Name
})
.ToList();
Currently I'm doing this with nested foreach loops, but is there a better way?
Well, it depends of what do you mean by "better". Compare your code with LINQ solutions and answer the question. I personally do not see LINQ being better in this case (except no curly braces and different indentation, but a lot of a hidden garbage), and what to say about the second LINQ version in this answer - if that's "better" than your code, I don't know where are we going.

How can I select from an included entity in LINQ and get a flat list similar to a SQL Join would give?

I have two classes:
public class Topic
{
public Topic()
{
this.SubTopics = new HashSet<SubTopic>();
}
public int TopicId { get; set; }
public string Name { get; set; }
public virtual ICollection<SubTopic> SubTopics { get; set; }
}
public class SubTopic
public int SubTopicId { get; set; }
public int Number { get; set; }
public int TopicId { get; set; }
public string Name { get; set; }
public virtual Topic Topic { get; set; }
}
What I would like to do is to get a Data Transfer Object output from LINQ that will show me. I do want to see the TopicId repeated if there is more than one SubTopic inside that topic:
TopicId Name SubTopicId Name
1 Topic1 1 SubTopic1
1 Topic1 2 SubTopic2
1 Topic1 3 SubTopic3
2 Topic2 4 SubTopic4
I tried to code a Linq statement like this:
var r = context.Topics
.Select ( s => new {
id = s.TopicId,
name = s.Name,
sid = s.SubTopics.Select( st => st.SubTopicId),
sidname = s.SubTopics.Select ( st => st.Name)
}).
ToList();
But this does not really work as it returns sid and sidname as lists.
How will it be possible for me to get a flat output showing what I need?
You need SelectMany to expand a nested collection, along these lines
var r = context.Topics.SelectMany(t => t.SubTopics
.Select(st => new
{
TopicID = t.TopicId,
TopicName = t.Name,
SubTopicID = st.SubTopicId,
SubTopicName = st.Name
}));
try this :
var r = context.Topics
.Select ( s => new {
id = s.TopicId,
name = s.Name,
sid = s.SubTopics.Where(st=>st.TopicId==s.TopicId).Select( st => st.SubTopicId ),
sidname = s.SubTopics..Where(st=>st.TopicId==s.TopicId).Select ( st => st.Name)
}).
ToList();
Hope it will help
#Sweko provided an answer that satisfies the exact output that you requested. However, this can be even simpler if you just return the subtopic intact. It may run a bit quicker as well, since you don't need to create a new object for each element in the result.
Lastly, it looks like you wanted your result set ordered. For completeness, I've added those clauses as well.
var r = context.Topics
.SelectMany( topic => topic.SubTopics )
.OrderBy(sub => sub.TopicId)
.ThenBy(sub => sub.SubTopicId);

IList<T> in multi map/reduce result?

I'm struggling with RavenDB's multi map/reduce concept and recently asked this question regarding how to properly write a multi map/reduce index.
I got the simple index in that question working but when I tried to make it a bit more complicated, I cannot make it work. What I want to do is to have the result of the index to contain a list of string, i.e:
class RootDocument {
public string Id { get; set; }
public string Foo { get; set; }
public string Bar { get; set; }
public IList<string> Items { get; set; }
}
public class ChildDocument {
public string Id { get; set; }
public string RootId { get; set; }
public int Value { get; set; }
}
class RootsByIdIndex: AbstractMultiMapIndexCreationTask<RootsByIdIndex.Result> {
public class Result {
public string Id { get; set; }
public string Foo { get; set; }
public string Bar { get; set; }
public IList<string> Items { get; set; }
public int Value { get; set; }
}
public RootsByIdIndex() {
AddMap<ChildDocument>(
children => from child in children
select new {
Id = child.RootId,
Foo = (string)null,
Bar = (string)null,
Items = default(IList<string>),
Value = child.Value
});
AddMap<RootDocument>(
roots => from root in roots
select new {
Id = root.Id,
Foo = root.Foo,
Bar = root.Bar,
Items = root.Items,
Value = 0
});
Reduce =
results => from result in results
group result by result.Id into g
select new {
Id = g.Key,
Foo = g.Select(x => x.Foo).Where(x => x != null).FirstOrDefault(),
Bar = g.Select(x => x.Bar).Where(x => x != null).FirstOrDefault(),
Items = g.Select(x => x.Items).Where(
x => x != default(IList<string>).FirstOrDefault(),
Value = g.Sum(x => x.Value)
};
}
}
Basically I tried to set the Items property to default(IList) when mapping the ChildDocuments and to value of the RootDocument's Items property. This doesn't work, however. It gives the error message
Error on request Could not understand query:
-- line 2 col 285: invalid Expr
-- line 2 col 324: Can't parse double .0.0
when uploading the index. How do I handle lists in multi map/reduce indexes?
Don't use List in your indexes, instead, use arrays.
AddMap<ChildDocument>(
children => from child in children
select new {
Id = child.RootId,
Foo = (string)null,
Bar = (string)null,
Items = new string[0],
Value = child.Value
});
AddMap<RootDocument>(
roots => from root in roots
select new {
Id = root.Id,
Foo = root.Foo,
Bar = root.Bar,
Items = root.Items,
Value = 0
});
Reduce =
results => from result in results
group result by result.Id into g
select new {
Id = g.Key,
Foo = g.Select(x => x.Foo).Where(x => x != null).FirstOrDefault(),
Bar = g.Select(x => x.Bar).Where(x => x != null).FirstOrDefault(),
Items = g.SelectMany(x=>x.Items),
Value = g.Sum(x => x.Value)
};
David, you need to understand that RavenDB stores its indexes with Lucene.NET. That means, you cannot have any complex .net type inside your index.
In your example, I suggest you use a simple string instead of IList<string>. You can then join your string-items:
AddMap<ChildDocument>(
children => from child in children
select new {
Id = child.RootId,
Foo = (string)null,
Bar = (string)null,
Items = (string)null,
Value = child.Value
});
AddMap<RootDocument>(
roots => from root in roots
select new {
Id = root.Id,
Foo = root.Foo,
Bar = root.Bar,
Items = string.Join(";", root.Items),
Value = 0
});

Categories