Skip to content

Run the admin report queries asynchronously and page bestsellers in the aggregation - #972

Merged
KrzysztofPajak merged 3 commits into
developfrom
perf/admin-report-aggregations
Oct 21, 2026
Merged

KrzysztofPajak merged 3 commits into
developfrom
perf/admin-report-aggregations

Conversation

@KrzysztofPajak

@KrzysztofPajak KrzysztofPajak commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

What

The admin reports executed their aggregations synchronously, and the bestsellers report paged in memory.

OrderReportService

  • BestSellersReport read every product group of the unwind + group over the matching orders with a synchronous .ToList(), then paged the list in memory. The sort and the page now run in the aggregation through PagedAsync, so only the requested page leaves the database.
  • GetCountryReport, GetOrderByTimeReport, GetOrderAverageReportLine and GetOrderPeriodReport use the repository's async reads instead of a synchronous .ToList() / .FirstOrDefault() wrapped in Task.FromResult.
  • GetAlsoPurchasedProductsIds reads only the product ids, asynchronously.
  • ProductsNeverSold loaded the product id of every order item and removed duplicates in memory. The Distinct() now runs in the database, so only the unique sold ids come back.

CustomerReportService

Report fixes

Bugs in the same functions, fixed in a second commit:

  • Bestsellers and best customers sort with a unique tie-breaker (ProductId / CustomerId). Without it, paging in the aggregation could show a tied row on two pages and skip another.
  • Bestseller amounts and the vendor branches of the country and best-customer reports divide by CurrencyRate instead of Rate. GetOrderByTimeReport multiplied by CurrencyRate; it now divides, like every other report.
  • The vendor branches of GetCountryReport and GetBestCustomersReport counted order items as orders. They now group by order first.
  • GetOrderByTimeReport returns its rows sorted by time. Both by-time reports end at the start of the next day instead of 23:59:00, so the last minute of the end day is included.
  • GetRegisteredCustomersReport compared CreatedOnUtc with a user-time cutoff; it now uses DateTime.UtcNow. GetCustomerByTimeReport returns an empty report when the Registered group is missing instead of throwing.

Reported amounts change in two cases: the bestseller and vendor amounts when the primary currency's rate is not 1.0, and the orders-by-time sums whenever orders were placed in a currency other than the primary one. They now match the other dashboard figures.

Not in this PR: ProductsNeverSold still sends the sold product ids back in a $nin filter, which can approach the 16MB document limit on a very large catalog. Replacing it needs an index or a different approach, so it is left for a follow-up.

Measured

Not measured.

When a page comes back full, PagedAsync runs a count of the grouped query, so the bestsellers grouping runs twice: once for the page, once for the total. The admin grid needs that total. The storefront home page bestsellers widget does not use it, but it only calls the report on a cache miss.

Breaking changes

None for callers: the service interfaces are unchanged. CustomerReportService no longer takes IDateTimeService in its constructor; DI resolves it unchanged, only direct new calls need the argument removed.

Testing

  1. dotnet test on Grand.Business.Checkout.Tests (304 passed), Grand.Business.Customers.Tests (134 passed) and Grand.Web.Admin.Tests (1580 passed). New tests on the Mongo test repository cover:
    • bestsellers grouped by product, ordered by quantity and by amount
    • the requested bestsellers page with the total over all groups
    • store and vendor filters counting only the vendor's items
    • order averages with no orders and across currency rates
    • products never sold excluding products sold in several orders
    • also purchased products ordered by quantity and limited
    • registered customers counted per store, customers by day, best customers paged with the total
    • tied bestsellers paged without overlap, bestseller amounts in the primary currency
    • vendor country and best-customer reports counting orders, not items
    • both by-time reports sorted, in the primary currency, including the last minute of the end day
    • customers by time with no Registered group

Copilot AI balanced review requested due to automatic review settings October 21, 2026 19:07

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.

- Bestsellers and best customers sort with a unique tie-breaker, so pages
  paged in the aggregation no longer repeat or skip tied rows
- Bestseller and vendor amounts divide by CurrencyRate, and the order-by-time
  report divides instead of multiplying, matching the other reports
- Vendor country and best-customer reports count orders, not order items
- By-time reports are sorted by time and include the whole end day
- Registered customers report uses a UTC cutoff; customer-by-time report
  returns empty when the Registered group is missing
@KrzysztofPajak
KrzysztofPajak merged commit 5066fc1 into develop Oct 21, 2026
9 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the perf/admin-report-aggregations branch October 21, 2026 20:30
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