diff --git a/src/Libraries/Nop.Services/Catalog/CategoryService.cs b/src/Libraries/Nop.Services/Catalog/CategoryService.cs index 01076ac2fff..b3aea00e770 100644 --- a/src/Libraries/Nop.Services/Catalog/CategoryService.cs +++ b/src/Libraries/Nop.Services/Catalog/CategoryService.cs @@ -1,4 +1,4 @@ -using Nop.Core; +using Nop.Core; using Nop.Core.Caching; using Nop.Core.Domain.Catalog; using Nop.Core.Domain.Customers; @@ -236,637 +236,311 @@ public virtual async Task DeleteCategoriesAsync(IList categories) /// 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); - } - - /// - /// Deletes a list of product category mapping - /// - /// Product categories - /// A task that represents the asynchronous operation - public virtual async Task DeleteProductCategoriesAsync(IList productCategories) - { - await _productCategoryRepository.DeleteAsync(productCategories); - } - - /// - /// 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) - { - return source.FirstOrDefault(pc => pc.ProductId == productId && pc.CategoryId == categoryId); - } - - /// - /// 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; - }); - } - - /// - /// Update category store mappings - /// - /// Category - /// A list of store ids for mapping - /// A task that represents the asynchronous operation - public async Task UpdateCategoryStoreMappingsAsync(Category category, IList limitedToStoresIds) - { - category.LimitedToStores = limitedToStoresIds.Any(); - await UpdateCategoryAsync(category); - - var existingStoreMappings = await _storeMappingService.GetStoreMappingsAsync(category); - var allStores = await _storeService.GetAllStoresAsync(); - foreach (var store in allStores) - { - if (limitedToStoresIds.Contains(store.Id)) - { - //new store - if (existingStoreMappings.All(sm => sm.StoreId != store.Id)) - await _storeMappingService.InsertStoreMappingAsync(category, store.Id); - } - else - { - //remove store - var storeMappingToDelete = existingStoreMappings.FirstOrDefault(sm => sm.StoreId == store.Id); - if (storeMappingToDelete != null) - await _storeMappingService.DeleteStoreMappingAsync(storeMappingToDelete); - } - } - } - - #endregion + /// 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. + //Store full Category objects in the lookup to avoid follow-up DB queries in call sites. + var lookup = await _staticCacheManager.GetAsync( + _staticCacheManager.PrepareKeyForDefaultCache(NopCatalogDefaults.ChildCategoryIdLookupCacheKey, storeId, showHidden), + async () => (await GetAllCategoriesAsync(storeId: storeId, showHidden: showHidden)) + .ToGroupedDictionary(c => c.ParentCategoryId, x => x)); + + var categoryIds = new List(); + if (lookup.TryGetValue(parentCategoryId, out var categories)) + { + categoryIds.AddRange(categories.Select(c => c.Id)); + var childCategoryIds = categories.SelectAwait(async c => await GetChildCategoryIdsAsync(c.Id, storeId, showHidden)); + // avoid allocating a new list or blocking with ToEnumerable + await foreach (var cIds in childCategoryIds) + categoryIds.AddRange(cIds); + } + + return categoryIds; + }); + } + + /// + /// 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 HashSet(); + + 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; + }); + } + + /// + /// Update category store mappings + /// + /// Category + /// A list of store ids for mapping + /// A task that represents the asynchronous operation + public async Task UpdateCategoryStoreMappingsAsync(Category category, IList limitedToStoresIds) + { + category.LimitedToStores = limitedToStoresIds.Any(); + await UpdateCategoryAsync(category); + + var existingStoreMappings = await _storeMappingService.GetStoreMappingsAsync(category); + var allStores = await _storeService.GetAllStoresAsync(); + foreach (var store in allStores) + { + if (limitedToStoresIds.Contains(store.Id)) + { + //new store + if (existingStoreMappings.All(sm => sm.StoreId != store.Id)) + await _storeMappingService.InsertStoreMappingAsync(category, store.Id); + } + else + { + //remove store + var storeMappingToDelete = existingStoreMappings.FirstOrDefault(sm => sm.StoreId == store.Id); + if (storeMappingToDelete != null) + await _storeMappingService.DeleteStoreMappingAsync(storeMappingToDelete); + } + } + } + + #endregion } \ No newline at end of file