Repository navigation
Return pages instead of IQueryable from the Business queries - #949
Merged
Merged
Conversation
added 2 commits
October 18, 2026 16:39
PagedAsync now reads the page first and only counts when the page came back full, or empty past the first page. Full loads (PageSize = int.MaxValue) and lookups that find nothing no longer pay for a second query. The skip is computed in 64 bits and clamped, so PageIndex >= 2 with the default int.MaxValue page size no longer overflows to a negative skip.
A merchandise return covering several order items added every item's quantity to each of them, so items with units left were refused a return. Also forward the cancellation token in AffiliateDeleteValidator.
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.
Part of the architecture backlog (item S7:
IQueryableleaking from Business)Type: feature
Issue
Five public mediator queries in
Grand.Business.CorereturnIQueryable<T>:GetOrderQuery,GetCustomerQuery,GetGiftVoucherQuery,GetMerchandiseReturnQueryandGetPaymentTransactionQuery. The caller, not Business, decides how the Mongo query runs:.Any(),.Count(),.FirstOrDefault(),.ToList(),foreach), which blocks a thread-pool thread on database I/O. This happens on the admin dashboard, in the affiliate delete validator, in the product and vendor review checks, in the gift voucher coupon validator and in the customer's downloadable products and returns pages.PageIndex/PageSize, but the handlers ignore them. Paging is left to whoever runs the query, and a LINQ translation error only shows up at runtime in Web.Reproduce:
grep -rn "IQueryable" src/Business/Grand.Business.Core --include=*.cslists the five query classes.Solution
IRepository<T>.PagedAsync(query, request.PageIndex, request.PageSize, cancellationToken). The five queries now returnIPagedList<T>.OrderService.SearchOrders,CustomerService.GetAllCustomers,GiftVoucherService.GetAllGiftVouchers,MerchandiseReturnService.SearchMerchandiseReturnsandPaymentTransactionService.SearchPaymentTransactionsreturn the handler's page. They used to callPagedAsyncthemselves with the same arguments, so their results do not change.PageSize = 1and readTotalCount, the server-side count. These are the admin dashboard counters (abandoned carts, today's registrations, which previously had no page size),AffiliateDeleteValidator, the "confirmed purchase" checks for product and vendor reviews, and the gift voucher code lookup.IsMerchandiseReturnAllowedQueryHandler) keep the default page size and get the list loaded asynchronously.CustomerIdfilter inGetOrderQueryHandler.After this,
grep -rn "IQueryable" src/Business/Grand.Business.Corereturns nothing.Known trade-off: the full-list callers now run one extra count query (
PagedAsynccounts, then loads). On LiteDB both passes scan the collection.Breaking changes
GetOrderQuery,GetCustomerQuery,GetGiftVoucherQuery,GetMerchandiseReturnQueryandGetPaymentTransactionQuerychanges fromIQueryable<T>toIPagedList<T>. External code that sends them and composes further LINQ must instead set the query's filters andPageIndex/PageSize. Code that replaces their handlers must returnIPagedList<T>.PageIndex/PageSizeon these queries are now applied. Senders that relied on them being ignored (for example, settingPageSize = 1and then calling.Count()) now get one page. UseTotalCountinstead.Grand.Module.Apihas its ownGetCustomerQuerytype, which is unchanged.Testing
Automated: the solution builds, and every test project passes. New tests:
GetOrderQueryHandlerTests.Handle_FiltersByCustomerAndPagesandGetCustomerQueryHandlerTests.Handle_PageSizeTwo_ReturnsFirstPageAndTotal.Manual:
develop, including a cart created as a guest just before.develop.