I am programming a multilevel menu / submenu in ASP.NET MVC.
I have this table:
Id Name ParentId
----------------------------------
1 Menu1 null
2 Menu2 null
3 Submenu1-menu1 1
4 Submenu2-menu1 1
5 1-Level3-submenu1 3
6 2-Level3-submenu1 3
7 3-Level3-submenu1 3
To fetch the data using Entity Framework, I wrote this code:
var category = _context.Categories
.Include(p => p.SubCategories)
.Where(p => p.ParentId == null)
.ToList()
.Select(p => new MenuItemDto
{
CatId = p.Id,
Name = p.Name,
Child = p.SubCategories.ToList().Select(child => new MenuItemDto
{
CatId = child.Id,
Name = child.Name,
Child = child.SubCategories?.ToList().Select(grandchild => new MenuItemDto
{
CatId = grandchild.Id,
Name = grandchild.Name,
}).ToList()
}).ToList(),
}).ToList();
public class MenuItemDto
{
public long CatId { get; set; }
public string Name { get; set; }
public List<MenuItemDto> Child { get; set; }
}
but the result is that just Menu1 and Menu2 and the children of Menu1 (Submenumen1-menu1 and Submenu2-menu1), I could not fetch 1-Level3-submenu1 and 2-Level3-submenu1 and 3-Level3-submenu1
I would recommend simplifying the database call and building the tree in code as this would be more efficient.
To do this create a recursive method for building the children of an parent.
In the example below I am manually generating the Database list and taking the assumption of the ParentId being a string. I am also manually looping through the output and only going to 3 levels for this example. You should be aware that you can have infinite levels if you self reference.
You can see a working example of the code below at https://dotnetfiddle.net/BCyO6I
public class MenuItemDb
{
public long Id { get; set; }
public string Name { get; set; }
public string ParentId { get; set; }
}
public class MenuItemDto
{
public long CatId { get; set; }
public string Name { get; set; }
public List<MenuItemDto> Children { get; set; }
}
public class Program{
public static void Main(string[] args){
//get all data from Context
//var categories = _context.Categories.ToList();
//for the purpose of this test manually generate categories
List<MenuItemDb> categories = new List<MenuItemDb>();
categories.Add(new MenuItemDb() {
Id = 1, Name = "Menu1", ParentId = null
});
categories.Add(new MenuItemDb() {
Id = 2, Name = "Menu2", ParentId = null
});
categories.Add(new MenuItemDb() {
Id = 3, Name = "Submenu1-menu1", ParentId = "1"
});
categories.Add(new MenuItemDb() {
Id = 4, Name = "Submenu1-menu2", ParentId = "1"
});
categories.Add(new MenuItemDb() {
Id = 5, Name = "1-Level3-submenu1", ParentId = "3"
});
categories.Add(new MenuItemDb() {
Id = 6, Name = "2-Level3-submenu1", ParentId = "3"
});
categories.Add(new MenuItemDb() {
Id = 7, Name = "3-Level3-submenu1", ParentId = "3"
});
List<MenuItemDto> menu = new List<MenuItemDto>();
//build top level
foreach(var child in categories.Where(w => w.ParentId == null))
{
MenuItemDto childDto = new MenuItemDto() {
CatId = child.Id,
Name = child.Name
};
AddChildren(childDto, categories);
menu.Add(childDto);
}
foreach(var item in menu){
Console.WriteLine(item.Name);
foreach(var childLevel1 in item.Children){
Console.WriteLine(" -- " + childLevel1.Name);
foreach(var childLevel2 in item.Children){
Console.WriteLine(" -- " + childLevel2.Name);
}
}
}
}
public static void AddChildren(MenuItemDto parent, List<MenuItemDb> categories){
parent.Children = new List<MenuItemDto>();
foreach(var child in categories.Where(w => w.ParentId == parent.CatId.ToString()))
{
var childDto = new MenuItemDto() {
CatId = child.Id,
Name = child.Name
};
AddChildren(childDto, categories);
parent.Children.Add(childDto);
}
}
}
I think you want to create tree of categories so you can do like that:
first Get all of your category and put it in to the memory for better performance of sorting.
with this you can do it:
var unsortedCategories = _context.Categories.ToList()
and then sort your category with parentId
private async Task<List<Category>> SortCategoriesForTreeAsync(
List<Category> source,
int parentId = 0,
CancellationToken cancellationToken = default)
{
if (source is null)
throw new ArgumentNullException(nameof(source));
var result = new List<Category>();
foreach (var cat in source.Where(c => c.ParentId == parentId).ToList())
{
result.Add(cat);
result.AddRange(await SortCategoriesForTreeAsync(source, cat.Id, true, cancellationToken));
}
if (ignoreCategoriesWithoutExistingParent || result.Count == source.Count)
return result;
//find categories without parent in provided category source and insert them into result
foreach (var cat in source)
if (result.FirstOrDefault(x => x.Id == cat.Id) is null)
result.Add(cat);
return result;
}
use method like this:
var sortedAllCategory = await SortCategoriesForTreeAsync(unsortedCategories)
then when you have sorted categories you can create the tree:
private async Task<List<MenuItemDto>> CreateCategories(List<Category> allSortedCategories, int categoryId, CancellationToken cancellationToken)
{
var categories = allSortedCategories
.Where(c => c.ParentId == categoryId);
var listModels = new List<MenuItemDto>();
foreach (var category in categories)
{
var categoryModel = new MenuItemDto
{
CatId = category.Id,
Name = category.Name,
};
var subCategories = await PrepareCategories(allSortedCategories, category.Id, cancellationToken);
categoryModel.Child.AddRange(subCategories);
categoryModel.HaveSubCategories = categoryModel.SubCategories.Any();
listModels.Add(categoryModel);
}
return listModels;
}
so your can use this method to create tree like this:
var result = await PrepareCategories(sortedAllCategory, 0, cancellationToken);
Related
I have an object:
public class Customer
{
public int ID { get; set; }
public string Name { get; set; }
public int GroupID { get; set; }
}
I return a list that may look like the following:
List<Customer> CustomerList = new List<Customer>();
CustomerList.Add( new Customer { ID = 1, Name = "One", GroupID = 1 } );
CustomerList.Add( new Customer { ID = 2, Name = "Two", GroupID = 1 } );
CustomerList.Add( new Customer { ID = 3, Name = "Three", GroupID = 2 } );
CustomerList.Add( new Customer { ID = 4, Name = "Four", GroupID = 1 } );
CustomerList.Add( new Customer { ID = 5, Name = "Five", GroupID = 3 } );
CustomerList.Add( new Customer { ID = 6, Name = "Six", GroupID = 3 } );
I want to return a linq query which will look like
CustomerList
GroupID =1
UserID = 1, UserName = "UserOne", GroupID = 1
UserID = 2, UserName = "UserTwo", GroupID = 1
UserID = 4, UserName = "UserFour", GroupID = 1
GroupID =2
UserID = 3, UserName = "UserThree", GroupID = 2
GroupID =3
UserID = 5, UserName = "UserFive", GroupID = 3
UserID = 6, UserName = "UserSix",
I tried from
Using Linq to group a list of objects into a new grouped list of list of objects
code
var groupedCustomerList = CustomerList
.GroupBy(u => u.GroupID)
.Select(grp => grp.ToList())
.ToList();
works but does not give the desired output.
var groupedCustomerList = CustomerList.GroupBy(u => u.GroupID)
.Select(grp =>new { GroupID =grp.Key, CustomerList = grp.ToList()})
.ToList();
var groupedCustomerList = CustomerList
.GroupBy(u => u.GroupID, u=>{
u.Name = "User" + u.Name;
return u;
}, (key,g)=>g.ToList())
.ToList();
If you don't want to change the original data, you should add some method (kind of clone and modify) to your class like this:
public class Customer {
public int ID { get; set; }
public string Name { get; set; }
public int GroupID { get; set; }
public Customer CloneWithNamePrepend(string prepend){
return new Customer(){
ID = this.ID,
Name = prepend + this.Name,
GroupID = this.GroupID
};
}
}
//Then
var groupedCustomerList = CustomerList
.GroupBy(u => u.GroupID, u=>u.CloneWithNamePrepend("User"), (key,g)=>g.ToList())
.ToList();
I think you may want to display the Customer differently without modifying the original data. If so you should design your class Customer differently, like this:
public class Customer {
public int ID { get; set; }
public string Name { get; set; }
public int GroupID { get; set; }
public string Prefix {get;set;}
public string FullName {
get { return Prefix + Name;}
}
}
//then to display the fullname, just get the customer.FullName;
//You can also try adding some override of ToString() to your class
var groupedCustomerList = CustomerList
.GroupBy(u => {u.Prefix="User", return u.GroupID;} , (key,g)=>g.ToList())
.ToList();
is this what you want?
var grouped = CustomerList.GroupBy(m => m.GroupID).Select((n) => new { GroupId = n.Key, Items = n.ToList() });
var result = from cx in CustomerList
group cx by cx.GroupID into cxGroup
orderby cxGroup.Key
select cxGroup;
foreach (var cxGroup in result) {
Console.WriteLine(String.Format("GroupID = {0}", cxGroup.Key));
foreach (var cx in cxGroup) {
Console.WriteLine(String.Format("\tUserID = {0}, UserName = {1}, GroupID = {2}",
new object[] { cx.ID, cx.Name, cx.GroupID }));
}
}
The desired result can be obtained using IGrouping, which represents a collection of objects that have a common key in this case a GroupID
var newCustomerList = CustomerList.GroupBy(u => u.GroupID)
.Select(group => new { GroupID = group.Key, Customers = group.ToList() })
.ToList();
I have the next mongo document structure :
_id
-countryCode
-keywordID
-name
-displayName
-categories:[Array]
-_id
-name
-position
-canonical
I would like to get all the keywords that are in a specific category only knowing the category's ID. I am using the mongo C# driver but don't know how could I check what's inside that array.
I would like to send a list with the category ID's and get back all the keywords that have a category from that list.
public async Task<List<Keyword>> GetKeywords(List<long> keywordCatIds, string countryCode)
{
var mongoCollection = MongoDatabase.GetCollection<Keyword>("Keywords");
try
{
FilterDefinition<Keyword> mongoFilter = Builders<Keyword>.Filter.In(c=>c.Categories, keywordCatIds);
return await mongoCollection.Find(mongoFilter,null).ToListAsync<Keyword>();
}
catch (Exception ex)
{
Logger.Error(ex, "Multiple ids for Country Code: {0}, ids: {1}", countryCode, string.Join(',', keywordCatIds.Select(s => s)));
return null;
}
}
Your In function looks like a "categories._id" filter in normal mongoDB. Which transitions into an ElemMatch. I created a project which fills the db, than selects
all the keywords that are in a specific category only knowing the category's ID
public class CustomID
{
public string CountryCode { get; set; }
public long KeywordId { get; set; }
public string Name { get; set; }
}
public class Keyword
{
[BsonId]
public CustomID Id { get; set; }
public List<Category> Categories { get; set; }
}
public class Category
{
[BsonId]
public long Id { get; set; }
public string Name { get; set; }
public int Position { get; set; }
}
internal class Program
{
public static IMongoDatabase MongoDatabase { get; private set; }
public static async Task Main()
{
var conventionPack = new ConventionPack
{
new CamelCaseElementNameConvention()
};
ConventionRegistry.Register(
"CustomConventionPack",
conventionPack,
t => true);
var client = new MongoClient();
MongoDatabase = client.GetDatabase("SO");
var ret = await GetKeywords(new List<long> {1L, 2L}, "HU-hu");
// ret is A and B. C is filtered out because no category id of 1L or 2L, D is not HU-hu
}
public static async Task<List<Keyword>> GetKeywords(List<long> keywordCatIds, string countryCode)
{
var mongoCollection = MongoDatabase.GetCollection<Keyword>("keywords");
// be ware! removes all elements. For debug purposes uncomment>
//await mongoCollection.DeleteManyAsync(FilterDefinition<Keyword>.Empty);
await mongoCollection.InsertManyAsync(new[]
{
new Keyword
{
Categories = new List<Category>
{
new Category {Id = 1L, Name = "CatA", Position = 1},
new Category {Id = 3L, Name = "CatC", Position = 3}
},
Id = new CustomID
{
CountryCode = "HU-hu",
KeywordId = 1,
Name = "A"
}
},
new Keyword
{
Categories = new List<Category>
{
new Category {Id = 2L, Name = "CatB", Position = 2}
},
Id = new CustomID
{
CountryCode = "HU-hu",
KeywordId = 2,
Name = "B"
}
},
new Keyword
{
Categories = new List<Category>
{
new Category {Id = 3L, Name = "CatB", Position = 2}
},
Id = new CustomID
{
CountryCode = "HU-hu",
KeywordId = 3,
Name = "C"
}
},
new Keyword
{
Categories = new List<Category>
{
new Category {Id = 1L, Name = "CatA", Position = 1}
},
Id = new CustomID
{
CountryCode = "EN-en",
KeywordId = 1,
Name = "EN-A"
}
}
});
var keywordFilter = Builders<Keyword>.Filter;
var categoryFilter = Builders<Category>.Filter;
var mongoFilter =
keywordFilter.ElemMatch(k => k.Categories, categoryFilter.In(c => c.Id, keywordCatIds)) &
keywordFilter.Eq(k => k.Id.CountryCode, countryCode);
return await mongoCollection.Find(mongoFilter).ToListAsync();
}
}
i have a method GetChild(id) that return the children with respect to id of parent passed as a parameter
those children can also have their own children to any level
if i want to create a JSON which represent the entire hierarchy of child and parent then how should i proceed?
public ActionResult GetChild(long id)
{
Dal objDal = new Dal();
var res = objDal.db.ChildGet(id).ToList();
return Json(res, JsonRequestBehavior.AllowGet);
}
this is justfor first level
how can i use this GetChild(id) method recursively?
any kind of help will be appreciated
public class Comment
{
public int Id { get; set; }
public int ParentId { get; set; }
public string Text { get; set; }
public List<Comment> Children { get; set; }
}
public JsonResult Test()
{
List<Comment> categories = new List<Comment>()
{
new Comment () { Id = 1, Text = "Yabancı Dil", ParentId = 0},
new Comment() { Id = 2, Text = "İngilizce", ParentId = 1 },
new Comment() { Id = 3, Text = "YDS", ParentId = 2 },
new Comment() { Id = 4, Text = "TOEFL", ParentId = 2 },
new Comment() { Id = 5, Text = "YDS Seviye1", ParentId = 3 },
new Comment() { Id = 6, Text = "TOEFL Seviye1", ParentId = 4 }
};
List<Comment> hierarchy = new List<Comment>();
hierarchy = categories
.Where(c => c.Id == 2)
.Select(c => new Comment()
{
Id = c.Id,
Text = c.Text,
ParentId = c.ParentId,
Children = GetChildren(categories, c.Id)
})
.ToList();
List<Comment> list = new List<Comment>();
List<string> list2 = new List<string>();
if (hierarchy != null)
{
liste.AddRange(hierarchy);
}
return Json(liste, JsonRequestBehavior.AllowGet);
}
public static List<Comment> GetChildren(List<Comment> comments, int parentId)
{
hAbDbContext db = new hAbDbContext();
return comments
.Where(c => c.ParentId == parentId)
.Select(c => new Comment()
{
Id = c.Id,
Text = c.Text,
ParentId = c.ParentId,
Children = GetChildren(comments, c.Id)
})
.ToList();
}
I am using C# and I have a list of items that have a nullable parent ID property.
I need to convert this to a list of items that have a list of their children and keep going down generations until there are no items left.
My existing class
public class Item
{
public int Id { get; set; }
public int? ParentId { get; set; }
}
My first thoughts...
Create a class
public class ItemWithChildren
{
public Item Item { get; set; }
public List<ItemWithChildren> Children { get; set; }
}
Now I need some way to get a List<ItemWithChildren> that has all the top level Item objects and their children into the Children property.
Note that the nesting is not a set number of levels.
I was hoping there was an elegant LINQ query that would work. So far I just have this...
var itemsWithChildren = items.Select(a => new ItemWithChildren{ Item = a });
It is more readable to not use pure Linq for this task, but a mixture of Linq and looping.
Given the following container:
class Node
{
public int Id { get; set; }
public int? ParentId { get; set; }
public List<Node> Children { get; set; }
}
Then you can make the tree with the following code.
var nodes = new List<Node>
{
new Node{ Id = 1 },
new Node{ Id = 2 },
new Node{ Id = 3, ParentId = 1 },
new Node{ Id = 4, ParentId = 1 },
new Node{ Id = 5, ParentId = 3 }
};
foreach (var item in nodes)
{
item.Children = nodes.Where(x => x.ParentId.HasValue && x.ParentId == item.Id).ToList();
}
var tree = nodes.Where(x => !x.ParentId.HasValue).ToList();
This will handle any level of depth and return a proper tree.
Given the following method to print the tree:
private void PrintTree(IEnumerable<Node> nodes, int indent = 0)
{
foreach(var root in nodes)
{
Console.WriteLine(string.Format("{0}{1}", new String('-', indent), root.Id));
PrintTree(root.Children, indent + 1);
}
}
The output of this call is:
1
-3
--5
-4
2
If however you want to use pure Linq for this, you can do the following, however to me it is harder to read:
var tree = nodes.Select(item =>
{
item.Children = nodes.Where(child => child.ParentId.HasValue && child.ParentId == item.Id).ToList();
return item;
})
.Where(item => !item.ParentId.HasValue)
.ToList();
This might help ?
var itemsWithChildren = items.Select(a => new ItemWithChildren{
Item = a,
Children = items.Where(b => b.ParentId==a.Id)
.ToList()
});
Then update model to achieve it as
public string Name { get; set; }
[DisplayName("Parent Category")]
public virtual Guid? CategoryUID { get; set; }
public virtual ICollection<Category> Categories { get; set; }
I think you need to do it in two steps...
I've tested and it definitely works...
var items = new List<Item>();
items.Add(new Item { Id = 1, ParentId = null });
items.Add(new Item { Id = 2, ParentId = 1 });
items.Add(new Item { Id = 3, ParentId = 1 });
items.Add(new Item { Id = 4, ParentId = 3 });
items.Add(new Item { Id = 5, ParentId = 3 });
var itemsWithChildren = items.Select(a =>
new ItemWithChildren { Item = a }).ToList();
itemsWithChildren.ForEach(a =>
a.Children = itemsWithChildren.Where(b =>
b.Item.ParentId == a.Item.Id).ToList());
var root = itemsWithChildren.Single(a => !a.Item.ParentId.HasValue);
Console.WriteLine(root.Item.Id);
Console.WriteLine(root.Children.Count);
Console.WriteLine(root.Children[0].Children.Count);
Console.WriteLine(root.Children[1].Children.Count);
Output...
1
2
0
2
I have an object:
public class Customer
{
public int ID { get; set; }
public string Name { get; set; }
public int GroupID { get; set; }
}
I return a list that may look like the following:
List<Customer> CustomerList = new List<Customer>();
CustomerList.Add( new Customer { ID = 1, Name = "One", GroupID = 1 } );
CustomerList.Add( new Customer { ID = 2, Name = "Two", GroupID = 1 } );
CustomerList.Add( new Customer { ID = 3, Name = "Three", GroupID = 2 } );
CustomerList.Add( new Customer { ID = 4, Name = "Four", GroupID = 1 } );
CustomerList.Add( new Customer { ID = 5, Name = "Five", GroupID = 3 } );
CustomerList.Add( new Customer { ID = 6, Name = "Six", GroupID = 3 } );
I want to return a linq query which will look like
CustomerList
GroupID =1
UserID = 1, UserName = "UserOne", GroupID = 1
UserID = 2, UserName = "UserTwo", GroupID = 1
UserID = 4, UserName = "UserFour", GroupID = 1
GroupID =2
UserID = 3, UserName = "UserThree", GroupID = 2
GroupID =3
UserID = 5, UserName = "UserFive", GroupID = 3
UserID = 6, UserName = "UserSix",
I tried from
Using Linq to group a list of objects into a new grouped list of list of objects
code
var groupedCustomerList = CustomerList
.GroupBy(u => u.GroupID)
.Select(grp => grp.ToList())
.ToList();
works but does not give the desired output.
var groupedCustomerList = CustomerList.GroupBy(u => u.GroupID)
.Select(grp =>new { GroupID =grp.Key, CustomerList = grp.ToList()})
.ToList();
var groupedCustomerList = CustomerList
.GroupBy(u => u.GroupID, u=>{
u.Name = "User" + u.Name;
return u;
}, (key,g)=>g.ToList())
.ToList();
If you don't want to change the original data, you should add some method (kind of clone and modify) to your class like this:
public class Customer {
public int ID { get; set; }
public string Name { get; set; }
public int GroupID { get; set; }
public Customer CloneWithNamePrepend(string prepend){
return new Customer(){
ID = this.ID,
Name = prepend + this.Name,
GroupID = this.GroupID
};
}
}
//Then
var groupedCustomerList = CustomerList
.GroupBy(u => u.GroupID, u=>u.CloneWithNamePrepend("User"), (key,g)=>g.ToList())
.ToList();
I think you may want to display the Customer differently without modifying the original data. If so you should design your class Customer differently, like this:
public class Customer {
public int ID { get; set; }
public string Name { get; set; }
public int GroupID { get; set; }
public string Prefix {get;set;}
public string FullName {
get { return Prefix + Name;}
}
}
//then to display the fullname, just get the customer.FullName;
//You can also try adding some override of ToString() to your class
var groupedCustomerList = CustomerList
.GroupBy(u => {u.Prefix="User", return u.GroupID;} , (key,g)=>g.ToList())
.ToList();
is this what you want?
var grouped = CustomerList.GroupBy(m => m.GroupID).Select((n) => new { GroupId = n.Key, Items = n.ToList() });
var result = from cx in CustomerList
group cx by cx.GroupID into cxGroup
orderby cxGroup.Key
select cxGroup;
foreach (var cxGroup in result) {
Console.WriteLine(String.Format("GroupID = {0}", cxGroup.Key));
foreach (var cx in cxGroup) {
Console.WriteLine(String.Format("\tUserID = {0}, UserName = {1}, GroupID = {2}",
new object[] { cx.ID, cx.Name, cx.GroupID }));
}
}
The desired result can be obtained using IGrouping, which represents a collection of objects that have a common key in this case a GroupID
var newCustomerList = CustomerList.GroupBy(u => u.GroupID)
.Select(group => new { GroupID = group.Key, Customers = group.ToList() })
.ToList();