Try your search with a different keyword or use * as a wildcard.
using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Discounts;
using Nop.Data;
using Nop.Services.Customers;
using Nop.Services.Discounts;
using Nop.Services.Localization;
using Nop.Services.Security;
using Nop.Services.Stores;
namespace Nop.Services.Catalog;
/// <summary>
/// Category service
/// </summary>
public partial class CategoryService : ICategoryService
{
#region Fields
protected readonly IAclService _aclService;
protected readonly ICustomerService _customerService;
protected readonly ILocalizationService _localizationService;
protected readonly IRepository<Category> _categoryRepository;
protected readonly IRepository<DiscountCategoryMapping> _discountCategoryMappingRepository;
protected readonly IRepository<Product> _productRepository;
protected readonly IRepository<ProductCategory> _productCategoryRepository;
protected readonly IStaticCacheManager _staticCacheManager;
protected readonly IStoreContext _storeContext;
protected readonly IStoreMappingService _storeMappingService;
protected readonly IWorkContext _workContext;
#endregion
#region Ctor
public CategoryService(IAclService aclService,
ICustomerService customerService,
ILocalizationService localizationService,
IRepository<Category> categoryRepository,
IRepository<DiscountCategoryMapping> discountCategoryMappingRepository,
IRepository<Product> productRepository,
IRepository<ProductCategory> productCategoryRepository,
IStaticCacheManager staticCacheManager,
IStoreContext storeContext,
IStoreMappingService storeMappingService,
IWorkContext workContext)
{
_aclService = aclService;
_customerService = customerService;
_localizationService = localizationService;
_categoryRepository = categoryRepository;
_discountCategoryMappingRepository = discountCategoryMappingRepository;
_productRepository = productRepository;
_productCategoryRepository = productCategoryRepository;
_staticCacheManager = staticCacheManager;
_storeContext = storeContext;
_storeMappingService = storeMappingService;
_workContext = workContext;
}
#endregion
#region Utilities
/// <summary>
/// Gets a product category mapping collection
/// </summary>
/// <param name="productId">Product identifier</param>
/// <param name="storeId">Store identifier (used in multi-store environment). "showHidden" parameter should also be "true"</param>
/// <param name="showHidden"> A value indicating whether to show hidden records</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the product category mapping collection
/// </returns>
protected virtual async Task<IList<ProductCategory>> GetProductCategoriesByProductIdAsync(int productId, int storeId,
bool showHidden = false)
{
if (productId == 0)
return new List<ProductCategory>();
var customer = await _workContext.GetCurrentCustomerAsync();
var customerRoleIds = await _customerService.GetCustomerRoleIdsAsync(customer);
return await _productCategoryRepository.GetAllAsync(async query =>
{
if (!showHidden)
{
var categoriesQuery = _categoryRepository.Table.Where(c => c.Published);
//apply store mapping constraints
categoriesQuery = await _storeMappingService.ApplyStoreMapping(categoriesQuery, storeId);
//apply ACL constraints
categoriesQuery = await _aclService.ApplyAcl(categoriesQuery, customerRoleIds);
query = query.Where(pc => categoriesQuery.Any(c => !c.Deleted && c.Id == pc.CategoryId));
}
return query
.Where(pc => pc.ProductId == productId)
.OrderBy(pc => pc.DisplayOrder)
.ThenBy(pc => pc.Id);
}, cache => _staticCacheManager.PrepareKeyForDefaultCache(NopCatalogDefaults.ProductCategoriesByProductCacheKey,
productId, showHidden, customerRoleIds, storeId));
}
/// <summary>
/// Sort categories for tree representation
/// </summary>
/// <param name="categoriesByParentId">Categories for sort</param>
/// <param name="parentId">Parent category identifier</param>
/// <param name="ignoreCategoriesWithoutExistingParent">A value indicating whether categories without parent category in provided category list (source) should be ignored</param>
/// <returns>
/// An enumerable containing the sorted categories
/// </returns>
protected virtual IEnumerable<Category> SortCategoriesForTree(
ILookup<int, Category> categoriesByParentId,
int parentId = 0,
bool ignoreCategoriesWithoutExistingParent = false)
{
ArgumentNullException.ThrowIfNull(categoriesByParentId);
var remaining = parentId > 0
? new HashSet<int>(0)
: categoriesByParentId.Select(g => g.Key).ToHashSet();
remaining.Remove(parentId);
foreach (var cat in categoriesByParentId[parentId].OrderBy(c => c.DisplayOrder).ThenBy(c => c.Id))
{
yield return cat;
remaining.Remove(cat.Id);
foreach (var subCategory in SortCategoriesForTree(categoriesByParentId, cat.Id, true))
{
yield return subCategory;
remaining.Remove(subCategory.Id);
}
}
if (ignoreCategoriesWithoutExistingParent)
yield break;
//find categories without parent in provided category source and return them
var orphans = remaining
.SelectMany(id => categoriesByParentId[id])
.OrderBy(c => c.ParentCategoryId)
.ThenBy(c => c.DisplayOrder)
.ThenBy(c => c.Id);
foreach (var orphan in orphans)
yield return orphan;
}
#endregion
#region Methods
/// <summary>
/// Check the possibility of adding products to the category for the current vendor
/// </summary>
/// <param name="category">Category</param>
/// <param name="allCategories">All categories</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task<bool> CanVendorAddProductsAsync(Category category, IList<Category> allCategories = null)
{
ArgumentNullException.ThrowIfNull(category);
if (await _workContext.GetCurrentVendorAsync() is null) // check vendors only
return true;
if (category.RestrictFromVendors)
return false;
var breadcrumb = await GetCategoryBreadCrumbAsync(category, allCategories, showHidden: true);
return !breadcrumb.Any(c => c.RestrictFromVendors);
}
/// <summary>
/// Clean up category references for a specified discount
/// </summary>
/// <param name="discount">Discount</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task ClearDiscountCategoryMappingAsync(Discount discount)
{
ArgumentNullException.ThrowIfNull(discount);
var mappings = _discountCategoryMappingRepository.Table.Where(dcm => dcm.DiscountId == discount.Id);
await _discountCategoryMappingRepository.DeleteAsync(await mappings.ToListAsync());
}
/// <summary>
/// Delete category
/// </summary>
/// <param name="category">Category</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task DeleteCategoryAsync(Category category)
{
await _categoryRepository.DeleteAsync(category);
//reset a "Parent category" property of all child subcategories
var subcategories = await GetAllCategoriesByParentCategoryIdAsync(category.Id, true);
foreach (var subcategory in subcategories)
{
subcategory.ParentCategoryId = 0;
await UpdateCategoryAsync(subcategory);
}
}
/// <summary>
/// Delete Categories
/// </summary>
/// <param name="categories">Categories</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task DeleteCategoriesAsync(IList<Category> categories)
{
ArgumentNullException.ThrowIfNull(categories);
foreach (var category in categories)
await DeleteCategoryAsync(category);
}
/// <summary>
/// Gets all categories
/// </summary>
/// <param name="storeId">Store identifier; 0 if you want to get all records</param>
/// <param name="showHidden">A value indicating whether to show hidden records</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the categories
/// </returns>
public virtual async Task<IList<Category>> GetAllCategoriesAsync(int storeId = 0, bool showHidden = false)
{
var key = _staticCacheManager.PrepareKeyForDefaultCache(NopCatalogDefaults.CategoriesAllCacheKey,
storeId,
await _customerService.GetCustomerRoleIdsAsync(await _workContext.GetCurrentCustomerAsync()),
showHidden);
var categories = await _staticCacheManager
.GetAsync(key, async () => (await GetAllCategoriesAsync(string.Empty, storeId, showHidden: showHidden)).ToList());
return categories;
}
/// <summary>
/// Gets all categories
/// </summary>
/// <param name="categoryName">Category name</param>
/// <param name="storeId">Store identifier; 0 if you want to get all records</param>
/// <param name="pageIndex">Page index</param>
/// <param name="pageSize">Page size</param>
/// <param name="showHidden">A value indicating whether to show hidden records</param>
/// <param name="overridePublished">
/// null - process "Published" property according to "showHidden" parameter
/// true - load only "Published" products
/// false - load only "Unpublished" products
/// </param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the categories
/// </returns>
public virtual async Task<IPagedList<Category>> GetAllCategoriesAsync(string categoryName, int storeId = 0,
int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false, bool? overridePublished = null)
{
var unsortedCategories = await _categoryRepository.GetAllAsync(async query =>
{
if (!showHidden)
query = query.Where(c => c.Published);
else if (overridePublished.HasValue)
query = query.Where(c => c.Published == overridePublished.Value);
if (!showHidden || storeId > 0)
{
//apply store mapping constraints
query = await _storeMappingService.ApplyStoreMapping(query, storeId);
}
if (!showHidden)
{
//apply ACL constraints
var customer = await _workContext.GetCurrentCustomerAsync();
query = await _aclService.ApplyAcl(query, customer);
}
if (!string.IsNullOrWhiteSpace(categoryName))
query = query.Where(c => c.Name.Contains(categoryName));
return query.Where(c => !c.Deleted);
});
//sort categories
var sortedCategories = SortCategoriesForTree(unsortedCategories.ToLookup(c => c.ParentCategoryId))
.ToList();
//paging
return new PagedList<Category>(sortedCategories, pageIndex, pageSize);
}
/// <summary>
/// Gets all categories filtered by parent category identifier
/// </summary>
/// <param name="parentCategoryId">Parent category identifier</param>
/// <param name="showHidden">A value indicating whether to show hidden records</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the categories
/// </returns>
public virtual async Task<IList<Category>> GetAllCategoriesByParentCategoryIdAsync(int parentCategoryId,
bool showHidden = false)
{
var store = await _storeContext.GetCurrentStoreAsync();
var customer = await _workContext.GetCurrentCustomerAsync();
var customerRoleIds = await _customerService.GetCustomerRoleIdsAsync(customer);
var categories = await _categoryRepository.GetAllAsync(async query =>
{
if (!showHidden)
{
query = query.Where(c => c.Published);
//apply store mapping constraints
query = await _storeMappingService.ApplyStoreMapping(query, store.Id);
//apply ACL constraints
query = await _aclService.ApplyAcl(query, customerRoleIds);
}
query = query.Where(c => !c.Deleted && c.ParentCategoryId == parentCategoryId);
return query.OrderBy(c => c.DisplayOrder).ThenBy(c => c.Id);
}, cache => cache.PrepareKeyForDefaultCache(NopCatalogDefaults.CategoriesByParentCategoryCacheKey,
parentCategoryId, showHidden, customerRoleIds, store));
return categories;
}
/// <summary>
/// Gets all categories displayed on the home page
/// </summary>
/// <param name="showHidden">A value indicating whether to show hidden records</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the categories
/// </returns>
public virtual async Task<IList<Category>> GetAllCategoriesDisplayedOnHomepageAsync(bool showHidden = false)
{
var categories = await _categoryRepository.GetAllAsync(query =>
{
return from c in query
orderby c.DisplayOrder, c.Id
where c.Published &&
!c.Deleted &&
c.ShowOnHomepage
select c;
}, cache => cache.PrepareKeyForDefaultCache(NopCatalogDefaults.CategoriesHomepageCacheKey));
if (showHidden)
return categories;
var cacheKey = _staticCacheManager.PrepareKeyForDefaultCache(NopCatalogDefaults.CategoriesHomepageWithoutHiddenCacheKey,
await _storeContext.GetCurrentStoreAsync(), await _customerService.GetCustomerRoleIdsAsync(await _workContext.GetCurrentCustomerAsync()));
var result = await _staticCacheManager.GetAsync(cacheKey, async () =>
{
return await categories
.WhereAwait(async c => await _aclService.AuthorizeAsync(c) && await _storeMappingService.AuthorizeAsync(c))
.ToListAsync();
});
return result;
}
/// <summary>
/// Get category identifiers to which a discount is applied
/// </summary>
/// <param name="discount">Discount</param>
/// <param name="customer">Customer</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the category identifiers
/// </returns>
public virtual async Task<IList<int>> GetAppliedCategoryIdsAsync(Discount discount, Customer customer)
{
ArgumentNullException.ThrowIfNull(discount);
var store = await _storeContext.GetCurrentStoreAsync();
var cacheKey = _staticCacheManager.PrepareKeyForDefaultCache(NopDiscountDefaults.CategoryIdsByDiscountCacheKey,
discount,
await _customerService.GetCustomerRoleIdsAsync(customer),
store);
var result = await _staticCacheManager.GetAsync(cacheKey, async () =>
{
var ids = await _discountCategoryMappingRepository.Table
.Where(dmm => dmm.DiscountId == discount.Id).Select(dmm => dmm.EntityId)
.Distinct()
.ToListAsync();
if (!discount.AppliedToSubCategories)
return ids;
ids.AddRange(await ids.SelectManyAwait(async categoryId =>
await GetChildCategoryIdsAsync(categoryId, store.Id))
.ToListAsync());
return ids.Distinct().ToList();
});
return result;
}
/// <summary>
/// Gets child category identifiers
/// </summary>
/// <param name="parentCategoryId">Parent category identifier</param>
/// <param name="storeId">Store identifier; 0 if you want to get all records</param>
/// <param name="showHidden">A value indicating whether to show hidden records</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the category identifiers
/// </returns>
public virtual async Task<IList<int>> GetChildCategoryIdsAsync(int parentCategoryId, int storeId = 0, bool showHidden = false)
{
var cacheKey = _staticCacheManager.PrepareKeyForDefaultCache(NopCatalogDefaults.CategoriesChildIdsCacheKey,
parentCategoryId,
await _customerService.GetCustomerRoleIdsAsync(await _workContext.GetCurrentCustomerAsync()),
storeId,
showHidden);
return await _staticCacheManager.GetAsync(cacheKey, async () =>
{
//little hack for performance optimization
//there's no need to invoke "GetAllCategoriesByParentCategoryId" multiple times (extra SQL commands) to load childs
//so we load all categories at once (we know they are cached) and process them server-side
var lookup = await _staticCacheManager.GetAsync(
_staticCacheManager.PrepareKeyForDefaultCache(NopCatalogDefaults.ChildCategoryIdLookupCacheKey, storeId, showHidden),
async () => (await GetAllCategoriesAsync(storeId: storeId, showHidden: showHidden))
.ToGroupedDictionary(c => c.ParentCategoryId, x => x.Id));
var categoryIds = new List<int>();
if (lookup.TryGetValue(parentCategoryId, out var categories))
{
categoryIds.AddRange(categories);
var childCategoryIds = categories.SelectAwait(async cId => await GetChildCategoryIdsAsync(cId, storeId, showHidden));
// avoid allocating a new list or blocking with ToEnumerable
await foreach (var cIds in childCategoryIds)
categoryIds.AddRange(cIds);
}
return categoryIds;
});
}
/// <summary>
/// Gets a category
/// </summary>
/// <param name="categoryId">Category identifier</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the category
/// </returns>
public virtual async Task<Category> GetCategoryByIdAsync(int categoryId)
{
var category = await _categoryRepository.GetByIdAsync(categoryId, cache => default);
return category;
}
/// <summary>
/// Get categories for which a discount is applied
/// </summary>
/// <param name="discountId">Discount identifier; pass null to load all records</param>
/// <param name="showHidden">A value indicating whether to load deleted categories</param>
/// <param name="pageIndex">Page index</param>
/// <param name="pageSize">Page size</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the list of categories
/// </returns>
public virtual async Task<IPagedList<Category>> GetCategoriesByAppliedDiscountAsync(int? discountId = null,
bool showHidden = false, int pageIndex = 0, int pageSize = int.MaxValue)
{
var categories = _categoryRepository.Table;
if (discountId.HasValue)
categories = from category in categories
join dcm in _discountCategoryMappingRepository.Table on category.Id equals dcm.EntityId
where dcm.DiscountId == discountId.Value
select category;
if (!showHidden)
categories = categories.Where(category => !category.Deleted);
categories = categories.OrderBy(category => category.DisplayOrder).ThenBy(category => category.Id);
return await categories.ToPagedListAsync(pageIndex, pageSize);
}
/// <summary>
/// Inserts category
/// </summary>
/// <param name="category">Category</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task InsertCategoryAsync(Category category)
{
await _categoryRepository.InsertAsync(category);
}
/// <summary>
/// Get a value indicating whether discount is applied to category
/// </summary>
/// <param name="categoryId">Category identifier</param>
/// <param name="discountId">Discount identifier</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the result
/// </returns>
public virtual async Task<DiscountCategoryMapping> GetDiscountAppliedToCategoryAsync(int categoryId, int discountId)
{
return await _discountCategoryMappingRepository.Table
.FirstOrDefaultAsync(dcm => dcm.EntityId == categoryId && dcm.DiscountId == discountId);
}
/// <summary>
/// Inserts a discount-category mapping record
/// </summary>
/// <param name="discountCategoryMapping">Discount-category mapping</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task InsertDiscountCategoryMappingAsync(DiscountCategoryMapping discountCategoryMapping)
{
await _discountCategoryMappingRepository.InsertAsync(discountCategoryMapping);
}
/// <summary>
/// Deletes a discount-category mapping record
/// </summary>
/// <param name="discountCategoryMapping">Discount-category mapping</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task DeleteDiscountCategoryMappingAsync(DiscountCategoryMapping discountCategoryMapping)
{
await _discountCategoryMappingRepository.DeleteAsync(discountCategoryMapping);
}
/// <summary>
/// Updates the category
/// </summary>
/// <param name="category">Category</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task UpdateCategoryAsync(Category category)
{
ArgumentNullException.ThrowIfNull(category);
//validate category hierarchy
var parentCategory = await GetCategoryByIdAsync(category.ParentCategoryId);
while (parentCategory != null)
{
if (category.Id == parentCategory.Id)
{
category.ParentCategoryId = 0;
break;
}
parentCategory = await GetCategoryByIdAsync(parentCategory.ParentCategoryId);
}
await _categoryRepository.UpdateAsync(category);
}
/// <summary>
/// Deletes a product category mapping
/// </summary>
/// <param name="productCategory">Product category</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task DeleteProductCategoryAsync(ProductCategory productCategory)
{
await _productCategoryRepository.DeleteAsync(productCategory);
}
/// <summary>
/// Deletes a list of product category mapping
/// </summary>
/// <param name="productCategories">Product categories</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task DeleteProductCategoriesAsync(IList<ProductCategory> productCategories)
{
await _productCategoryRepository.DeleteAsync(productCategories);
}
/// <summary>
/// Gets product category mapping collection
/// </summary>
/// <param name="categoryId">Category identifier</param>
/// <param name="pageIndex">Page index</param>
/// <param name="pageSize">Page size</param>
/// <param name="showHidden">A value indicating whether to show hidden records</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the product a category mapping collection
/// </returns>
public virtual async Task<IPagedList<ProductCategory>> GetProductCategoriesByCategoryIdAsync(int categoryId,
int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false)
{
if (categoryId == 0)
return new PagedList<ProductCategory>(new List<ProductCategory>(), pageIndex, pageSize);
var query = from pc in _productCategoryRepository.Table
join p in _productRepository.Table on pc.ProductId equals p.Id
where pc.CategoryId == categoryId && !p.Deleted
orderby pc.DisplayOrder, pc.Id
select pc;
if (!showHidden)
{
var categoriesQuery = _categoryRepository.Table.Where(c => c.Published);
//apply store mapping constraints
var store = await _storeContext.GetCurrentStoreAsync();
categoriesQuery = await _storeMappingService.ApplyStoreMapping(categoriesQuery, store.Id);
//apply ACL constraints
var customer = await _workContext.GetCurrentCustomerAsync();
categoriesQuery = await _aclService.ApplyAcl(categoriesQuery, customer);
query = query.Where(pc => categoriesQuery.Any(c => c.Id == pc.CategoryId));
}
return await query.ToPagedListAsync(pageIndex, pageSize);
}
/// <summary>
/// Gets a product category mapping collection
/// </summary>
/// <param name="productId">Product identifier</param>
/// <param name="showHidden"> A value indicating whether to show hidden records</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the product category mapping collection
/// </returns>
public virtual async Task<IList<ProductCategory>> GetProductCategoriesByProductIdAsync(int productId, bool showHidden = false)
{
var store = await _storeContext.GetCurrentStoreAsync();
return await GetProductCategoriesByProductIdAsync(productId, store.Id, showHidden);
}
/// <summary>
/// Gets a product category mapping
/// </summary>
/// <param name="productCategoryId">Product category mapping identifier</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the product category mapping
/// </returns>
public virtual async Task<ProductCategory> GetProductCategoryByIdAsync(int productCategoryId)
{
return await _productCategoryRepository.GetByIdAsync(productCategoryId, cache => default);
}
/// <summary>
/// Inserts a product category mapping
/// </summary>
/// <param name="productCategory">>Product category mapping</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task InsertProductCategoryAsync(ProductCategory productCategory)
{
await _productCategoryRepository.InsertAsync(productCategory);
}
/// <summary>
/// Updates the product category mapping
/// </summary>
/// <param name="productCategory">>Product category mapping</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task UpdateProductCategoryAsync(ProductCategory productCategory)
{
await _productCategoryRepository.UpdateAsync(productCategory);
}
/// <summary>
/// Returns a list of names of not existing categories
/// </summary>
/// <param name="categoryIdsNames">The names and/or IDs of the categories to check</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the list of names and/or IDs not existing categories
/// </returns>
public virtual async Task<string[]> GetNotExistingCategoriesAsync(string[] categoryIdsNames)
{
ArgumentNullException.ThrowIfNull(categoryIdsNames);
var query = _categoryRepository.Table.Where(c => !c.Deleted);
var queryFilter = categoryIdsNames.Distinct().ToArray();
//filtering by name
var filter = await query.Select(c => c.Name)
.Where(c => queryFilter.Contains(c))
.ToListAsync();
queryFilter = queryFilter.Except(filter).ToArray();
//if some names not found
if (!queryFilter.Any())
return queryFilter.ToArray();
//filtering by IDs
filter = await query.Select(c => c.Id.ToString())
.Where(c => queryFilter.Contains(c))
.ToListAsync();
return queryFilter.Except(filter).ToArray();
}
/// <summary>
/// Get category IDs for products
/// </summary>
/// <param name="productIds">Products IDs</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the category IDs for products
/// </returns>
public virtual async Task<IDictionary<int, int[]>> GetProductCategoryIdsAsync(int[] productIds)
{
var query = _productCategoryRepository.Table;
return (await query.Where(p => productIds.Contains(p.ProductId))
.Select(p => new { p.ProductId, p.CategoryId })
.ToListAsync())
.GroupBy(a => a.ProductId)
.ToDictionary(items => items.Key, items => items.Select(a => a.CategoryId).ToArray());
}
/// <summary>
/// Gets categories by identifier
/// </summary>
/// <param name="categoryIds">Category identifiers</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the categories
/// </returns>
public virtual async Task<IList<Category>> GetCategoriesByIdsAsync(int[] categoryIds)
{
return await _categoryRepository.GetByIdsAsync(categoryIds, includeDeleted: false);
}
/// <summary>
/// Returns a ProductCategory that has the specified values
/// </summary>
/// <param name="source">Source</param>
/// <param name="productId">Product identifier</param>
/// <param name="categoryId">Category identifier</param>
/// <returns>A ProductCategory that has the specified values; otherwise null</returns>
public virtual ProductCategory FindProductCategory(IList<ProductCategory> source, int productId, int categoryId)
{
return source.FirstOrDefault(pc => pc.ProductId == productId && pc.CategoryId == categoryId);
}
/// <summary>
/// Get formatted category breadcrumb
/// Note: ACL and store mapping is ignored
/// </summary>
/// <param name="category">Category</param>
/// <param name="allCategories">All categories</param>
/// <param name="separator">Separator</param>
/// <param name="languageId">Language identifier for localization</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the formatted breadcrumb
/// </returns>
public virtual async Task<string> GetFormattedBreadCrumbAsync(Category category, IList<Category> allCategories = null,
string separator = ">>", int languageId = 0)
{
var result = string.Empty;
var breadcrumb = await GetCategoryBreadCrumbAsync(category, allCategories, true);
for (var i = 0; i <= breadcrumb.Count - 1; i++)
{
var categoryName = await _localizationService.GetLocalizedAsync(breadcrumb[i], x => x.Name, languageId);
result = string.IsNullOrEmpty(result) ? categoryName : $"{result} {separator} {categoryName}";
}
return result;
}
/// <summary>
/// Get category breadcrumb
/// </summary>
/// <param name="category">Category</param>
/// <param name="allCategories">All categories</param>
/// <param name="showHidden">A value indicating whether to load hidden records</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the category breadcrumb
/// </returns>
public virtual async Task<IList<Category>> GetCategoryBreadCrumbAsync(Category category, IList<Category> allCategories = null, bool showHidden = false)
{
ArgumentNullException.ThrowIfNull(category);
var breadcrumbCacheKey = _staticCacheManager.PrepareKeyForDefaultCache(NopCatalogDefaults.CategoryBreadcrumbCacheKey,
category,
await _customerService.GetCustomerRoleIdsAsync(await _workContext.GetCurrentCustomerAsync()),
await _storeContext.GetCurrentStoreAsync(),
await _workContext.GetWorkingLanguageAsync(),
showHidden);
return await _staticCacheManager.GetAsync(breadcrumbCacheKey, async () =>
{
//use a local variable, so we don't mutate the parameter captured by the closure
var currentCategory = category;
//index all categories once (provided list or fetched), keep first per id
var allCategoriesById = (allCategories ?? await GetAllCategoriesAsync(showHidden: showHidden))
.DistinctBy(c => c.Id)
.ToDictionary(c => c.Id);
var result = new List<Category>();
//used to prevent circular references (HashSet ? O(1) lookups)
var alreadyProcessedCategoryIds = new HashSet<int>();
while (currentCategory != null && //not null
!currentCategory.Deleted && //not deleted
(showHidden || currentCategory.Published) && //published
!alreadyProcessedCategoryIds.Contains(currentCategory.Id) && //prevent circular references
(showHidden || await _aclService.AuthorizeAsync(currentCategory)) && //ACL
(showHidden || await _storeMappingService.AuthorizeAsync(currentCategory))) //store mapping
{
result.Add(currentCategory);
alreadyProcessedCategoryIds.Add(currentCategory.Id);
//move to parent using the pre-indexed map
allCategoriesById.TryGetValue(currentCategory.ParentCategoryId, out currentCategory);
}
result.Reverse();
return result;
});
}
#endregion
}