I have three level (Category- Subcategory - Nestedcategory) dropdown navigation menu on my website for which data must come dynamically from database. My main problem in generation of InvokeAsync() method to make it work. I can write two levels which work fine as I checked, but confused in defining Nestedcategories - need to get it from subcategories which derived from categories.
Here is my Controller
public class MenuViewComponent: ViewComponent
{
private readonly SamirDbContext _samirDbContext;
public MenuViewComponent(SamirDbContext samirDbContext)
{
_samirDbContext = samirDbContext;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var menu = await _samirDbContext.Categories.Include(x => x.Subcategories).ThenInclude(y => y.NestedCategories).
Select(x => new MenusModel()
{
Category = x,
Id = x.Id,
Subcategories = x.Subcategories,
**NestedCategories = ...**
}).ToListAsync();
return View(menu);
}
}
Here are models:
public class Category
{
public Category()
{
Subcategories = new HashSet<Subcategory>();
}
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Subcategory> Subcategories { get; set; }
}
public class Subcategory
{
public Subcategory()
{
Posts = new HashSet<Post>();
NestedCategories = new HashSet<NestedCategory>();
}
public int Id { get; set; }
public string Name { get; set; }
public Category Category { get; set; }
public int CategoryId { get; set; }
public ICollection<Post> Posts { get; set; }
public ICollection<NestedCategory> NestedCategories { get; set; }
}
public class NestedCategory
{
public NestedCategory()
{
Posts = new HashSet<Post>();
}
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Post> Posts { get; set; }
public Subcategory Subcategory { get; set; }
public int SubcategoryId { get; set; }
}
Menu ViewModel
public class MenusModel
{
public int Id { get; set; }
public Category Category { get; set; }
public IEnumerable<Category> Categories { get; set; }
public IEnumerable<Subcategory> Subcategories { get; set; }
public Subcategory Subcategory { get; set; }
public IEnumerable<NestedCategory> NestedCategories { get; set; }
public NestedCategory NestedCategory { get; set; }
}
Please, help in completion InvokeAsyinc() method in order to get work for 3 level menu.
You can use SelectMany() method, change the linq like below:
var menu = await _samirDbContext.Categories
.Select(x => new MenusModel()
{
Category = x,
Id = x.Id,
Subcategories = x.Subcategories,
NestedCategories = x.Subcategories.SelectMany(s => s.NestedCategories).ToList()
}).ToListAsync();
Looking at your models for Category, Subcategory, and NestedCategory, I'm asking myself why you actually could need to have a separate output property (**NestedCategories = ...**) in your final Select statement.
Let's think this way if the NestedCategory is defined inside the Subcategory collection, then every Subcategory element should have its own list of NestedCategory-ies, which will be available when you will check some Subcategory from the dropdown.
So, my advice here is to leave the result as follows:
var menu = await _samirDbContext.Categories
.Include(x => x.Subcategories)
.ThenInclude(y => y.NestedCategories)
.Select(x => new MenusModel()
{
Category = x,
Id = x.Id,
Subcategories = x.Subcategories
.Select(sb => new SubcategoryDTO
{
sb.Id,
sb.Name,
...
NestedCategories = sb.NestedCategories
.Select(nst => new NestedCategoriesDTO
{
nst.Id,
nst.Name,
...
})
}),
}).ToListAsync();
Then you can use the above model in your UI.
Hope this will help ))
Related
I have the following Code:
public class ProductTbl
{
public override int Id { get; set; }
public string ProductName { get; set; }
public List<ProductManufacturer> ProductManufacturer { get; set; } //M2M
}
public class Manufacturer_LKP
{
public override int Id { get; set; }
public string ManufacturerName { get; set; }
public List<ProductManufacturer> ProductManufacturer { get; set; } //M2M
}
public class ProductManufacturer
{
public ProductTbl Product { get; set; }
public int ProductID { get; set; }
public Manufacturer_LKP Manufacturer { get; set; }
public int ManufacturerID { get; set; }
}
public class SupplierTbl
{
public int SupplierID { get; set; }
public string SupplierName { get; set; }
}
public class ProductSuppliertbl
{
public int Id { get; set; }
public ProductTbl Product { get; set; }
public int ProductID { get; set; }
public SuppilerTbl Supplier { get; set; }
public int SupplierID { get; set; }
}
*I need to write Linq query to join all 3 tables (Product,Manufacture,ProductManufacturer) to get ProductName and ManufatureName together in one DB trip
*When I do the following I missed the Manufacture object (Manufacture=Null)
DbSet<ProductTbl>()
.Where(a => a.Id == 5)
.AsNoTracking()
.Include(a => a.ProductType)
.Include(a => a.ProductManufacturer)
Above Linq Just joint Product table with ProductManufacture Table So I cannot Get "ManufactureName"
So Is there is any way to join the 3 tables to get ManufactureName beside the ProductName in one DB trip?
Projection is your friend when trying to load related data. The issue with many-to-many is that you are saying a product has many manufacturers, while at the same time it has many suppliers
The Product would need a reference to the ProductSuppliers for that product to easily manage the many suppliers requirement.
var productData = context.Products
.Select(p => new
{
p.ProductName,
ManufacturerNames = p.ProductManufacturers.Select(pm => pm.Manufacturer.ManufacturerName).ToList(),
SupplierNames = x.ProductSuppliers.Select(ps => ps.Supplier.SupplierName).ToList()
}).ToList();
This gives you a list of products, with each product's associated manufacturer names and supplier names. With that data you can format output how you see fit.
If you want the entities themselves, then the missing bit is ThenInclude:
var products = context.Products
.Include(p => p.ProductManufacturers)
.ThenInclude(pm => pm.Manufacturer)
.Include(p => p.ProductSuppliers)
.ThenInclude(ps => ps.Supplier)
.AsNoTracking()
.ToList();
This would load the entire entity graph.
If you don't want or cannot put a ProductSuppliers collection in product then you can build the query entirely from the ProductSupplier, but it's a bit messier.
If you are using EF Core 5 and your joining entities (ProductManufacturer/ProductSupplier) are just simply the FK references to their respective entities, then you can do away with the joining entity and let EF manage it behind the scenes. Product would just contain a collection of Manufacturers and a collection of Suppliers. These can be configured still with a HasMany..WithMany, but makes queries a lot cleaner to look at without the intermediate entities.
I.e.
var productData = context.Products
.Select(p => new
{
p.ProductName,
ManufacturerNames = p.Manufacturers.Select(m => m.ManufacturerName).ToList(),
SupplierNames = x.Suppliers.Select(s => ps.SupplierName).ToList()
}).ToList();
and
var products = context.Products
.Include(p => p.Manufacturers)
.Include(p => p.Suppliers)
.AsNoTracking()
.ToList();
... respectively. Intermediate joining entities are only needed if there are additional properties you want to access in the joining entity. (I.e. CreatedBy/At, etc.)
try this
var list = context.ProductManufactures
.Select(i => new
{
ProductName = i.Product.ProductName,
ManufacturerName = i.Manufacturer.ManufacturerName,
SupplierNames = i.Product.ProductSuppliers.Select(s => s.SupplierName).ToList()
}).ToList();
or you can try this too
var productData = context.Products
.Select(i => new
{
ProductName= i.ProductName,
ManufacturerNames = i.ProductManufacturers.Select(m => m.Manufacturer.ManufacturerName),
SupplierNames = i.ProductSuppliers.Select(s => s.Supplier.SupSupplierName)
}).ToList();
but before this you have to fix some navigation properties
public class Product
{
public int Id { get; set; }
public string ProductName { get; set; }
public List<ProductManufacturer> ProductManufacturers { get; set; }
public List<ProductSupplier> ProductSuppliers { get; set; }
}
public class Supplier
{
public int SupplierID { get; set; }
public string SupplierName { get; set; }
public List<ProductSupplier> ProductSuppliers { get; set; }
}
public class ProductSupplier
{
public int Id { get; set; }
public Product Product { get; set; }
public int ProductID { get; set; }
public Supplier Supplier { get; set; }
public int SupplierID { get; set; }
}
public class Manufacturer_LKP
{
public int Id { get; set; }
public string ManufacturerName { get; set; }
public List<ProductManufacturer> ProductManufacturer { get; set; }
}
public class ProductManufacturer
{
public Product Product { get; set; }
public int ProductID { get; set; }
public Manufacturer_LKP Manufacturer { get; set; }
public int ManufacturerID { get; set; }
}
I want to reduce duplicated code. In order to achieve that I want to reference the projections of my Entities.
Entities
public class Category
{
public string Id { get; set; }
public string CategoryName { get; set; }
public static Expression<Func<Category, Category>> Proj() => c => new Category
{
CategoryName = c.CategoryName
};
}
public class Image
{
public string Id { get; set; }
public string Url { get; set; }
public static Expression<Func<Image, Image>> Proj() => i => new Image
{
Url = i.Url
};
}
public class Product
{
public string Id { get; set; }
public string Name { get; set; }
public ICollection<Image> Images { get; set; }
public Category Category { get; set; }
}
Projection Query
var categoryProjection = Category.Proj().Compile();
var products = _ctx.Products.Select(p => new Product
{
Id = p.Id,
Name = p.Name,
Images = p.Images.AsQueryable().Select(Image.Proj()).ToHashSet(),
Category = categoryProjection.Invoke(p.Category)
});
When I execute the projection then it will work correctly for Product and Images. But for Category the genereted SQL will contain all Columns (Id and CategoryName).
I have 2 simple model classes:
public class Category
{
public int Id { get; set; }
public string Description { get; set; }
public ICollection<SubCategory> SubCategories { get; set; }
}
public class SubCategory
{
public int Id { get; set; }
public string Description { get; set; }
public Category Category { get; set; }
}
And a simple context:
public class SeederContext : DbContext
{
public DbSet<Category> Categories { get; set; }
public DbSet<SubCategory> SubCategories { get; set; }
}
I'm trying to seed the following csv files
BetCategories.csv:
Id,Description
1,Sports
2,Politics
BetSubCategories:
Id,Category_Id,Description
1,1,Soccer
2,1,Basketball
3,1,Tennis
4,1,Poker
5,2,Election
I built a seeder using CsvHelper:
public void Seed(SeederContext context)
{
SeedTable<Category>("../../Data/BetCategories.csv", t =>
{
context.Categories.AddOrUpdate(c => c.Id, t);
context.SaveChanges();
});
SeedTable<SubCategory>("../../Data/BetSubCategories.csv", t =>
{
context.SubCategories.AddOrUpdate(c => new { c.Description}, t);
context.SaveChanges();
});
}
public void SeedTable<T>(string path, Action<T[]> callback)
{
var filename = path;
if (filename != null)
using (var reader = new StreamReader(filename, Encoding.UTF8))
{
var csvReader = new CsvReader(reader);
csvReader.Configuration.WillThrowOnMissingField = false;
var list = csvReader.GetRecords<T>().ToArray();
callback(list);
}
}
The problem occurs when seeding the sub categories.
On the first seed (Categories) it goes OK and 2 categories written to the database.
On the second seed, the SubCategories goes OK and 5 sub categories are written to the database. BUT, they are also written to the Categories table - so now I have 2+5=7 categories in total.
I'm not sure what I'm doing wrong.
This is a very simple code-first with no configuration at all.
Please advise.
Small update
when doing the seed manually - it works
SeedTable<Category>("../../Data/BetCategories.csv", t =>
{
context.Categories.AddOrUpdate(c => c.Id, t);
context.SaveChanges();
});
var subCategory = new SubCategory
{
Category = context.Categories.FirstOrDefault(c => c.Description == "Sports"),
Description = "Soccer"
};
context.SubCategories.AddOrUpdate(c => c.Description, subCategory);
context.SaveChanges();
subCategory = new SubCategory
{
Category = context.Categories.FirstOrDefault(c => c.Description == "Sports"),
Description = "Basketball"
};
context.SubCategories.AddOrUpdate(c => c.Description, subCategory);
context.SaveChanges();
I wonder if this will solve
public class SubCategory
{
public int Id { get; set; }
[ForeignKey("Category")]
public int CategoryId { get; set; }
public string Description { get; set; }
public virtual Category Category { get; set; }
}
I have 3 tables , one to many relationship.
I need to get only specific columns with SelectMany method.
I need to get only Categories.CategoryName and Comments.CommentDate of the selected News object.
Here is my code
News news = db.News.Include(w => w.Categories)
.Include(w => w.Comments).SingleOrDefault(n => n.NewsId == Id);
Here are my Entities:
News Entity:
public partial class News
{
public News()
{
this.Categories = new HashSet<Category>();
this.Comments = new HashSet<Comment>();
}
public int NewsId { get; set; }
public string NewsTitle { get; set; }
public string NewsBody { get; set; }
public System.DateTime NewsDate { get; set; }
public string NewsImagePath { get; set; }
public virtual ICollection<Category> Categories { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
Category Entity:
public partial class Category
{
public Category()
{
this.News = new HashSet<News>();
}
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public virtual ICollection<News> News { get; set; }
}
Comment Entity:
public partial class Comment
{
public Comment()
{
this.News = new HashSet<News>();
}
public int CommentId { get; set; }
public string CommentBody { get; set; }
public Nullable<System.DateTime> CommentDate { get; set; }
public virtual ICollection<News> News { get; set; }
}
This LINQ query should take care of it:
var query =
from news in db.News
where news.Id == Id
let categoryNames =
from category in news.Categories
select category.Name
let commentDates =
from comment in news.Comments
select comment.CommentDate
select new {
CategoryNames = categoryNames.ToList(),
CommentDates = commentDates.ToList()
};
That query is not using SelectMany, but that wouldn't help you, since then you wouldn't be able to group your categories and comments by news items. Since categories and comments are not directly connected, you'd need two SelectManys and then you'd need to cross join the results. That would obviously not be what you want.
Maybe try using the following?
var categoryNames = news.Categories.Select(c=>c.CategoryName);
var commentDates = news.Comments.Select(c=>c.CommentDate);
Note that SelectMany is used to flatten lists.For example, lets say you have collection of news matching certain search criteria, and then you use SelectMany to collect all the Categories/Comments of these news set, in a flat list.
I Have a problem with the EF.
The realated entity is always null. I didn't get any solutions so far.
Here are the models:
public class Categories
{
public int ID { get; set; }
public string Name { get; set; }
public int AtpID { get; set; }
public virtual ICollection<SubCategories> SubCategories { get; set; }
}
public class SubCategories
{
public int ID { get; set; }
public string Name { get; set; }
public int CategoryID { get; set; }
public string LinkToProducts { get; set; }
}
So, every Categorie has more subcategories.
In the Seed method I populated the database, so I have data:
var categories = new List<Categories>
{
new Categories{Name="Abgasanlage", ID=1},
new Categories{Name="Elektrik",ID=2},
new Categories{Name="Filter", ID=3},
new Categories{Name="Karosserie", ID=4},
new Categories{Name="Kuhlunkg",ID=5}
};
categories.ForEach(s => context.Categories.Add(s));
context.SaveChanges();
var subCategories = new List<SubCategories>
{
new SubCategories{Name="Montageteile", ID=1, CategoryID=1},
new SubCategories{Name="Lamdasonde",ID=2, CategoryID=1},
new SubCategories{Name="Anlasser", ID=3, CategoryID=2},
new SubCategories{Name="Luftfilter", ID=4, CategoryID = 3},
new SubCategories{Name="Ohlfilter", ID=5, CategoryID = 3},
new SubCategories{Name="Sonstige", ID=6, CategoryID = 4},
new SubCategories{Name="Wasserpumpe", ID=7, CategoryID = 5}
};
subCategories.ForEach(s => context.SubCategories.Add(s));
context.SaveChanges();
Altought it seems that evrything is ok, the related entity is always null, even with the Include(), is null.
I tried this way:
Models.Categories entity = db.Categories.Where(m => m.ID == 3)
.Include(m => m.SubCategories)
.FirstOrDefault();
but entity.SubCategories is always null.
with Include also the related entity is null
var setting = (from s in db.Categories.Include("SubCategories")
where s.ID == 3
select s).FirstOrDefault();
In my project I have more related entities, where the lazy loading is working.
Only with these models (Categories and SubCategories) I have the problem.
What I'm doing wrong?
Your problem is on your SubCategories model.You have to fix that as shown below.
Note : use public virtual Categories Categories { get; set; } on it.Hence you didn't do that,EF doesn't know how to fetch the related entities (or navigational property) from the db when you use Include or Lazy loading.And also you need to change CategoryID as CategoriesID.B'cos your model's name is Categories.
public class SubCategories
{
public int ID { get; set; }
public string Name { get; set; }
[ForeignKey("CategoriesID")]
public virtual Categories Categories{ get; set; }//you have to do this
public int CategoriesID { get; set; }
public string LinkToProducts { get; set; }
}
Try this:
public class SubCategories
{
public int ID { get; set; }
public string Name { get; set; }
[ForeignKey("CategoryID")]
public Categories Category { get; set; }
public int CategoryID { get; set; }
public string LinkToProducts { get; set; }
}
You are missing the navigation property in SubCategories class:
public virtual Categories Categories { get; set; }