Skip to content

Return pages instead of IQueryable from the Business queries - #949

Merged
KrzysztofPajak merged 4 commits into
developfrom
refactor/business-quaryable
Oct 18, 2026
Merged

KrzysztofPajak merged 4 commits into
developfrom
refactor/business-quaryable

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Part of the architecture backlog (item S7: IQueryable leaking from Business)
Type: feature

Issue

Five public mediator queries in Grand.Business.Core return IQueryable<T>: GetOrderQuery, GetCustomerQuery, GetGiftVoucherQuery, GetMerchandiseReturnQuery and GetPaymentTransactionQuery. The caller, not Business, decides how the Mongo query runs:

  • Every caller outside the services enumerates the cursor synchronously (.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.
  • The queries carry 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=*.cs lists the five query classes.

Solution

  • Each handler keeps its filters and ordering and ends with IRepository<T>.PagedAsync(query, request.PageIndex, request.PageSize, cancellationToken). The five queries now return IPagedList<T>.
  • OrderService.SearchOrders, CustomerService.GetAllCustomers, GiftVoucherService.GetAllGiftVouchers, MerchandiseReturnService.SearchMerchandiseReturns and PaymentTransactionService.SearchPaymentTransactions return the handler's page. They used to call PagedAsync themselves with the same arguments, so their results do not change.
  • Callers that only count or check existence send PageSize = 1 and read TotalCount, 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.
  • Callers that need every row (downloadable products, the customer's merchandise returns, IsMerchandiseReturnAllowedQueryHandler) keep the default page size and get the list loaded asynchronously.
  • Removed a duplicated CustomerId filter in GetOrderQueryHandler.

After this, grep -rn "IQueryable" src/Business/Grand.Business.Core returns nothing.

Known trade-off: the full-list callers now run one extra count query (PagedAsync counts, then loads). On LiteDB both passes scan the collection.

Breaking changes

  • The result type of GetOrderQuery, GetCustomerQuery, GetGiftVoucherQuery, GetMerchandiseReturnQuery and GetPaymentTransactionQuery changes from IQueryable<T> to IPagedList<T>. External code that sends them and composes further LINQ must instead set the query's filters and PageIndex/PageSize. Code that replaces their handlers must return IPagedList<T>.
  • PageIndex/PageSize on these queries are now applied. Senders that relied on them being ignored (for example, setting PageSize = 1 and then calling .Count()) now get one page. Use TotalCount instead.
  • No in-repo plugin or module is affected. Grand.Module.Api has its own GetCustomerQuery type, which is unchanged.

Testing

Automated: the solution builds, and every test project passes. New tests: GetOrderQueryHandlerTests.Handle_FiltersByCustomerAndPages and GetCustomerQueryHandlerTests.Handle_PageSizeTwo_ReturnsFirstPageAndTotal.

Manual:

  1. Admin → Dashboard: "Abandoned carts" and the registered-today counter show the same numbers as on develop, including a cart created as a guest just before.
  2. Admin → Affiliates: deleting an affiliate that has a customer or an order is refused; deleting an unused one works.
  3. Storefront: buy a downloadable product and complete the order. My account → Downloadable products lists it.
  4. With "Product reviews must be from purchasers" settings on, write a review of the product from step 3. It is saved as a confirmed purchase. A product never ordered is not.
  5. With "Vendor review possible only after purchasing" on, review a vendor you bought from (allowed) and one you did not (refused).
  6. Create and activate a gift voucher in Admin, then apply its code in the cart: it is accepted. A wrong code is rejected.
  7. Request a merchandise return for a delivered order item. My account → Returns lists it, and once the full quantity is returned the item is no longer returnable.
  8. Admin → Orders, Customers, Gift vouchers and Payment transactions: paging and totals match develop.

Copilot AI balanced review requested due to automatic review settings October 18, 2026 14:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

support 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.
@KrzysztofPajak
KrzysztofPajak merged commit 3335a91 into develop Oct 18, 2026
9 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the refactor/business-quaryable branch October 18, 2026 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants