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 /// /// Check the possibility of adding products to the category for the current vendor /// /// Category /// All categories /// A task that represents the asynchronous operation public virtual async Task CanVendorAddProductsAsync(Category category, IList 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); } /// /// 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(await mappings.ToListAsync()); } /// /// 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 }