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;
///
/// Category service
///
public partial class CategoryService : ICategoryService
{
#region Fields
protected readonly IAclService _aclService;
protected readonly ICustomerService _customerService;
protected readonly ILocalizationService _localizationService;
protected readonly IRepository _categoryRepository;
protected readonly IRepository _discountCategoryMappingRepository;
protected readonly IRepository _productRepository;
protected readonly IRepository _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 categoryRepository,
IRepository discountCategoryMappingRepository,
IRepository productRepository,
IRepository 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
///
/// Gets a product category mapping collection
///
/// Product identifier
/// Store identifier (used in multi-store environment). "showHidden" parameter should also be "true"
/// A value indicating whether to show hidden records
///
/// A task that represents the asynchronous operation
/// The task result contains the product category mapping collection
///
protected virtual async Task> GetProductCategoriesByProductIdAsync(int productId, int storeId,
bool showHidden = false)
{
if (productId == 0)
return new List();
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));
}
///
/// Sort categories for tree representation
///
/// Categories for sort
/// Parent category identifier
/// A value indicating whether categories without parent category in provided category list (source) should be ignored
///
/// An enumerable containing the sorted categories
///
protected virtual IEnumerable SortCategoriesForTree(
ILookup categoriesByParentId,
int parentId = 0,
bool ignoreCategoriesWithoutExistingParent = false)
{
ArgumentNullException.ThrowIfNull(categoriesByParentId);
var remaining = parentId > 0
? new HashSet(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
///
/// Clean up category references for a specified discount
///
/// Discount
/// A task that represents the asynchronous operation
public virtual async Task ClearDiscountCategoryMappingAsync(Discount discount)
{
ArgumentNullException.ThrowIfNull(discount);
var mappings = _discountCategoryMappingRepository.Table.Where(dcm => dcm.DiscountId == discount.Id);
await _discountCategoryMappingRepository.DeleteAsync(mappings.ToList());
}
///
/// Delete category
///
/// Category
/// A task that represents the asynchronous operation
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);
}
}
///
/// Delete Categories
///
/// Categories
/// A task that represents the asynchronous operation
public virtual async Task DeleteCategoriesAsync(IList categories)
{
ArgumentNullException.ThrowIfNull(categories);
foreach (var category in categories)
await DeleteCategoryAsync(category);
}
///
/// Gets all categories
///
/// Store identifier; 0 if you want to get all records
/// A value indicating whether to show hidden records
///
/// A task that represents the asynchronous operation
/// The task result contains the categories
///
public virtual async Task> 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;
}
///
/// Gets all categories
///
/// Category name
/// Store identifier; 0 if you want to get all records
/// Page index
/// Page size
/// A value indicating whether to show hidden records
///
/// null - process "Published" property according to "showHidden" parameter
/// true - load only "Published" products
/// false - load only "Unpublished" products
///
///
/// A task that represents the asynchronous operation
/// The task result contains the categories
///
public virtual async Task> 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(sortedCategories, pageIndex, pageSize);
}
///
/// Gets all categories filtered by parent category identifier
///
/// Parent category identifier
/// A value indicating whether to show hidden records
///
/// A task that represents the asynchronous operation
/// The task result contains the categories
///
public virtual async Task> 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;
}
///
/// Gets all categories displayed on the home page
///
/// A value indicating whether to show hidden records
///
/// A task that represents the asynchronous operation
/// The task result contains the categories
///
public virtual async Task> 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;
}
///
/// Get category identifiers to which a discount is applied
///
/// Discount
/// Customer
///
/// A task that represents the asynchronous operation
/// The task result contains the category identifiers
///
public virtual async Task> 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;
}
///
/// Gets child category identifiers
///
/// Parent category identifier
/// Store identifier; 0 if you want to get all records
/// A value indicating whether to show hidden records
///
/// A task that represents the asynchronous operation
/// The task result contains the category identifiers
///
public virtual async Task> 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();
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;
});
}
///
/// Gets a category
///
/// Category identifier
///
/// A task that represents the asynchronous operation
/// The task result contains the category
///
public virtual async Task GetCategoryByIdAsync(int categoryId)
{
return await _categoryRepository.GetByIdAsync(categoryId, cache => default);
}
///
/// Get categories for which a discount is applied
///
/// Discount identifier; pass null to load all records
/// A value indicating whether to load deleted categories
/// Page index
/// Page size
///
/// A task that represents the asynchronous operation
/// The task result contains the list of categories
///
public virtual async Task> 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);
}
///
/// Inserts category
///
/// Category
/// A task that represents the asynchronous operation
public virtual async Task InsertCategoryAsync(Category category)
{
await _categoryRepository.InsertAsync(category);
}
///
/// Get a value indicating whether discount is applied to category
///
/// Category identifier
/// Discount identifier
///
/// A task that represents the asynchronous operation
/// The task result contains the result
///
public virtual async Task GetDiscountAppliedToCategoryAsync(int categoryId, int discountId)
{
return await _discountCategoryMappingRepository.Table
.FirstOrDefaultAsync(dcm => dcm.EntityId == categoryId && dcm.DiscountId == discountId);
}
///
/// Inserts a discount-category mapping record
///
/// Discount-category mapping
/// A task that represents the asynchronous operation
public virtual async Task InsertDiscountCategoryMappingAsync(DiscountCategoryMapping discountCategoryMapping)
{
await _discountCategoryMappingRepository.InsertAsync(discountCategoryMapping);
}
///
/// Deletes a discount-category mapping record
///
/// Discount-category mapping
/// A task that represents the asynchronous operation
public virtual async Task DeleteDiscountCategoryMappingAsync(DiscountCategoryMapping discountCategoryMapping)
{
await _discountCategoryMappingRepository.DeleteAsync(discountCategoryMapping);
}
///
/// Updates the category
///
/// Category
/// A task that represents the asynchronous operation
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);
}
///
/// Deletes a product category mapping
///
/// Product category
/// A task that represents the asynchronous operation
public virtual async Task DeleteProductCategoryAsync(ProductCategory productCategory)
{
await _productCategoryRepository.DeleteAsync(productCategory);
}
///
/// Gets product category mapping collection
///
/// Category identifier
/// Page index
/// Page size
/// A value indicating whether to show hidden records
///
/// A task that represents the asynchronous operation
/// The task result contains the product a category mapping collection
///
public virtual async Task> GetProductCategoriesByCategoryIdAsync(int categoryId,
int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false)
{
if (categoryId == 0)
return new PagedList(new List(), 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);
}
///
/// Gets a product category mapping collection
///
/// Product identifier
/// A value indicating whether to show hidden records
///
/// A task that represents the asynchronous operation
/// The task result contains the product category mapping collection
///
public virtual async Task> GetProductCategoriesByProductIdAsync(int productId, bool showHidden = false)
{
var store = await _storeContext.GetCurrentStoreAsync();
return await GetProductCategoriesByProductIdAsync(productId, store.Id, showHidden);
}
///
/// Gets a product category mapping
///
/// Product category mapping identifier
///
/// A task that represents the asynchronous operation
/// The task result contains the product category mapping
///
public virtual async Task GetProductCategoryByIdAsync(int productCategoryId)
{
return await _productCategoryRepository.GetByIdAsync(productCategoryId, cache => default);
}
///
/// Inserts a product category mapping
///
/// >Product category mapping
/// A task that represents the asynchronous operation
public virtual async Task InsertProductCategoryAsync(ProductCategory productCategory)
{
await _productCategoryRepository.InsertAsync(productCategory);
}
///
/// Updates the product category mapping
///
/// >Product category mapping
/// A task that represents the asynchronous operation
public virtual async Task UpdateProductCategoryAsync(ProductCategory productCategory)
{
await _productCategoryRepository.UpdateAsync(productCategory);
}
///
/// Returns a list of names of not existing categories
///
/// The names and/or IDs of the categories to check
///
/// A task that represents the asynchronous operation
/// The task result contains the list of names and/or IDs not existing categories
///
public virtual async Task 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();
}
///
/// Get category IDs for products
///
/// Products IDs
///
/// A task that represents the asynchronous operation
/// The task result contains the category IDs for products
///
public virtual async Task> 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());
}
///
/// Gets categories by identifier
///
/// Category identifiers
///
/// A task that represents the asynchronous operation
/// The task result contains the categories
///
public virtual async Task> GetCategoriesByIdsAsync(int[] categoryIds)
{
return await _categoryRepository.GetByIdsAsync(categoryIds, includeDeleted: false);
}
///
/// Returns a ProductCategory that has the specified values
///
/// Source
/// Product identifier
/// Category identifier
/// A ProductCategory that has the specified values; otherwise null
public virtual ProductCategory FindProductCategory(IList source, int productId, int categoryId)
{
foreach (var productCategory in source)
if (productCategory.ProductId == productId && productCategory.CategoryId == categoryId)
return productCategory;
return null;
}
///
/// Get formatted category breadcrumb
/// Note: ACL and store mapping is ignored
///
/// Category
/// All categories
/// Separator
/// Language identifier for localization
///
/// A task that represents the asynchronous operation
/// The task result contains the formatted breadcrumb
///
public virtual async Task GetFormattedBreadCrumbAsync(Category category, IList 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;
}
///
/// Get category breadcrumb
///
/// Category
/// All categories
/// A value indicating whether to load hidden records
///
/// A task that represents the asynchronous operation
/// The task result contains the category breadcrumb
///
public virtual async Task> GetCategoryBreadCrumbAsync(Category category, IList 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 () =>
{
var result = new List();
//used to prevent circular references
var alreadyProcessedCategoryIds = new List();
while (category != null && //not null
!category.Deleted && //not deleted
(showHidden || category.Published) && //published
(showHidden || await _aclService.AuthorizeAsync(category)) && //ACL
(showHidden || await _storeMappingService.AuthorizeAsync(category)) && //Store mapping
!alreadyProcessedCategoryIds.Contains(category.Id)) //prevent circular references
{
result.Add(category);
alreadyProcessedCategoryIds.Add(category.Id);
category = allCategories != null
? allCategories.FirstOrDefault(c => c.Id == category.ParentCategoryId)
: await GetCategoryByIdAsync(category.ParentCategoryId);
}
result.Reverse();
return result;
});
}
#endregion
}