I am building an asp.net website with ms-sql db for products - each product belongs to one or more categories and each categories can belong to one or zero parent categories.
The user should be able to select zero or many categories, but i can't figure out a way to only return products in the selected categories.
i've got other filters which are working (minimum price, brand name etc) but cant' get the categories to work.
For example:
-Category 1
|---Sub-Category 1.1
|---Sub-Sub-Category 1.1.1
|---Sub-Sub-Category 1.1.2
|---Sub-Category 1.2
|---Sub-Category 1.3
If Category 1 is selected, then all products that have a category where the Ultimate Parent is Category 1 should be returned.
If Sub-Category 1.1 and Sub-Category 1.2 is selected then all products that have a category where the Ultimate Parent is either Sub-Category 1.1 OR Sub-Category 1.2 should be returned.
This is my Code:
Product:
public class Product
{
[Key]
public int ProductID { get; set; }
public string Description {get;set;}
public double Price {get;set;}
public virtual List<Category> Categories { get; set; }
public Product()
{
Categories = new List<Category>();
}
}
Category:
public class Category
{
[Key]
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public int? ParentCategoryID { get; set; }
[JsonIgnore]
public Category Parent { get; set; }
[JsonIgnore]
public virtual List<Product> Products { get; set; }
}
Simplified WebApi Controller:
public object Get( [FromUri] string[] categories)
{
List<Product> search_results = (from p in db.Products
where 1==1
&& p.Price >= minPrice && p.Price <=maxPrice
// only return products in selected categories
select p).ToList();
}
If you have no limits on sub categories, you can use recursion:
public object Get( [FromUri] string[] categories)
{
var categories = db.Categories.Where(c => categoriesIds.Contains(c.Id));
Func<Product, bool> filters = p => 1==1
&& p.Price >= minPrice && p.Price <=maxPrice
return GetCategoryiesProducts(categories, filters)
}
public IList<Product> GetCategoryiesProducts(IList<Category> categories, Func<Product, bool> filters)
{
var result = new List<Product>();
foreach (var c in categories)
{
result.AddRange(c.Products.Where(filters).ToList());
var subCategories = db.Categories.Where(s => s.ParentCategoryID != null && (int)s.ParentCategoryID == c.Id))
if (subCategories != null && subCategories.Count > 0)
{
result.AddRange(GetCategoryiesProducts(subCategories, filters))
}
}
List<Product> search_results result;
}
but if there be a lot of categories it will be expensive solution. To optimize for efficiency you can add product to each parent categories to eliminate recursion:
List<Product> search_results = (from p in db.Products
where 1==1
&& p.Price >= minPrice && p.Price <=maxPrice
&& p.Categories.Any(c => categoriesIds.Contains(c.Id))
select p).ToList();
Related
My application is .net Core MVC. I have two classes
public class Product
{
public int Id { get; set; }
public string Buyer { get; set; }
public int? ProductId { get; set; }
public ProductType ProductType { get; set; }
}
public class ProductType
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public ICollection<Product> Product { get; set; }
}
I am trying to generate a list of product name using the following:
List<ProductType> productnames;
var products = _context.Product().Where(x => x.Buyer == "Test" && x.ProductId != null).ToList();
foreach (var item in products)
{
productnames = _context.ProductType.Where(c => c.ProductId == item.ProductId).ToList();
}
Although I have 3 items in the list (products), I get just one item in productnames list.
Note: I can't use Include because I am using another Core Web API to retrieve the data and
can't use oData to return IQueryable object. So as a workaround I am
using Sqlite for the client application, while the API is using MS
Sql.
You only get one item (which will be the value from the last iteration) because you keep overwriting the value of productnames in each iteration.
You would need to add the result to the list. Assuming your .Where(c => c.ProductId == item.ProductId) returns only one record, then you could use .Single(), for example
foreach (var item in products)
{
productnames.Add(_context.ProductType.Single(c => c.ProductId == item.ProductId));
}
However you can simplify this by using a .Contains() statement
// Select just the ProductId
var products = _context.Product
.Where(x => x.Buyer == "Test" && x.ProductId != null)
.Select(x => x.ProductId);
List<ProductType> productnames = _context.ProductType
.Where(x => products.Contains(x.ProductId)).ToList();
I've seen the scenarios where a ViewModel is populated with one LINQ query as shown below. Question: How can I populate the TotalSale attribute (column) - that is a grand total of Sale column - of the following ViewModel? Note: I'm using latest version of ASP.NET Core with VS2015.
ViewModel:
public class CategProdViewModel
{
public string Category { get; set; }
public string ProductName { get; set; }
public float Sale { get; set; }
public float TotalSale { get; set; }
}
Controller:
var innerJoinQuery = from category in categories
join prod in products on category.ID equals prod.CategoryID
select new CategProdViewModel { Category = category.Name, ProductName = prod.Name, Sale = prod.Sale };
You will have many of these in your view:
public class CategProdViewModel
{
public string Category { get; set; }
public string ProductName { get; set; }
public float Sale { get; set; }
public real TotalSale { get; set; }
}
But you will only have the last property TotalSale once. Therefore, change your view model to this:
public class CategProdViewModel
{
public string Category { get; set; }
public string ProductName { get; set; }
public float Sale { get; set; }
}
Create this model for your view and pass it to your view.
public class UseThisOnYourView // Give it another name
{
public List<CategProdViewModel> Items { get; set; }
public real TotalSale { get { return this.Items.Sum(x => x.Sale); } }
}
Then your query will be like this:
var innerJoinQuery = from category in categories
join prod in products on category.ID equals prod.CategoryID
select new CategProdViewModel { Category = category.Name, ProductName = prod.Name, Sale = prod.Sale };
var model = new UseThisOnYourView
{
Items = innerJoinQuery.ToList()
};
NOTE: Please keep in mind you will need to adjust your view to use the new type you are passing to it and adjust the code accordingly.
You don't clarify the structure of Product, but you can Sum multiple values
var innerJoinQuery = from category in categories
join prod in products on category.ID equals prod.CategoryID
select new CategProdViewModel {
Category = category.Name,
ProductName = prod.Name,
Sale = prod.Sale,
TotalSales = products.Sum(t => t.Sale)
};
Without more about the code, it's hard to say, and also, the prod.sale is probably only going to be the amount of the last product. You can use Sum(this IEnumerable<TSource> source, Func<TSource, double> selector) to sum the total of multiple items.
This is a quick answer. It will probably need to be altered depending on what the rest of your code looks like. You can also Group the query results to sum.
I have a class that holds Categories.
public class CategoryDto
{
public int Id { get; set; }
public int PortfolioId { get; set; }
public string Description { get; set; }
public List<SubCategoryDto> SubCategories { get; set; }
public CategoryDto()
{
SubCategories = new List<SubCategoryDto>();
}
}
It has a List in it, which is a list of SubCategory classes:
public class SubCategoryDto
{
public int Id { get; set; }
public int CategoryId { get; set; }
public CategoryDto Category { get; set; }
public string Description { get; set; }
}
I then populate this item, but I am getting a list bases on a 'PortfolioId'.
var cats = (from p in Context.transaction_category
where p.account_portfolio_id == portfolioId
&& p.deleted == null
select new CategoryDto
{
Id = p.id,
Description = p.description,
PortfolioId = p.account_portfolio_id
}).ToList();
The Categories table has a foreign key to SubCategories. Each category has 0:n sub categories. So, the entity framework model has a Context.transaction_category.transaction_sub_categories collection.
So now what I do is, foreach through the categories in the list above, and populate the sub categories.
Is there a way to do this in the same link statement? The Categories object has a List list. Can it be done in the above Linq statement?
Edit:
This is the fix attempt, as recommended, but is presenting an error:
var cats = (from p in Context.transaction_category
where p.account_portfolio_id == portfolioId
&& p.deleted == null
select new CategoryDto
{
Id = p.id,
Description = p.description,
PortfolioId = p.account_portfolio_id,
SubCategories = (from s in Context.transaction_sub_category where s.transaction_category_id == p.id
&& s.deleted == null
select new SubCategoryDto
{
Id = s.id,
Description = s.description,
CategoryId = s.transaction_category_id
}).ToList()
}).ToList();
LINQ to Entities does not recognize the method
'System.Collections.Generic.List1[Objects.SubCategoryDto]
ToList[SubCategoryDto](System.Collections.Generic.IEnumerable1[Objects.SubCategoryDto])'
method, and this method cannot be translated into a store expression.
You can do it like this:
var cats = (from p in Context.transaction_category
where p.account_portfolio_id == portfolioId
&& p.deleted == null
select new CategoryDto
{
Id = p.id,
Description = p.description,
PortfolioId = p.account_portfolio_id,
SubCategories = (from s in Context.transaction_category.transaction_sub_categories
where s.CategoryId == p.Id
select new SubCategoryDto {
Id = s.Id,
Description = s.Decription
}).ToList()
}).ToList();
Update: To make it easier change your SubCategories and Category properties like this:
public virtual List<SubCategoryDto> SubCategories { get; set; }
public virtual CategoryDto Category { get; set; }
Then you can use include and simply load your sub categories like this:
var cats = Context.transaction_category
.Where(p => p.account_portfolio_id == portfolioId && p.deleted == null)
.Include(p => p.SubCategories);
Given the following database structure
Category
ID
CategoryNameResID
ParentCategory(Optional)
Resources
ID
Text
Lang
And given a ViewModel
public class CategoryViewModel
{
public int ID { get; set; }
public int CategoryNameResID { get; set; }
public string CategoryName { get; set; }
public int ParentCategory { get; set; }
public string ParentCategoryName { get; set; }
}
I want to get the list of all Categories with ParentCategoryName included
What I did so far is:
var categories = (from cat in db.Categories
join res in db.Resources on cat.CategoryNameResID equal res.ID
select new CategoryViewModel{
ID = cat.ID,
CategoryNameResID = cat.CategoryNameResID,
CategoryName = res.Text,
ParentCategory = cat.ParentCategory,
ParentCategoryName = (from p in db.Resources
where p.ID == cat.ParentCategory
select p.Text)
}).ToList();
I can't figure out how to get the ParentCategoryName without having to iterate again, which is definitely wrong.
Try this:
(from cat in cats
join res in resources on cat.ResId equals res.Id let categoryName = res.Text
join cat1 in cats on cat.ParentId equals cat1.Id into parentJoin
from pj in parentJoin.DefaultIfEmpty() let parentCatResId =pj==null?0: pj.ResId
join res1 in resources on parentCatResId equals res1.Id into resJoin
from res2 in resJoin.DefaultIfEmpty() let parentName = (res2==null?string.Empty:res2.Text)
select new CategoryVM
{
Id = cat.Id,
ResId = cat.ResId,
CatName = categoryName,
ParentId = cat.ParentId,
ParentName = parentName
}).ToList();
Say you have the following data with your tables
dbo.Categories
ID CategoryNameResID ParentCategory
1 1 NULL
2 2 NULL
3 3 1
4 4 NULL
5 5 4
6 6 4
7 7 4
dbo.Resources
ID Text Lang
1 Standard en-GB
2 Custom en-GB
3 Standard Oversize en-GB
4 Loose en-GB
5 Loose 2F Set en-GB
6 Loose (4” Scale) en-GB
7 Loose (6” Scale) en-GB
The following LINQ statements will output the desired results:
public class CategoryViewModel
{
public int ID { get; set; }
public int CategoryNameResID { get; set; }
public string CategoryName { get; set; }
public int? ParentCategory { get; set; }
public string ParentCategoryName { get; set; }
}
var categories = (from cat in Categories
join res in Resources on cat.CategoryNameResID equals res.ID let categoryName = res.Text
select new CategoryViewModel
{
ID = cat.ID,
CategoryNameResID = cat.CategoryNameResID,
CategoryName = categoryName,
ParentCategory = cat.ParentCategory,
ParentCategoryName = Resources.FirstOrDefault(r => r.ID == cat.ParentCategory).Text
}).ToList();
foreach(var c in categories)
{
Console.WriteLine(c.CategoryName + " - " + c.ParentCategoryName);
}
// Prints
Standard -
Custom -
Standard Oversize - Standard
Loose -
Loose 2F Set - Loose
Loose (4” Scale) - Loose
Loose (6” Scale) - Loose
I'm trying to work through a problem where I'm mapping EF Entities to POCO which serve as DTO.
I have two tables within my database, say Products and Categories. A Product belongs to one category and one category may contain many Products. My EF entities are named efProduct and efCategory. Within each entity there is the proper Navigation Property between efProduct and efCategory.
My Poco objects are simple
public class Product
{
public string Name { get; set; }
public int ID { get; set; }
public double Price { get; set; }
public Category ProductType { get; set; }
}
public class Category
{
public int ID { get; set; }
public string Name { get; set; }
public List<Product> products { get; set; }
}
To get a list of products I am able to do something like
public IQueryable<Product> GetProducts()
{
return from p in ctx.Products
select new Product
{
ID = p.ID,
Name = p.Name,
Price = p.Price
ProductType = p.Category
};
}
However there is a type mismatch error because p.Category is of type efCategory. How can I resolve this? That is, how can I convert p.Category to type Category?
Likewise when I do
return from c in ctx.Categories
where c.ID == id
select new Category
{
ID = c.ID,
Name = c.Name,
ProductList = c.Products;
};
I get a mismatch because ProductList is of type Product, where c.Products is an EntityCollection
I know in .NET EF has added support for POCO, but I'm forced to use .NET 3.5 SP1.
return from p in ctx.Products
select new Product
{
ID = p.ID,
Name = p.Name,
Price = p.Price
ProductType = new Category
{
ID = p.Category.ID,
Name = p.Category.Name // etc.
}
};
For Category you do:
return from c in ctx.Categories
where c.ID == id
select new Category
{
ID = c.ID,
Name = c.Name,
ProductList = from p in c.Products
select new Product
{
ID = p.ID,
Name = p.Name,
Price = p.Price
}
};