Skip to content

ARCH-001 Phase 27: consolidate SearchController picker (Category/Collection/Brand) - #828

Merged
KrzysztofPajak merged 1 commit into
developfrom
arch001/phase27-search-picker-consolidation
Sep 11, 2026
Merged

ARCH-001 Phase 27: consolidate SearchController picker (Category/Collection/Brand)#828
KrzysztofPajak merged 1 commit into
developfrom
arch001/phase27-search-picker-consolidation

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

ARCH-001 Phase 27: SearchController picker consolidation (Category/Collection/Brand)

Fresh-survey candidate (found alongside PictureController/Phase 25). Admin's,
Store's, and Vendor's SearchController each duplicate 3 Kendo-autocomplete
picker actions: Category, Collection, Brand. Admin's Index (the full
admin command/menu search box) and its CustomerGroup/Stores/Vendor
pickers have no Store/Vendor equivalent and stay duplicated in Admin's own
controller, untouched
— same "consolidate only a sub-resource" shape as
TaxCategory (Phase 22), but the first 3-way (Admin+Store+Vendor) instance
of it in this initiative.

Design finding: why this does NOT reuse IAdminDataScope<TEntity>

The obvious first instinct — inject IAdminDataScope<Category>/<Collection>/
<Brand>, already registered for all 3 hosts from the Category/Collection/Brand
phases — turns out to be a trap here. Those entities' routed scopes
(RoutedCategoryDataScope etc.) fail closed for the "Vendor" area, because
Category/Collection/Brand have no Vendor CRUD screen at all:

return area switch {
    "Admin" => globalScope,
    "Store" => storeScope,
    _ => throw new InvalidOperationException(...) // includes "Vendor"
};

This picker sub-resource does run under Vendor (Vendor's own SearchController
already exposed it pre-consolidation) — reusing those scopes would have thrown
on every single Vendor picker call.

Investigated each host's actual original code instead of assuming: Admin and
Vendor both hardcoded storeId: "" (no store filter at all); only Store scoped
by WorkContext.CurrentCustomer.StaffStoreId. Confirmed in
CategoryService.GetAllCategories (and the identical shape in Collection/Brand's
services) that storeId: "" and storeId: null are handled identically —
!string.IsNullOrEmpty(storeId) gates the store filter, true for both. So a
protected virtual string PickerStoreId => ""; on the new BaseSearchController
(matching Admin/Vendor's original behavior, no override needed for either) that
only Store's concrete subclass overrides exactly replicates all 3 hosts'
original behavior. No behavior change.

Change

New BaseSearchController in Grand.Web.AdminShared holds Category/
Collection/Brand. Admin/Store/Vendor's SearchController are now thin
subclasses restating their host's [Authorize*]/[Area]/[AuthorizeMenu]/
[AutoValidateAntiforgeryToken] attribute set — BaseSearchController can't
inherit any single host's base controller, same reason as every other
Base*Controller in this initiative.

Tests

No prior test coverage existed for any of the 3 originals. Added:

  • BaseSearchControllerTestsCategory/Collection/Brand × {default ""
    storeId, overridden storeId} exercised directly against the shared base.
  • SearchControllerAttributeTests (Admin + Store) — subclass/attribute wiring;
    Store's includes one behavior test proving PickerStoreId actually reflects
    WorkContext.CurrentCustomer.StaffStoreId end-to-end, not just that the
    override exists.
  • SearchControllerSurfaceTests (Vendor) — same attribute-wiring shape, matching
    Grand.Web.Vendor.Tests' existing *SurfaceTests naming convention
    (VendorReviewControllerSurfaceTests).

Verification

dotnet build GrandNode.sln — 0 errors. Per-project test runs (parallel
full-solution runs are known-flaky, per repo history):

  • Grand.Web.Admin.Tests: 1439/1439
  • Grand.Web.Store.Tests: 142/142
  • Grand.Web.Vendor.Tests: 26/26

No live smoke test run this phase — deferred, not blocking. These are
read-only picker endpoints (Kendo autocomplete data sources); no cross-tenant
write surface or access-control boundary changed, same deferral justification
class as Phase 24 (Currency/Language cleanup).

…/Brand picker

Admin/Store/Vendor's SearchController each duplicate 3 Kendo-autocomplete picker
methods (Category/Collection/Brand). Admin's Index (full admin command/menu
search) and CustomerGroup/Stores/Vendor pickers have no Store/Vendor equivalent
and stay in Grand.Web.Admin's own controller untouched - same
"consolidate-a-sub-resource" shape as TaxCategory (Phase 22), but the first
3-way (Admin+Store+Vendor) instance of it.

Design note - deliberately does NOT reuse IAdminDataScope<Category>/<Collection>/
<Brand>: those entities' routed scopes (RoutedCategoryDataScope etc.) fail closed
for the "Vendor" area, because Category/Collection/Brand have no Vendor CRUD
screen. This picker sub-resource DOES run under Vendor (Vendor's own
SearchController already exposed it), so resolving one of those scopes here
would throw InvalidOperationException on every Vendor picker call. Investigated
each host's original code instead: Admin and Vendor both hardcoded
storeId: "" (no store filter); only Store scoped by
WorkContext.CurrentCustomer.StaffStoreId. Confirmed in CategoryService.
GetAllCategories (and the same shape in Collection/Brand's services) that
storeId: "" and storeId: null are handled identically
(!string.IsNullOrEmpty(storeId) gates the filter both ways), so a new
`protected virtual string PickerStoreId => "";` on BaseSearchController
(Admin/Vendor's original behavior, no override needed) overridden only by
Store's concrete subclass exactly replicates all 3 hosts' original behavior.
No behavior change.

New BaseSearchController in Grand.Web.AdminShared holds Category/Collection/
Brand. Admin/Store/Vendor's SearchController are now thin subclasses restating
their host's [Authorize*]/[Area]/[AuthorizeMenu]/[AutoValidateAntiforgeryToken]
attribute set (BaseSearchController can't inherit any single host's base
controller - same reason as every other Base*Controller in this initiative).

No prior test coverage existed for any of the 3 originals. Added:
- BaseSearchControllerTests: Category/Collection/Brand x {default "" storeId,
  overridden storeId} against the shared base directly.
- SearchControllerAttributeTests (Admin + Store) - subclass/attribute wiring,
  Store's includes one behavior test proving PickerStoreId actually reflects
  WorkContext.CurrentCustomer.StaffStoreId end-to-end.
- SearchControllerSurfaceTests (Vendor) - same attribute-wiring shape, matching
  Grand.Web.Vendor.Tests' existing *SurfaceTests naming convention
  (VendorReviewControllerSurfaceTests).

Verified: dotnet build GrandNode.sln 0 errors. Per-project test runs (parallel
full-solution runs are known-flaky): Grand.Web.Admin.Tests 1439/1439,
Grand.Web.Store.Tests 142/142, Grand.Web.Vendor.Tests 26/26.

No live smoke test run this phase - deferred, not blocking (read-only picker
endpoints, no cross-tenant/access-control surface changed, same justification
class as Phase 24's deferral).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y3MqvZq7y1Uc5p4JZad2i
Copilot AI lite review requested due to automatic review settings September 11, 2026 01:54

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.

@KrzysztofPajak
KrzysztofPajak merged commit ff40e3a into develop Sep 11, 2026
6 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the arch001/phase27-search-picker-consolidation branch September 11, 2026 14:00
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