Repository navigation
Count discount usage with one query in limitation checks - #969
Merged
Merged
Conversation
A listing validates every applicable discount for every product. For an NTimes or NTimesPerUser discount each check sent GetDiscountUsageHistoryQuery with PageSize = 1, which costs a sorted find plus a count: 48 queries on a page of 24 products. DiscountValidationService now counts the usage history with one CountAsync and keeps the count task for the rest of the scope, keyed by discount and customer. Product overviews are prepared concurrently, so concurrent checks share one query. ExistsCodeInDiscount uses AnyAsync instead of loading a list.
added 2 commits
October 21, 2026 17:47
GetOrAdd could run the count factory once per racing thread, so a concurrent listing still sent duplicate queries; the count is now a Lazy task. A faulted count is removed from the scope instead of failing every later validation of that discount. The concurrency test now counts repository calls, and a new test covers the retry after a failed count.
The scope-wide count also served order placement, which validates through the same pricing path as a listing. A usage recorded earlier in the scope went unseen, so an N-times limit could be passed again. Every validation now counts the usage history itself. The tests check that a usage recorded between two validations in one scope is seen.
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.
What
A storefront listing validates every applicable discount for every product (
DiscountHandlerService.GetAllowedDiscounts). For a discount limited withNTimesorNTimesPerUser, each check sentGetDiscountUsageHistoryQuerywithPageSize = 1. That query goes throughPagedAsync, so each check cost a sorted find plus a count. A discount assigned to all products therefore cost 48 queries on a page of 24 products.Changes in
DiscountValidationService:CountAsyncinstead ofPagedAsync, so each check costs one query instead of two.ExistsCodeInDiscountusesAnyAsyncinstead of loading the matching coupons into a list.IRepository<DiscountUsageHistory>instead ofIMediator. It already usedIRepository<DiscountCoupon>the same way.The count is read on every validation and is not kept anywhere. Order placement validates through the same pricing path as a listing, so a count reused across the scope would miss a usage recorded earlier in that scope and let an N-times limit pass again.
The limit semantics are unchanged: the count still includes canceled usages, and
NTimesPerUserstill filters by the customer.The
DiscountUsageHistoryindexes (DiscountId,CustomerId) are already created by the installer, so no migration is needed.Measured
Local MongoDB, 100,000 usage history documents, one limited discount with 1,000 usages, the 24 checks of a page run concurrently as in
GetProductOverviewHandler, median of 7 runs per measurement, database queries only. Ranges are over three runs of the benchmark:NTimesNTimesPerUserThese are localhost timings. With network latency to the database, the 24 round trips saved are worth more.
Breaking changes
DiscountValidationServiceconstructor takesIRepository<DiscountUsageHistory>instead ofIMediator. This only affects code that constructs or subclasses the service directly; DI resolves it as before.Testing
dotnet teston the discount tests inGrand.Business.Catalog.Tests(51 passed). New tests cover:NTimeslimit, both reached and belowNTimesPerUsercounting only that customer's usagesExistsCodeInDiscountPluginDependencyCycleTests(3 passed). The tests needed a mock of the new repository dependency.dotnet build GrandNode.slnx: 0 errors.