Flat Data to Hierarchical Model C# - c#

I have some flat data coming from the database that looks like this:
List<FlatDataGroup> elements = new List<FlatDataGroup>()
{
new FlatDataGroup {Text = "", GroupID = 1, ParentGroupID = 0, GroupName = "Admin", UserID = 1, UserName = "John Doe"},
new FlatDataGroup {Text = "", GroupID = 1, ParentGroupID = 0, GroupName = "Admin", UserID = 2, UserName = "Jane Smith"},
new FlatDataGroup {Text = "", GroupID = 2, ParentGroupID = 1, GroupName = "Support", UserID = 3, UserName = "Johnny Support"},
new FlatDataGroup {Text = "", GroupID = 3, ParentGroupID = 2, GroupName = "SubSupport", UserID = 4, UserName = "Sub Johnny Support"},
new FlatDataGroup {Text = "", GroupID = 4, ParentGroupID = 1, GroupName = "Production", UserID = 5, UserName = "Johnny Production"}
};
I would like to convert it to this:
List<Group> model = new List<Group>
{
new Group()
{
ID = 1,
Name = "Admin",
ParentGroupID = 0,
Type = "Group",
Users = new List<User>()
{
new User()
{
ID = 1,
Name = "John Doe",
GroupID = 1,
Type = "User",
},
new User()
{
ID = 2,
Name = "Jane Smith",
GroupID = 1,
Type = "User",
},
},
Groups = new List<Group>
{
new Group()
{
ID = 2,
Name = "Support",
ParentGroupID = 1,
Type = "Group",
Users = new List<User>()
{
new User()
{
ID = 3,
Name = "Johnny Support",
GroupID = 2,
Type = "User",
}
},
Groups = new List<Group>()
{
new Group()
{
ID = 3,
Name = "SubSupport",
ParentGroupID = 2,
Type = "Group",
Users = new List<User>()
{
new User()
{
ID = 4,
Name = "Sub Johnny Support",
GroupID = 3,
Type = "User",
}
},
Groups = null
}
}
},
new Group()
{
ID = 4,
Name = "Production",
ParentGroupID = 1,
Type = "Group",
Users = new List<User>()
{
new User()
{
ID = 5,
Name = "Johnny Production",
GroupID = 4,
Type = "User",
}
},
Groups = null
}
}
}
};
which will ultimately display like this in a treeview:
+Admin (Group)
John Doe (User)
Jane Smith (User)
+Support (Group)
Johnny Support (User)
+SubSupport (Group)
Sub Johnny Support (User)
+Production (Group)
Johnny Production (User)
This is what I've come up with so far to transform the flat data into the model above:
List<Group> model = new List<Group>();
var parentGrouping = elements.GroupBy(x => x.ParentGroupID);
foreach (var parentGroup in parentGrouping)
{
var grouping = parentGroup.GroupBy(y => y.GroupID);
foreach (var group in grouping)
{
Group groupItem = new Group()
{
ID = group.FirstOrDefault().GroupID,
Name = group.FirstOrDefault().GroupName,
ParentGroupID = group.FirstOrDefault().ParentGroupID,
Type = "Group",
Users = new List<User>()
};
foreach (var user in group)
{
groupItem.Users.Add(new User()
{
ID = user.UserID,
Name = user.UserName,
GroupID = user.GroupID,
Type = "User",
});
}
model.Add(groupItem);
}
}
All my groups come out along with their children users but the hierarchy is not preserved. I think I may need to do this recursively but I can't seem to get my head around it. Any help would be greatly appreciated.
Here are the models for the sake of completeness:
public class FlatDataGroup
{
public string Text { get; set; }
public int GroupID { get; set; }
public int ParentGroupID { get; set; }
public string GroupName { get; set; }
public int UserID { get; set; }
public string UserName { get; set; }
}
public class Group
{
public int ID { get; set; }
public int ParentGroupID { get; set; }
public string Name { get; set; }
public List<Group> Groups { get; set; }
public List<User> Users { get; set; }
public string Type { get; set; }
}
public class User
{
public int ID { get; set; }
public int GroupID { get; set; }
public string Name { get; set; }
public string Type { get; set; }
}

I'd do this in 3 passes:
Create all Group classes and populate them with data other than child groups, adding them incrementally to a dictionary mapping ID to Group.
Loop through all the groups in the dictionary and add children to their parents' Groups list of children.
Return a filtered list of all groups with no parent group -- these are the root groups. (I also sorted them by ID to remove the random ordering that the dictionary will introduce.)
Thus:
public static class FlatDataGroupExtensions
{
public const string UserType = "User";
public const string GroupType = "Group";
public static List<Group> ToGroups(this IEnumerable<FlatDataGroup> elements)
{
// Allocate all groups and index by ID.
var groups = new Dictionary<int, Group>();
foreach (var element in elements)
{
Group group;
if (!groups.TryGetValue(element.GroupID, out group))
groups[element.GroupID] = (group = new Group() { ID = element.GroupID, Name = element.GroupName, ParentGroupID = element.ParentGroupID, Type = GroupType });
group.Users.Add(new User() { GroupID = element.GroupID, ID = element.UserID, Name = element.UserName, Type = UserType });
}
// Attach child groups to their parents.
foreach (var group in groups.Values)
{
Group parent;
if (groups.TryGetValue(group.ParentGroupID, out parent) && parent != group) // Second check for safety.
parent.Groups.Add(group);
}
// Return only root groups, sorted by ID.
return groups.Values.Where(g => !groups.ContainsKey(g.ParentGroupID)).OrderBy(g => g.ID).ToList();
}
}
I also modified your Group class a little to automatically allocate the lists:
public class Group
{
List<Group> groups = new List<Group>();
List<User> users = new List<User>();
public int ID { get; set; }
public int ParentGroupID { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public List<Group> Groups { get { return groups; } }
public List<User> Users { get { return users; } }
public override string ToString()
{
return string.Format("Group: ID={0}, Name={1}, Parent ID={2}, #Users={3}, #Groups={4}", ID, Name, ParentGroupID, Users.Count, Groups.Count);
}
}

Related

Filter List inside List Linq

I have list say list of customers and inside each list there is another list of orders
Class Customer
{
int ID,
string Name
List<Order> orders
}
Class Order{
int ID,
string Name
}
Also have a integer list of filteredorderIds = {1,2,3,4}
I want to filter the list of customers who has got orderIds from filteredorderIds list.
So far I am stuck at query like
var filteredCustomers = Customers.Where(x => x.Orders.Any(filteredorderIds.contains(y => y.Id)));
please give credit to #Johnathan Barclay, since he posted faster than i typed example
void Main()
{
var customers = new List<Customer>(){
new Customer(){
ID =1,
Name = "Cust1",
orders = new List<Order>(){
new Order(){ID = 4, Name = "o11"},
new Order(){ID = 5, Name = "o12"},
new Order(){ID = 6, Name = "o13"}
}
},
new Customer(){
ID = 2,
Name = "Cust2",
orders = new List<Order>(){
new Order(){ID = 3, Name = "o21"},
new Order(){ID = 7, Name = "o22"},
new Order(){ID = 8, Name = "o23"}
}
}
};
customers.Where(w =>
w.orders.Any(w => filteredorderIds.Contains(w.ID))
).Dump();
}
List<int> filteredorderIds = new List<int>() { 1, 2, 3, 4 };
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 Name { get; set; }
}

LINQ distinct on groups of information

I have multiple items (List)
I need to get distinct features for the items but the issue is that each item has two features. So both features need to match on GroupTypeId and GroupId to become a distinct group.
I need to group items that have the same features (per above distinct groups found). I don't need features here again at the item level since I will have these per above in a separate object.
I need to keep items order intact, the first item will go in group 1, then second might go in group 1 or group 2 and so on.
Also, each item in group, item number needs to be overwritten per the new sequence in that group.
Can i do above tasks purely with LINQ rather than using nested loops?
In the below sample for items
i have 3 distinct feature groups
and 3 item groups. Item 2 and 4 needs to be grouped together and the line no needs to change to 1 and 2. For item 1 and 3, line number should become as 1.
Need to add to List whose count will be 3
Index[0] will have 2 features and 1 item
Index[1] will have 2 features and 2 items
Index[2] will have 2 features and 1 item
public class ItemPicked
{
public int Id { get; set; }
public string Description { get; set; }
public int LineNumber { get; set; }
public int PartId { get; set; }
public List<ItemFeature> Features { get; set; }
}
public class ItemFeature
{
public string OriginalReceived { get; set; }
public Group Feature { get; set; }
}
public class Group
{
public int Id { get; set; }
public string GroupTypeId { get; set; }
public string GroupId { get; set; }
public int SequenceNo { get; set; }
}
public class PickedGrouping
{
public List<ItemFeature> Features { get; set; }
public List<ItemPicked> Items { get; set; }
}
var SampleFeatures1 = new List<ItemFeature>() {
new ItemFeature {
OriginalReceived = "SomeThing",
Feature = new Group() {
Id = 1, SequenceNo = 1, GroupTypeId = "A", GroupId = "B"
}
},
new ItemFeature {
OriginalReceived = "SomeThing2",
Feature = new Group() {
Id = 2, SequenceNo = 2, GroupTypeId = "Y", GroupId = "Z"
}
}
};
var SampleFeatures2 = new List<ItemFeature>() {
new ItemFeature {
OriginalReceived = "SomeThing3",
Feature = new Group() {
Id = 3, SequenceNo = 3, GroupTypeId = "C", GroupId = "D"
}
},
new ItemFeature {
OriginalReceived = "SomeThing4",
Feature = new Group() {
Id = 4, SequenceNo = 4, GroupTypeId = "X", GroupId = "Y"
}
}
};
var SampleFeatures3 = new List<ItemFeature>() {
new ItemFeature {
OriginalReceived = "SomeThing5",
Feature = new Group() {
Id = 3, SequenceNo = 3, GroupTypeId = "C", GroupId = "D"
}
},
new ItemFeature {
OriginalReceived = "SomeThing5",
Feature = new Group() {
Id = 2, SequenceNo = 2, GroupTypeId = "M", GroupId = "K"
}
}
};
var items = new List<ItemPicked>(){
new ItemPicked{
Id = 1, Description = "Item 1", LineNumber = 1, Features = SampleFeatures1
},
new ItemPicked{
Id = 2, Description = "Item 2", LineNumber = 2, Features = SampleFeatures2
},
new ItemPicked{
Id = 3, Description = "Item 3", LineNumber = 3, Features = SampleFeatures3
},
new ItemPicked{
Id = 4, Description = "Item 4", LineNumber = 4, Features = SampleFeatures2
}
};
var pickedGroupings = new List<PickedGrouping>();
PickedGrouping selectedGroup = null;
foreach (var item in items)
{
var found = 0;
if(item.Features == null || !item.Features.Any())
{
selectedGroup = pickedGroupings.FirstOrDefault(x => x.Features == null || !x.Features.Any());
if (selectedGroup == null) selectedGroup = new PickedGrouping();
selectedGroup.Features.AddRange(item.Features);
}
else
{
foreach (var feature in item.Features)
{
foreach (var pg in pickedGroupings)
{
if ((item.Features == null || !item.Features.Any()) && (pg.Features == null || !pg.Features.Any())){
selectedGroup = pg;
found += 1;
}
else
{
foreach (var pgf in pg.Features)
{
if (pgf.Feature == null) continue;
if (pgf.Feature.GroupId == feature.Feature.GroupId && pgf.Feature.GroupTypeId == feature.Feature.GroupTypeId)
{
selectedGroup = pg;
found += 1;
}
}
}
}
}
if (found < 2)
{
pickedGroupings.Add(new PickedGrouping() { Features = item.Features });
selectedGroup = pickedGroupings[pickedGroupings.Count - 1];
}
}
//add item
if (selectedGroup.Items == null) selectedGroup.Items = new List<ItemPicked>();
selectedGroup.Items.Add(item);
}
//update line number
foreach(var pg in pickedGroupings)
{
var lineNum = 1;
foreach(var item in pg.Items)
{
item.LineNumber = lineNum;
lineNum += 1;
}
}

How can I join different property object collections

I have a list of my issue class
var firstList = new List<object> {
new { id =1, Name = "item-1"},
new { id =2, Name = "item-2"},
new { id =3, Name = "item-3"},
new { id =4, Name = "item-4"}
}
And my second list is like this
var secondList = new List<Issue> {
new Issue{ id =1, Date = "01.01.2017"},
new Issue{ id =2, Date = "01.02.2017"}
}
So I want to create a new list like this. (same id objects will be isPlanned=true)
var firstList = new List<object> {
new { id =1, Name = "item-1", isPlanned=true, Date = "01.01.2017"},
new { id =2, Name = "item-2", isPlanned=true, Date = "01.02.2017"},
new { id =3, Name = "item-3", isPlanned=false },
new { id =4, Name = "item-4", isPlanned=false }
}
How can I do this linq functions or lambda operations?
You can use left join for this:
var items = new[] {
new { id =1, Name = "item-1"},
new { id =2, Name = "item-2"},
new { id =3, Name = "item-3"},
new { id =4, Name = "item-4"}
};
var issues = new[] {
new { id =1, Date = "01.01.2017"},
new { id =2, Date = "01.02.2017"}
};
var joined = from item in items
join issue in issues on item.id equals issue.id into gj
from sub in gj.DefaultIfEmpty()
select new { item.id, item.Name, isPlanned=sub?.Date != null, sub?.Date };
foreach (var t in joined) {
Console.WriteLine("{0} {1} {2} {3}", t.id, t.Name, t.isPlanned, t.Date);
}
The result will have nulls when dates are missing, i.e.
new { id =1, Name = "item-1", isPlanned=true, Date = "01.01.2017"},
new { id =2, Name = "item-2", isPlanned=true, Date = "01.02.2017"},
new { id =3, Name = "item-3", isPlanned=false, Date = null },
new { id =4, Name = "item-4", isPlanned=false, Date = null }
Demo.
If you wanted to use a lambda expression, you could do it the following way:
List<Foo> foos = new List<Foo>()
{
new Foo(){ Id = 1, Name = "item-1"},
new Foo(){ Id = 2, Name = "item-2"},
new Foo(){ Id = 3, Name = "item-3"},
new Foo(){ Id = 4, Name = "item-4"}
};
List<Bar> bars = new List<Bar>()
{
new Bar(){ Id = 1, Date = "01.01.2017"},
new Bar(){ Id = 2, Date = "01.02.2017"}
};
IEnumerable<dynamic> result = foos.GroupJoin(bars,
f => f.Id,
b => b.Id,
(foo, bar) => new
{
Id = foo.Id,
Name = foo.Name,
isPlanned = String.IsNullOrEmpty(bar.SingleOrDefault()?.Date),
Date = bar.SingleOrDefault()?.Date
});
here are the classes
public class Foo
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Bar
{
public int Id { get; set; }
public string Date { get; set; }
}
I noticed also from the comments from another answer that you are not able to use C#6.0, so this section would not work for you
isPlanned = String.IsNullOrEmpty(bar.SingleOrDefault()?.Date),
Date = bar.SingleOrDefault()?.Date
You can replace that with the following:
isPlanned = bar.SingleOrDefault() == null ? false : String.IsNullOrEmpty(bar.SingleOrDefault().Date),
Date = bar.SingleOrDefault() == null ? null : bar.SingleOrDefault().Date

Cannot group data in LINQ

I have a question about a LINQ grouping.
I thought that grouping would be a simple matter of using the GroupBy function on the result set and specifying what to group it by. However my items appear to not be grouping together and instead are displaying as if the GroupBy function wasn't there. I want to group by the itemPk, but I'm can't seem to do it. I have tried grouping by both category.ItemFk and Item.Itempk, but no luck. Could someone give me a pointer on this?
var itemIds = items.Select(i => i.ItemId).ToList();
var itemAndCatJoin =
from item in Context.SCS_Items
join category in Context.SCS_ItemCategories
on item.ItemPk equals category.ItemFk
into temp
from category in temp.DefaultIfEmpty()
select new ExportItemTable
{
Category = category,
Item = item
};
return itemAndCatJoin.Where(i => itemIds.Contains(i.Item.ItemPk))
.GroupBy(n => new {n.Item, n.Category})
.Select(i => new ExportableItem
{
ItemPk = i.Key.Item.ItemPk,
Name = i.Key.Item.Name,
Description = i.Key.Item.Description,
Price = i.Key.Item.Price,
Category = i.Key.Category.Category.Category_Name,
GLDepartment = i.Key.Category.GL_Department.Name ?? "",
GLName = i.Key.Category.GL_Name.Name ?? "",
StartDate = i.Key.Item.StartDate,
EndDate = i.Key.Item.EndDate,
FiscalYear = i.Key.Item.SCS_FiscalYear.Name,
School = i.Key.Item.School != null ? i.Key.Item.School.School_Name : i.Key.Item.Board.Board_Name,
Beneficiary = i.Key.Item.SCS_Beneficiary.Name,
Quantity = i.Key.Item.MaxQuantity,
Deleted = i.Key.Item.DeletedFlag,
OptionalStudents = i.Key.Item.SCS_Attachments.Where(a => !a.IsRequired).SelectMany(a => a.SCS_StudentAttachments).Where(s => !s.DeletedFlag).Select(s => s.StudentFk).Distinct().Count(),
RequiredStudents = i.Key.Item.SCS_Attachments.Where(a => a.IsRequired).SelectMany(a => a.SCS_StudentAttachments).Where(s => !s.DeletedFlag).Select(s => s.StudentFk).Distinct().Count(),
IsPublic = i.Key.Item.IsPublic,
AllowRecurring = i.Key.Item.AllowRecurringPayments,
EffectiveCutoff = i.Key.Item.SCS_Attachments.Where(a => !a.DeletedFlag && a.CourseDropCutoff.HasValue).Select(a => a.CourseDropCutoff).OrderBy(a => a).FirstOrDefault(),
CreatedDate = i.Key.Item.CreatedDate
}).OrderBy(i => i.ItemPk).ToList();
}
your groupbyy is indeed doing nothing for you, you need to tell the groupby what to group by....
like
.GroupBy(n => n.Category)
Here is a simple example to your grouping question:
class Program
{
static void Main()
{
var allItems = GetAllItems();
var groups = from item in allItems
group item by item.Category
into newGroup
select newGroup;
foreach (var group in groups)
{
Console.WriteLine($"\nCategory: {group.Key}");
foreach (var item in group)
{
Console.WriteLine($"{item.Name}: {item.Price}");
}
}
Console.ReadLine();
}
static List<Category> GetAllCategories()
{
return new List<Category>()
{
new Category() { Id = 1, Name = "Programming Books" },
new Category() { Id = 2, Name = "Fiction Books" }
};
}
static List<Item> GetAllItems()
{
return new List<Item>()
{
new Item() { Id = 1, Name = "Embedded Linux", Category = 1, Price = 9.9 },
new Item() { Id = 2, Name = "LINQ In Action", Category = 1, Price = 36.19 },
new Item() { Id = 3, Name = "C# 6.0 and the .NET 4.6 Framework", Category = 1, Price = 40.99 },
new Item() { Id = 4, Name = "Thinking in LINQ", Category = 1, Price = 36.99 },
new Item() { Id = 5, Name = "The Book Thief", Category = 2, Price = 7.99 },
new Item() { Id = 6, Name = "All the Light We Cannot See", Category = 2, Price = 16.99 },
new Item() { Id = 7, Name = "The Life We Bury", Category = 2, Price = 8.96 }
};
}
}
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public int Category { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
This example is simple enough for anyone new to LINQ. I am sure you can make some adjustment to make it work for your specific issue. Hope this will help.

LINQ group list and combine collections

Given these classes:
public class Employee
{
public int EmployeeId { get; set; }
public int GroupId { get; set; }
public List<Plans> Plans { get; set; }
}
public class Plan
{
public int PlanYearId { get; set; }
public string Name { get; set; }
}
And given a setup like so:
var employees = new List<Employee> {
new Employee {
EmployeeId = 1,
GroupId = 1,
Plans = new List<Plan> {
new Plan {
PlanReferenceId = 1111,
Name = "Benefit 1"
}}};
new Employee {
EmployeeId = 1,
GroupId = 1,
Plans = new List<Plan> {
new Plan {
PlanReferenceId= 2222,
Name = "Benefit 2"
},
new Plan {
PlanReferenceId= 2222,
Name = "Benefit 3"
}}}};
How can I use LINQ to group these employees by both EmployeeId and GroupId and then combine the two List<Plan> properties so that i would end up with something like this:
var employee = new Employee
{
EmployeeId = 1,
GroupId = 1,
Plans = new List<Plan> {
new Plan {
PlanReferenceId = 1111,
Name = "Benefit 1"
},
new Plan {
PlanReferenceId = 2222,
Name = "Benefit 2"
},
new Plan {
PlanReferenceId = 2222,
Name = "Benefit 3"
}
}
}
Just use combination of GroupBy and SelectMany:
var result = employees
.GroupBy(e => new { e.EmployeeId, e.GroupId })
.Select(g => new Employee
{
EmployeeId = g.Key.EmployeeId,
GroupId = g.Key.GroupId,
Plans = g.SelectMany(e => e.Plans).ToList()
}).ToList();

Categories