I have the following Lists of objects and I use linq join them using a Left Outer Join, after that I need to expand some dynamic rows into columns using linq but that is where I have an issue.
private List<ClasslistDto> GetClassList()
{
return new List<ClasslistDto>
{
new ClasslistDto{StudentID = "0101", StudentName = "Student 1", ClassID = "1", ClassName="Class 2", Sex = "M", ResStatus="Day"},
new ClasslistDto{StudentID = "0102", StudentName = "Student 2", ClassID = "1", ClassName="Class 2", Sex = "M", ResStatus="Day"},
new ClasslistDto{StudentID = "0103", StudentName = "Student 3", ClassID = "1", ClassName="Class 2", Sex = "F", ResStatus="Day"},
new ClasslistDto{StudentID = "0104", StudentName = "Student 4", ClassID = "1", ClassName="Class 2", Sex = "M", ResStatus="Day"},
new ClasslistDto{StudentID = "0105", StudentName = "Student 5", ClassID = "1", ClassName="Class 2", Sex = "F", ResStatus="Day"},
new ClasslistDto{StudentID = "0106", StudentName = "Student 6", ClassID = "1", ClassName="Class 2", Sex = "M", ResStatus="Day"},
new ClasslistDto{StudentID = "0107", StudentName = "Student 7", ClassID = "1", ClassName="Class 2", Sex = "M", ResStatus="Day"},
new ClasslistDto{StudentID = "0108", StudentName = "Student 8", ClassID = "1", ClassName="Class 2", Sex = "F", ResStatus="Day"},
new ClasslistDto{StudentID = "0109", StudentName = "Student 9", ClassID = "1", ClassName="Class 2", Sex = "F", ResStatus="Day"}
};
}
private List<ExamTypeDto> GetExams()
{
return new List<ExamTypeDto>
{
new ExamTypeDto{ExamTypeID="EX01", ExamType = "Test 1", MaxMark = 10},
new ExamTypeDto{ExamTypeID="EX02", ExamType = "Project", MaxMark = 20},
new ExamTypeDto{ExamTypeID="EX03", ExamType = "Test 2", MaxMark = 20},
new ExamTypeDto{ExamTypeID="EX04", ExamType = "MidTerm", MaxMark = 50},
new ExamTypeDto{ExamTypeID="EX05", ExamType = "Exams", MaxMark = 100}
};
}
private List<StudentMarkDto> GetStudentMarks()
{
return new List<StudentMarkDto>
{
new StudentMarkDto{StudentID = "0101", ClassID = "1", ExamTypeID="EX01", Mark=8},
new StudentMarkDto{StudentID = "0101", ClassID = "1", ExamTypeID="EX02", Mark=17},
new StudentMarkDto{StudentID = "0101", ClassID = "1", ExamTypeID="EX03", Mark=12},
new StudentMarkDto{StudentID = "0101", ClassID = "1", ExamTypeID="EX04", Mark=38},
new StudentMarkDto{StudentID = "0101", ClassID = "1", ExamTypeID="EX05", Mark=97},
new StudentMarkDto{StudentID = "0102", ClassID = "1", ExamTypeID="EX01", Mark=9},
new StudentMarkDto{StudentID = "0102", ClassID = "1", ExamTypeID="EX02", Mark=13.7},
new StudentMarkDto{StudentID = "0102", ClassID = "1", ExamTypeID="EX03", Mark=15},
new StudentMarkDto{StudentID = "0102", ClassID = "1", ExamTypeID="EX04", Mark=47},
new StudentMarkDto{StudentID = "0102", ClassID = "1", ExamTypeID="EX05", Mark=91},
new StudentMarkDto{StudentID = "0103", ClassID = "1", ExamTypeID="EX01", Mark=10},
new StudentMarkDto{StudentID = "0103", ClassID = "1", ExamTypeID="EX02", Mark=14},
new StudentMarkDto{StudentID = "0103", ClassID = "1", ExamTypeID="EX03", Mark=15},
new StudentMarkDto{StudentID = "0103", ClassID = "1", ExamTypeID="EX04", Mark=50},
new StudentMarkDto{StudentID = "0103", ClassID = "1", ExamTypeID="EX05", Mark=86},
new StudentMarkDto{StudentID = "0104", ClassID = "1", ExamTypeID="EX01", Mark=5},
new StudentMarkDto{StudentID = "0104", ClassID = "1", ExamTypeID="EX02", Mark=10},
new StudentMarkDto{StudentID = "0104", ClassID = "1", ExamTypeID="EX03", Mark=15},
new StudentMarkDto{StudentID = "0104", ClassID = "1", ExamTypeID="EX04", Mark=25},
new StudentMarkDto{StudentID = "0104", ClassID = "1", ExamTypeID="EX05", Mark=59},
new StudentMarkDto{StudentID = "0105", ClassID = "1", ExamTypeID="EX01", Mark=8},
new StudentMarkDto{StudentID = "0105", ClassID = "1", ExamTypeID="EX02", Mark=13},
new StudentMarkDto{StudentID = "0105", ClassID = "1", ExamTypeID="EX03", Mark=10.5},
new StudentMarkDto{StudentID = "0105", ClassID = "1", ExamTypeID="EX04", Mark=39},
new StudentMarkDto{StudentID = "0105", ClassID = "1", ExamTypeID="EX05", Mark=75},
new StudentMarkDto{StudentID = "0106", ClassID = "1", ExamTypeID="EX01", Mark=8},
new StudentMarkDto{StudentID = "0106", ClassID = "1", ExamTypeID="EX02", Mark=14},
new StudentMarkDto{StudentID = "0106", ClassID = "1", ExamTypeID="EX03", Mark=15},
new StudentMarkDto{StudentID = "0106", ClassID = "1", ExamTypeID="EX04", Mark=40},
new StudentMarkDto{StudentID = "0106", ClassID = "1", ExamTypeID="EX05", Mark=65},
new StudentMarkDto{StudentID = "0107", ClassID = "1", ExamTypeID="EX01", Mark=10},
new StudentMarkDto{StudentID = "0107", ClassID = "1", ExamTypeID="EX02", Mark=14},
new StudentMarkDto{StudentID = "0107", ClassID = "1", ExamTypeID="EX03", Mark=15},
new StudentMarkDto{StudentID = "0107", ClassID = "1", ExamTypeID="EX04", Mark=50},
new StudentMarkDto{StudentID = "0107", ClassID = "1", ExamTypeID="EX05", Mark=86},
new StudentMarkDto{StudentID = "0108", ClassID = "1", ExamTypeID="EX01", Mark=10},
new StudentMarkDto{StudentID = "0108", ClassID = "1", ExamTypeID="EX02", Mark=14},
new StudentMarkDto{StudentID = "0108", ClassID = "1", ExamTypeID="EX03", Mark=15},
new StudentMarkDto{StudentID = "0108", ClassID = "1", ExamTypeID="EX04", Mark=50},
new StudentMarkDto{StudentID = "0108", ClassID = "1", ExamTypeID="EX05", Mark=86},
new StudentMarkDto{StudentID = "0109", ClassID = "1", ExamTypeID="EX01", Mark=10},
new StudentMarkDto{StudentID = "0109", ClassID = "1", ExamTypeID="EX02", Mark=14},
new StudentMarkDto{StudentID = "0109", ClassID = "1", ExamTypeID="EX03", Mark=15},
new StudentMarkDto{StudentID = "0109", ClassID = "1", ExamTypeID="EX04", Mark=50},
new StudentMarkDto{StudentID = "0109", ClassID = "1", ExamTypeID="EX05", Mark=86}
};
}
I use the left outer join
var result = (from s in classlist
join m in studentMarks on s.StudentID equals m.StudentID
into st_def
from m in st_def.DefaultIfEmpty()
join e in exams on m.ExamTypeID equals e.ExamTypeID into ex_def
from e in ex_def.DefaultIfEmpty()
select new ManualEntryAllDto
{
StudentID = s.StudentID,
StudentName = s.StudentName,
ExamTypeID = m.ExamTypeID,
ExamName = e.ExamType,
MaxMark = e.MaxMark,
Marks = m.Mark
});
I however need to flatten it further so I get results like this
StudentID | StudentName | Test 1 | Project | Test 2 | MidTerm | Exams
0101 | Student 1 | 8 | 17 | 12 | 38 | 97
0102 | Student 2 | 9 | 13.7 | 15 | 47 | 91
...
...
In case GetStudentMarks()
return an empty list, we still need it to return
StudentID | StudentName | Test 1 | Project | Test 2 | MidTerm | Exams
0101 | Student 1 | 0 | 0 | 0 | 0 | 0
0102 | Student 2 | 0 | 0 | 0 | 0 | 0
...
...
The Other caveat is GetExams()
may return N number of rows
I found a function here on stackoverflow to flatten a list but I am not able to use it to get the desired results
public static dynamic[] ToPivotArray<T, TColumn, TRow, TData>(
this IEnumerable<T> source,
Func<T, TColumn> columnSelector,
Expression<Func<T, TRow>> rowSelector,
Func<IEnumerable<T>, TData> dataSelector)
{
var arr = new List<object>();
var cols = new List<string>();
String rowName = ((MemberExpression)rowSelector.Body).Member.Name;
var columns = source.Select(columnSelector).Distinct();
cols = (new[] { rowName }).Concat(columns.Select(x => x.ToString())).ToList();
var rows = source.GroupBy(rowSelector.Compile())
.Select(rowGroup => new
{
Key = rowGroup.Key,
Values = columns.GroupJoin(
rowGroup,
c => c,
r => columnSelector(r),
(c, columnGroup) => dataSelector(columnGroup))
}).ToArray();
foreach (var row in rows)
{
var items = row.Values.Cast<object>().ToList();
items.Insert(0, row.Key);
var obj = GetAnonymousObject(cols, items);
arr.Add(obj);
}
return arr.ToArray();
}
private static dynamic GetAnonymousObject(IEnumerable<string> columns, IEnumerable<object> values)
{
IDictionary<string, object> eo = new ExpandoObject() as IDictionary<string, object>;
int i;
for (i = 0; i < columns.Count(); i++)
{
eo.Add(columns.ElementAt<string>(i), values.ElementAt<object>(i));
}
return eo;
}
Any pointers will be great
Here is a variation of an extension method I have for pivoting IEnumerable<T>
that returns a List<dynamic>
instead of the original DataTable
:
public static class IEnumerableExt {
// pivot an IEnumerable<T> to a new DataTable
// over field creates new column names
// value field is value for new columns
// By fields are row ids and grouping key
public static dynamic PivotOverWithBysToDynamic<TRow>(this IEnumerable<TRow> rows, string OverColName, string WithValueName, params string[] ByRowNames) {
var res = new List<dynamic>();
if (rows.Count() > 0) {
var rowg = rows.GroupBy(row => ByRowNames.Select(f => row.GetValue(f)).ToList(),
row => new { Over = row.GetValue(OverColName).ToString(), With = row.GetValue(WithValueName) },
Make.IESequenceEqual<object>());
foreach (var rg in rowg) {
var rgKey = rg.Key.ToList();
var newr = new ExpandoObject();
var newrMap = (IDictionary<string, object>)newr;
for (int j1 = 0; j1 < ByRowNames.Length; ++j1)
newrMap[ByRowNames[j1]] = rgKey[j1];
foreach (var r in rg)
newrMap[r.Over] = r.With;
res.Add(newr);
}
}
return res;
}
}
This code depends on a lot of other extension methods in order to make getting the values out of the original object properties somewhat efficient (by caching functions that return the value for a property to minimize reflection overhead) and to make comparing sequences easier:
public static class Make {
public static IEqualityComparer<IEnumerable<T>> IESequenceEqual<T>() => new IEnumerableSequenceEqualityComparer<T>();
private class IEnumerableSequenceEqualityComparer<T> : IEqualityComparer<IEnumerable<T>> {
public bool Equals(IEnumerable<T> x, IEnumerable<T> y) =>
Object.ReferenceEquals(x, y) || (x != null && y != null && (x.SequenceEqual(y)));
public int GetHashCode(IEnumerable<T> items) {
var hc = new HashCode();
foreach (var item in items)
hc.Add(item);
return hc.ToHashCode();
}
}
}
//***
// Enhanced Dictionary that auto-creates missing entries with seed lambda based on key
// ala auto-vivification in Perl
//***
public class KeySeedFnDictionary<TKey, TValue> : Dictionary<TKey, TValue> {
Func<TKey, TValue> seedFn;
public KeySeedFnDictionary(Func<TKey, TValue> pSeedFn) : base() {
seedFn = pSeedFn;
}
public KeySeedFnDictionary(Func<TKey, TValue> pSeedFn, IDictionary<TKey, TValue> d) : base() {
seedFn = pSeedFn;
foreach (var kvp in d)
Add(kvp.Key, kvp.Value);
}
public new TValue this[TKey key] {
get {
if (!TryGetValue(key, out var val))
base[key] = (val = seedFn(key));
return val;
}
set => base[key] = value;
}
}
public static class MemberInfoExt {
public static Func<object, object> GetValueFunc(this MemberInfo member) {
switch (member) {
case FieldInfo mfi:
return mfi.GetValue;
case PropertyInfo mpi:
return mpi.GetValue;
case MethodInfo mi:
return (object srcObject) => mi.Invoke(srcObject, null);
default:
throw new ArgumentException("MemberInfo must be of type FieldInfo, PropertyInfo or MethodInfo", nameof(member));
}
}
}
public static class TypeExt {
public static MemberInfo GetPropertyOrField(this Type t, string memberName, BindingFlags bf = BindingFlags.Public | BindingFlags.Instance) =>
t.GetMember(memberName, bf).Where(mi => mi.MemberType == MemberTypes.Field || mi.MemberType == MemberTypes.Property).Single();
}
public static class ObjectExt {
static KeySeedFnDictionary<(Type t, string m), MemberInfo> memberInfoCache = new(tm => tm.t.GetPropertyOrField(tm.m));
static KeySeedFnDictionary<(Type t, string m), Func<object, object>> getFnCache = new(tm => memberInfoCache[(tm.t, tm.m)].GetValueFunc());
public static TRes GetValue<TRes>(this object obj, string memberName) {
var getFn = getFnCache[(obj.GetType(), memberName)];
return (TRes)getFn(obj);
}
public static object GetValue<T>(this T obj, string memberName) => ((object)obj).GetValue<object>(memberName);
}
In order to handle the possibility of GetStudentMarks()
returning an empty list, you must change the query to join with the marks last:
var result = (from s in classlist
from e in exams
join m in studentMarks on new { s.StudentID, e.ExamTypeID } equals new { m.StudentID, m.ExamTypeID } into mj
from m in mj.DefaultIfEmpty()
select new ManualEntryAllDto {
StudentID = s.StudentID,
StudentName = s.StudentName,
ExamTypeID = e.ExamTypeID,
ExamName = e.ExamType,
MaxMark = e.MaxMark,
Marks = m?.Mark ?? default
});
Once you have the data (essentially left join plus right join) you can pivot it:
var ans = result.PivotOverWithBysToDynamic("ExamName", "Marks", new[] { "StudentID", "StudentName" });