Repository navigation
Pass customer and store explicitly to catalog read services - #930
Merged
KrzysztofPajak merged 9 commits intoOct 16, 2026
Merged
Conversation
added 6 commits
October 16, 2026 16:00
The handlers cache the ACL-filtered result under keys shared by many customers. A query without a customer would authorize nothing and cache the empty list for all of them. Also pins DeleteCategory detaching its children and restores a negative assertion to any customer and store.
added 3 commits
October 16, 2026 16:38
…stomer for hidden records GetPersonalizedProductsQuery and GetSuggestedProductsQuery read the customer id and tags from Customer instead of separate fields that could disagree with it. The product mapping and child category lookups no longer read the customer or store when showHidden is set, so admin callers pass null. Methods that authorize through IAclService throw on a missing customer instead of returning nothing.
The catalog read services took a customer, a store id and a showHidden flag whose meaning depended on each other: the customer had to be null exactly when hidden records were asked for. CatalogAccess.For(customer, storeId) and CatalogAccess.IncludingHidden(storeId) make that choice one argument. The store limitation now applies whenever a store id is set, so the back office passes IncludingHidden() where it ignored the store before. The storefront builds its access from the request with IContextAccessor.CatalogAccess().
This was referenced Oct 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type: feature
Issue
Six Catalog read services (
BrandService,CategoryService,ProductCategoryService,CollectionService,ProductCollectionService,ProductService) injectedIContextAccessorand filtered byWorkContext.CurrentCustomer/StoreContext.CurrentStore. What they returned depended on who called them. Outside an HTTP request (scheduled tasks, the API module, background mediator handlers) the customer and store were wrong or missing. This class of bug caused #860 (sitemap) and #878 (scheduler work context).Solution
The six services no longer depend on
IContextAccessor. Every read that filtered by the customer and store now gets them from its caller.CatalogAccessThe reads that also offered a
showHiddenflag take oneCatalogAccessargument (Grand.Business.Core.Utilities.Catalog) instead of a customer, a store id and the flag. Before, the customer had to be null exactly when hidden records were asked for, and only a comment said so. Now the caller states which view it wants:CatalogAccess.For(customer, storeId): the published records the customer's groups may see in the store.Forrejects a null customer. The storefront builds it from the request withIContextAccessor.CatalogAccess()(Grand.Web.Common.Extensions).CatalogAccess.IncludingHidden(storeId = ""): every record, unpublished and group-limited ones included, for the admin panels, import, export and the sitemap. A store id still leaves out the records limited to other stores, which is how the store panel and the sitemap XML scope their lists.CatalogAccessis a record: two accesses with the same customer and store are equal, so tests can match it directly.Methods that take it:
IBrandService.GetAllBrands(brandName, access, pageIndex, pageSize),ICollectionService.GetAllCollections(collectionName, access, ...),ICategoryService.GetAllCategories(parentId, categoryName, access, ...).ICategoryService.GetAllCategoriesByParentCategoryId(access, parentCategoryId, includeAllLevels),GetAllCategoriesDisplayedOnHomePage(access),GetAllCategoriesFeaturedProductsOnHomePage(access)and bothGetCategoryBreadCrumboverloads.ICollectionService.GetAllCollectionFeaturedProductsOnHomePage(access).IProductCategoryService.GetProductCategoriesByCategoryId(categoryId, access, ...)andIProductCollectionService.GetProductCollectionsByCollectionId(collectionId, access, ...).IProductService.GetProductsByIds(ids, access),SearchProducts(access, ...)(itsstoreIdandshowHiddenparameters are gone) andGetAssociatedProducts(parentId, access, vendorId).Storefront-only reads
The reads that never show hidden records take the customer and the store explicitly:
ICategoryService.GetMenuCategories(customer, storeId)andGetAllCategoriesSearchBox(customer, storeId),IProductService.GetCrossSellProductsByShoppingCart(cart, number, customer, storeId)andIRecentlyViewedProductsService.GetRecentlyViewedProducts(customer, storeId, number).Queries
GetPersonalizedProductsQuery,GetRecommendedProductsQueryandGetSuggestedProductsQuerycarryCustomerandStoreId. Their handlers take the customer id, groups and tags fromCustomer, so the fields that duplicated it are gone.Behavior
One rule now decides the store limitation: it applies whenever the access has a store id. Before, some methods ignored the store for hidden records and others did not. Every back-office caller of the former passes
IncludingHidden()without a store, so what each screen returns is unchanged. Other details:GetSearchProductsQueryHandlerreads the customer's groups only when the group filter applies. Before, it read them even withShowHidden.GetAllCategoriesByParentCategoryId,GetProductCategoriesByCategoryIdandGetProductCollectionsByCollectionIdleave the customer's groups out of the cache key for hidden records, because they do not filter them.CategoryService.DeleteCategoryreads the child categories straight from the repository. Before, it used the cached lookup, which keys on the current customer's groups and store.IAclService.Authorizealready did. Every storefront caller passes the request's store.Not in this PR (later M1 steps):
PricingService,TaxService,PriceFormatterandProductAttributeFormatter. They also read the working currency, language and host.Breaking changes
CatalogAccess(or the customer and store for the storefront-only reads) and must be rebuilt.IRecentlyViewedProductsService.GetRecentlyViewedProductstakes theCustomerinstead of its id.GetPersonalizedProductsQuery,GetRecommendedProductsQueryandGetSuggestedProductsQuerynow requireCustomer, and their handlers throwArgumentNullExceptionwithout one. Their results are cached under keys shared by many customers, so a query without a customer must not fill that entry with an empty list. A plugin or theme that sends these queries has to setCustomer(andStoreId).GetPersonalizedProductsQuery.CustomerId,GetRecommendedProductsQuery.CustomerGroupIdsandGetSuggestedProductsQuery.CustomerTagIdsare removed.IContextAccessor. This affects code that constructs or subclasses them.BaseSearchController(and the Admin, Store and VendorSearchControllers), AdminProductTagsController, and theBlogPostProducts,HomePageProducts,RecommendedProducts,RelatedProductsandSimilarProductsview components now takeIContextAccessor.Testing
dotnet build GrandNode.slnxdotnet test GrandNode.slnx: all green.Grand.Business.Customers.Testshad two failures only in the parallel full run and passes when run alone.CatalogAccessTestsandCatalogAccessExtensionsTestscover the type:Forrejects a null customer,IncludingHiddenshows hidden records of any store by default, and equality compares the customer and the store.BrandServiceTests.GetAllBrands_FiltersByGroupsOfGivenCustomer,CollectionServiceTests.GetAllCollectionFeaturedProductsOnHomePage_AuthorizesAgainstGivenCustomerAndStore,CategoryServiceDbTests.GetMenuCategories_UsesGivenCustomerAndStore/GetCategoryBreadCrumb_StopsAtCategoryHiddenFromGivenCustomer/GetAllCategoriesByParentCategoryId_ShowHiddenWithoutCustomer_ReturnsHiddenChildren/GetAllCategoriesSearchBox_WithoutCustomer_Throws/DeleteCategory_DetachesItsChildCategories,ProductCategoryServiceTests.GetProductCategoriesByCategoryId_FiltersByGivenStore/GetProductCategoriesByCategoryId_ShowHiddenWithoutCustomer_ReturnsLimitedProducts,ProductCollectionServiceTests.GetProductCollectionsByCollectionId_FiltersByGroupsOfGivenCustomer,ProductServiceTests.GetProductsByIds_AuthorizesAgainstGivenCustomerAndStore/GetProductsByIds_IncludingHidden_ReturnsUnpublishedAndGroupLimitedProducts/GetProductsByIds_WithoutAccess_Throws/SearchProducts_SendsGivenCustomerInQuery,GetSearchProductsQueryHandlerTests.Handle_WhenShowHiddenWithoutCustomer_DoesNotFilterByGroups,Get{Personalized,Recommended,Suggested}ProductsQueryHandlerTests.Handle_WithoutCustomer_ThrowsBeforeCaching,GetRecommendedProductsQueryHandlerTests.Handle_AuthorizesProductsAgainstCustomerAndStoreOfQuery,GetPersonalizedProductsQueryHandlerTests.Handle_KeysAndLoadsByIdOfQueryCustomer,GetSuggestedProductsQueryHandlerTests.Handle_KeysAndFiltersByTagsOfQueryCustomer.CatalogAccess, so they still check them.CategoryServiceCacheKeyTestsstill pins the category tree cache key.Not done: a manual pass through the storefront and the admin panel.