diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/README.md b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/README.md index 873e010..b0dba27 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/README.md +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/README.md @@ -7,7 +7,7 @@ DataSource CRUD, collection, tracking, and filter tests. Tested with List and Ac ### Test Base (partial class) - **`SignalRDataSourceTestBase.cs`** — Abstract generic base with SignalR infrastructure setup. - **`SignalRDataSourceTestBase.Collections.cs`** — Count, Clear, Contains tests. -- **`SignalRDataSourceTestBase.ContextAndFilter.cs`** — ContextIds, FilterText, Expression filter with AcExpressionNode. +- **`SignalRDataSourceTestBase.ContextAndFilter.cs`** — ContextIds, CustomFilterCriteria, Expression filter with AcExpressionNode. - **`SignalRDataSourceTestBase.CrudOperations.cs`** — Add (auto-save, tracking-only, duplicate/default ID validation). - **`SignalRDataSourceTestBase.LoadDataSource.cs`** — Full DataSource load. - **`SignalRDataSourceTestBase.LoadItem.cs`** — Single item load by ID. diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTestBase.ContextAndFilter.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTestBase.ContextAndFilter.cs index 64b31ea..cf45766 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTestBase.ContextAndFilter.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTestBase.ContextAndFilter.cs @@ -22,12 +22,12 @@ public abstract partial class SignalRDataSourceTestBase } [TestMethod] - public virtual void FilterText_CanBeSetAndRetrieved() + public virtual void CustomFilterCriteria_CanBeSetAndRetrieved() { var dataSource = CreateDataSource(_client, _crudTags); - dataSource.FilterText = "search term"; + dataSource.CustomFilterCriteria = "search term"; - Assert.AreEqual("search term", dataSource.FilterText); + Assert.AreEqual("search term", dataSource.CustomFilterCriteria); } #endregion diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs index 2c0e82c..a42ffed 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs @@ -131,7 +131,7 @@ public class SignalRDataSourceTests_List_Binary : SignalRDataSourceTestBase base.ContextIds_CanBeSetAndRetrieved(); [TestMethod] - public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved(); + public override void CustomFilterCriteria_CanBeSetAndRetrieved() => base.CustomFilterCriteria_CanBeSetAndRetrieved(); [TestMethod] public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex(); [TestMethod] diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs index aebbaf0..07f1136 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs @@ -131,7 +131,7 @@ public class SignalRDataSourceTests_List_Binary_NoRef : SignalRDataSourceTestBas [TestMethod] public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved(); [TestMethod] - public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved(); + public override void CustomFilterCriteria_CanBeSetAndRetrieved() => base.CustomFilterCriteria_CanBeSetAndRetrieved(); [TestMethod] public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex(); [TestMethod] diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs index 07a2b77..ca208c9 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs @@ -136,7 +136,7 @@ public class SignalRDataSourceTests_List_Json : SignalRDataSourceTestBase base.ContextIds_CanBeSetAndRetrieved(); [TestMethod] - public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved(); + public override void CustomFilterCriteria_CanBeSetAndRetrieved() => base.CustomFilterCriteria_CanBeSetAndRetrieved(); [TestMethod] public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex(); [TestMethod] diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs index 820e3c5..7989503 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs @@ -132,7 +132,7 @@ public class SignalRDataSourceTests_Observable_Binary : SignalRDataSourceTestBas [TestMethod] public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved(); [TestMethod] - public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved(); + public override void CustomFilterCriteria_CanBeSetAndRetrieved() => base.CustomFilterCriteria_CanBeSetAndRetrieved(); [TestMethod] public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex(); [TestMethod] diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs index 701d521..6abe8a0 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs @@ -136,7 +136,7 @@ public class SignalRDataSourceTests_Observable_Json : SignalRDataSourceTestBase< [TestMethod] public override void ContextIds_CanBeSetAndRetrieved() => base.ContextIds_CanBeSetAndRetrieved(); [TestMethod] - public override void FilterText_CanBeSetAndRetrieved() => base.FilterText_CanBeSetAndRetrieved(); + public override void CustomFilterCriteria_CanBeSetAndRetrieved() => base.CustomFilterCriteria_CanBeSetAndRetrieved(); [TestMethod] public override void IList_Add_ReturnsCorrectIndex() => base.IList_Add_ReturnsCorrectIndex(); [TestMethod] diff --git a/AyCode.Services/SignalRs/AcSignalRDataSource.cs b/AyCode.Services/SignalRs/AcSignalRDataSource.cs index 3f89c7a..b1f34c1 100644 --- a/AyCode.Services/SignalRs/AcSignalRDataSource.cs +++ b/AyCode.Services/SignalRs/AcSignalRDataSource.cs @@ -176,7 +176,13 @@ namespace AyCode.Services.SignalRs protected readonly ChangeTracking TrackingItems = new(); public object[]? ContextIds; - public string? FilterText { get; set; } + /// + /// Consumer-supplied base filter criteria (a DevExpress CriteriaOperator string) appended as the LAST + /// positional GetAll parameter; the server parses + whitelists it. Renamed FilterText → CustomFilterText → + /// CustomFilterCriteria (2026-07-16) as the semantics settled to a criteria string. Under the ADR-0004 + /// unified model it is AND-merged with the grid's automatic FilterCriteria. + /// + public string? CustomFilterCriteria { get; set; } public AcSignalRClientBase SignalRClient; protected readonly SignalRCrudTags SignalRCrudTags; @@ -258,7 +264,7 @@ namespace AyCode.Services.SignalRs { var parameters = new List(); if (ContextIds != null) parameters.AddRange(ContextIds); - if (FilterText != null) parameters.Add(FilterText); + if (CustomFilterCriteria != null) parameters.Add(CustomFilterCriteria); if (parameters.Count == 0) parameters = null; return parameters?.ToArray(); diff --git a/AyCode.Services/docs/SIGNALR_DATASOURCE/README.md b/AyCode.Services/docs/SIGNALR_DATASOURCE/README.md index d5a9760..7973f74 100644 --- a/AyCode.Services/docs/SIGNALR_DATASOURCE/README.md +++ b/AyCode.Services/docs/SIGNALR_DATASOURCE/README.md @@ -109,7 +109,7 @@ await dataSource.LoadItem(id); // sin 3. **Fallback re-serialize** (responseData neither `byte[]` nor `TIList` — e.g., bare `List` in test scenarios without protocol): `AcBinarySerializer.Serialize(responseData)` → bytes → process as Path 1 (Binary). -**Context/Filtering:** `ContextIds` (`object[]`) and `FilterText` (`string`) are sent with every GetAll request for server-side filtering. +**Context/Filtering:** `ContextIds` (`object[]`) and `CustomFilterCriteria` (`string` — a DevExpress criteria string; renamed from `FilterText` → `CustomFilterText` 2026-07-16) are sent with every GetAll request for server-side filtering. ## Change Tracking diff --git a/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_TODO.md b/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_TODO.md index a7dd3ce..e35b4a2 100644 --- a/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_TODO.md +++ b/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_TODO.md @@ -2,7 +2,7 @@ Forward-looking work specific to `AcSignalRDataSource`. Transport-side TODOs live in `../SIGNALR/SIGNALR_TODO.md`. -> **Server-side filtering — design in ADR 0004.** The client→server filter mechanism (`SignalParams.FilterText` + `[SignalRFilterText]` hub injection + filter-service + per-entity whitelist) is captured in [`0004-signalr-datasource-server-side-filtering.md`](../../../docs/adr/0004-signalr-datasource-server-side-filtering.md) (Status: Proposed). Not yet broken into per-step entries — reference the ADR when implementation begins. +> **Server-side filtering — design in ADR 0004.** The client→server filter mechanism (`SignalParams.FilterCriteria` + `[FilterCriteriaParam]` hub injection + filter-service with zero-config whitelist) is captured in [`0004-signalr-datasource-server-side-filtering.md`](../../../docs/adr/0004-signalr-datasource-server-side-filtering.md) (Status: Proposed). Not yet broken into per-step entries — reference the ADR when implementation begins. ## Priority legend - **P0** blocker · **P1** important · **P2** nice-to-have · **P3** idea diff --git a/docs/adr/0004-signalr-datasource-server-side-filtering.md b/docs/adr/0004-signalr-datasource-server-side-filtering.md index 6a08d5e..edbc6b9 100644 --- a/docs/adr/0004-signalr-datasource-server-side-filtering.md +++ b/docs/adr/0004-signalr-datasource-server-side-filtering.md @@ -13,7 +13,7 @@ MgGrid grids currently load the **full** dataset from the server (`GetAll`) into Current state (verified): - `MgGridSignalRDataSource : GridCustomDataSource` exists but is **unwired** (zero instantiation sites); live grids (`FruitBankGridBase` → `MgGridBase`) bind the full collection to `DxGrid.Data`. - The client already produces the DevExpress `CriteriaOperator` (used for local filtering, and as a cache key via `CriteriaOperator.ToString()`). -- The transport already carries `FilterText` (string) + `ContextIds` (object[]) with every `GetAll` (`AcSignalRDataSource.GetContextParams()`). `ContextIds` is the live mechanism (domain-scoped typed params); `FilterText` is latent / unused in FruitBank. +- The transport already carries a consumer base-filter (string; now `CustomFilterCriteria` — a **criteria** string; renamed `FilterText` → `CustomFilterText` → `CustomFilterCriteria` 2026-07-16 as its semantics settled) + `ContextIds` (object[]) with every `GetAll` (`AcSignalRDataSource.GetContextParams()`). `ContextIds` is the live mechanism (domain-scoped typed params); `CustomFilterCriteria` is latent / unused in FruitBank. - A prior **expression-based** server filter exists but is **test-only**: `AcExpressionNode` + `AcSerializerCommon.ApplyQueryFromNode` / `ExecuteQueryFromNode` serialize and execute arbitrary LINQ expression trees server-side (including arbitrary `Expression.Call` to `Queryable.Count`/`Sum`/`Any`). Timing — decide now: filtering must move server-side to scale grids and unlock all-data access, AND the expression path (a remote-code-execution-shaped injection surface) must be replaced with a safe mechanism before more grids are built on it. @@ -31,7 +31,7 @@ Adopt a **criteria-string** server-side filter for the SignalR DataSource. The D | Wire carriage | Rides in the always-sent `SignalParams` metadata (`SignalParams.FilterCriteria`), NOT a positional method param | AyCode.Services (`ISignalParams`) | | Server binding | Hub (`AcWebSignalRHubBase`) injects `SignalParams.FilterCriteria` into a param marked `[FilterCriteriaParam]`, resolved **once** from the cached `AcMethodInfoModel.ParamInfos` (zero per-call reflection). **Convention (mandatory):** the `[FilterCriteriaParam]` parameter is the **LAST** parameter and optional (`string? filterCriteria = null`) — old clients bind positionally and the trailing param defaults to null via the existing trailing-defaults transport mechanic (no skip logic needed). The hub **validates the convention at registration time** (not-last / not-optional → loud startup diagnostic, zero per-call cost); the attribute's XML `` MUST state the mandatory last-position requirement | AyCode.Services.Server | | Mismatch diagnostic | If a `FilterMode.Server` grid sends `FilterCriteria` but the method has **no** `[FilterCriteriaParam]` param → **warning-log** the mismatch: a real misconfiguration (grid declared `Server`, endpoint can't filter). **Precise** — never fires for `FilterMode.Local` grids. Log level / per-tag suppression is an implementation decision | AyCode.Services.Server | -| Legacy `FilterText` coexistence | `SignalParams.FilterCriteria` (serialized criteria string) is a **NEW channel, separate from the legacy free-text `FilterText`** (appended by `AcSignalRDataSource.GetContextParams()` as the last positional param; per-endpoint manual handling, e.g. `GetAllLogItemsByFilterText`-style endpoints). The legacy mechanism stays untouched — two different features, two different names | AyCode.Services | +| Unified criteria model (`CustomFilterCriteria` + automatic) | There is **one criteria kind**, not a free-text channel + a criteria channel. `CustomFilterCriteria` — the consumer-supplied **base scope** (a DevExpress `CriteriaOperator` string; renamed `FilterText` → `CustomFilterText` → `CustomFilterCriteria` 2026-07-16) — is AND-merged with the grid's **automatic** `FilterCriteria` (column filters + `MgTagBox`) into one effective criteria → **one parser, one whitelist**. Rationale: a raw free-text string interpolated server-side is an injection risk; a parsed + whitelisted `CriteriaOperator` is safe and standard. **Sequencing:** v1 — `CustomFilterCriteria` already gives the server-side base scope (rides the positional GetAll param today, but parsed as criteria → security win now); the grid's automatic filters stay local until v2. v2 — base + automatic merge into a single `SignalParams.FilterCriteria`. **Trusted/untrusted nuance:** merging consumer-base (trusted) with user-filter (untrusted) into one re-parsed string means the whitelist covers both; if the base needs a non-whitelisted / relation column, carry it as a **separate trusted criterion**, not merged into the user part. Origin: the dead TourIAm consumer — a LogLevel `DxTagBox` scoping ~100k+ log rows to Error/Warning by default. | AyCode.Services | | Application | Shared **filter-service**: parse → **per-entity field whitelist** → apply to the endpoint's `IQueryable` **before** `.ToListAsync()` (SQL WHERE). Requires `DevExpress.Data` (non-UI) on the server | AyCode.Services.Server | | Client forwarding (`FilterMode`) | MgGrid carries a `FilterMode` enum (**Local** / **Server**). `Server`: the grid forwards its `CriteriaOperator` into `SignalParams.FilterCriteria` and a filter change triggers a server reload. `Local`: the grid filters the loaded set client-side — no forward, no round-trip (today's behavior). `FilterMode` is a **grid-level behavioral policy (intent)**, NOT a client-side claim about server capability — this is the distinction from the rejected `HasFilterTextParam`, so it does not reintroduce that drift. The server's cached reflection stays the single authority on *capability*; `FilterMode` decides *participation*. Default = user decision (lean `Local`) | AyCode.Blazor | | Rollout | Gradual, per-endpoint opt-in: add `[FilterCriteriaParam] string? filterText` + one filter-service call. No endpoint-signature-position fragility, no per-endpoint duplication of filter logic | consumer plugin | @@ -41,7 +41,7 @@ Adopt a **criteria-string** server-side filter for the SignalR DataSource. The D **Positive:** - **Bounded to a WHERE predicate — no RCE.** Replaces the `AcExpressionNode` surface (which executed arbitrary server-side expression trees) with a criteria DSL that can only express a filter predicate over the queried entity. The worst a tampered filter string can do is **add / remove / widen filter conditions over the same query** — not execute code. (The residual "which columns / relations / functions" surface is closed by the whitelist — see Negative.) -- Wire format is a plain string → vendor-neutral on the wire, fits the existing `FilterText` channel; the framework stays DevExpress-free on the wire. +- Wire format is a plain string → vendor-neutral on the wire (`SignalParams.FilterCriteria` metadata field); the framework stays DevExpress-free on the wire. - **Universal + gradual**: the injection logic lives **once** in the hub; endpoints opt in with an attribute + one service call. No positional-param fragility (the `ContextIds` object[] is variable-length). - **Single source of truth**: the server's cached reflection is authoritative on filter support; no drift-prone client-side flag duplicating it. - **Loud, not silent, precise**: a `FilterMode.Server` grid whose endpoint lacks `[FilterCriteriaParam]` triggers a server warning (a real misconfiguration signal), never a silent no-filter drop — and `FilterMode.Local` grids never false-trigger it. @@ -60,7 +60,7 @@ Adopt a **criteria-string** server-side filter for the SignalR DataSource. The D **Follow-ups required:** - File SIGDS + GRID TODO entries: `[FilterCriteriaParam]` attribute (lives beside the other SignalR attributes in AyCode.Services; XML `` MUST state the mandatory last-position + optional convention) + hub injection + registration-time convention validation + **mismatch warning-log** (AyCode.Services.Server); `SignalParams.FilterCriteria` field (AyCode.Services); filter-service — zero-config whitelist default + parse/length hard-reject + criteria Debug-log (AyCode.Services.Server); MgGrid `FilterMode` (Local / Server) + criteria serialization into `SignalParams.FilterCriteria` on `Server` (AyCode.Blazor); add `DevExpress.Data` to the server plugin. `[NotGridFilterable]` (AyCode.Core) is **deferred** — TODO-only when the filter-service lands. -- **Enabled extension — IMPLEMENTED (2026-07-16), local mode**: MgGrid declarative **quick-filter panel**. Shipped shape (simplified from the earlier two-wrapper draft): a `FilterPanel` RenderFragment slot on `MgGridBase` that **itself wraps** its content in the styled `.mg-grid-filter-panel` container — no separate panel component (the planned `MgGridFilterPanelBase` → `GridFilterPanel` pair was built, then removed as redundant nesting). Consumers place `MgTagBox` (`: DxTagBox` seam) controls directly in the slot, each with a `FieldName` (consistent with grid columns / DevExpress `FieldName`). On selection each control calls `DxGrid.SetFieldFilterCriteria(FieldName, criteria)` via `IMgGridBase.SetFieldQuickFilter`; DevExpress **AND-combines the per-field criteria automatically** (+ the user's filter row) — no manual aggregation, no clobber (**NOT** `SetFilterCriteria`, which replaces the whole filter). A toolbar **Show/Hide Filters** toggle (`IMgGridBase.HasFilterPanel` / `IsFilterPanelVisible` / `ToggleFilterPanel`) appears only when the grid has a filter panel. Implementation notes: imperative `Values` writes inside DevExpress editors must be wrapped in `BeginUpdate()`/`EndUpdate()` (parameter-tracker throws otherwise); first consumer: Partner grid (Currency tag box). The grid's `FilterCriteria` is the single source: locally it filters the bound `Data`; under `FilterMode.Server` the same `FilterCriteria` reaches `GridCustomDataSource.GetItemsAsync(options.FilterCriteria)` → the criteria-string path — so the component is built **once** for both. Needs a server distinct-values source under `FilterMode.Server` (see v2 paging). +- **Enabled extension — IMPLEMENTED (2026-07-16), local mode**: MgGrid declarative **quick-filter panel**. Shipped shape (simplified from the earlier two-wrapper draft): a `FilterPanel` RenderFragment slot on `MgGridBase` that **itself wraps** its content in the styled `.mg-grid-filter-panel` container and renders it **below the toolbar** (composed into the grid's `ToolbarTemplate`; 2026-07-16) — no separate panel component (the planned `MgGridFilterPanelBase` → `GridFilterPanel` pair was built, then removed as redundant nesting). Consumers place `MgTagBox` (`: DxTagBox` seam) controls directly in the slot, each with a `FieldName` (consistent with grid columns / DevExpress `FieldName`). On selection each control calls `DxGrid.SetFieldFilterCriteria(FieldName, criteria)` via `IMgGridBase.SetFieldQuickFilter`; DevExpress **AND-combines the per-field criteria automatically** (+ the user's filter row) — no manual aggregation, no clobber (**NOT** `SetFilterCriteria`, which replaces the whole filter). A toolbar **Show/Hide Filters** toggle (`IMgGridBase.HasFilterPanel` / `IsFilterPanelVisible` / `ToggleFilterPanel`) appears only when the grid has a filter panel. Implementation notes: imperative `Values` writes inside DevExpress editors must be wrapped in `BeginUpdate()`/`EndUpdate()` (parameter-tracker throws otherwise); first consumer: Partner grid (Currency tag box). The grid's `FilterCriteria` is the single source: locally it filters the bound `Data`; under `FilterMode.Server` the same `FilterCriteria` reaches `GridCustomDataSource.GetItemsAsync(options.FilterCriteria)` → the criteria-string path — so the component is built **once** for both. Needs a server distinct-values source under `FilterMode.Server` (see v2 paging). - Verify: `DevExpress.Data` version matching the client DevExpress Blazor major; license coverage on the server package feed; linq2db translation of `CriteriaToExpressionConverter` output (function whitelist). - **Quick-search (`SearchText`) — confirmed SEPARATE from `FilterCriteria`.** Per DevExpress docs (Filter API): *"Search settings do not affect the applied filter criteria"* — `SearchText` filters in parallel, NOT folded into `FilterCriteria`. So server-side search does **not** ride the criteria-string path for free; it needs its **own** handling: a thin `SearchText → OR-Contains criteria` adapter over searchable columns (then it rides the same channel), or a separate search field. Same whitelist / DoS caveats apply (search = OR-Contains across many columns). - AyCode.Blazor cross-ref: no `docs/adr/` folder there yet → note in MGGRID docs or a future AyCode.Blazor ADR.