diff --git a/AyCode.Core/Consts/AcEnv.cs b/AyCode.Core/Consts/AcEnv.cs index c421026..990ed64 100644 --- a/AyCode.Core/Consts/AcEnv.cs +++ b/AyCode.Core/Consts/AcEnv.cs @@ -34,6 +34,32 @@ namespace AyCode.Core.Consts set => _baseUrl = value; } + /// + /// A szerver ADAT-környezete: a mögöttes adatbázis PROD-e. FIGYELEM: NEM build-konfiguráció (Debug/Release) + /// és NEM hosting-környezet (ASPNETCORE_ENVIRONMENT) — három független tengely; ez az adat-tengely. + /// Forrás: a szerver a DB-katalógusából sorolja be magát (fail-closed); a kliens tükrözi (kapcsolat-felépüléskor + /// a szerver env-válaszából + push-on-connect announce). Fail-fast: hidratálatlan olvasás exception. + /// + private static bool? _isProdDb; + public static bool IsProdDb + => _isProdDb ?? throw new InvalidOperationException( + "AcEnv.IsProdDb nincs hidratálva — a szerver a DB-katalógusból tölti induláskor; a kliens a kapcsolat felépülésekor a szerver env-válaszából."); + + /// True, ha az IsProdDb már hidratált — kapu-logikához (első-connect await) és a push-összevetéshez. + public static bool IsProdDbHydrated => _isProdDb != null; + + /// Csak TÉNYLEGES változáskor tüzel (első hidratálás + announce-detektált env-váltás) — + /// a megjelenítők (title-badge, UI-sáv) feliratkozási pontja. + public static event Action? IsProdDbChanged; + + public static void SetIsProdDb(bool isProdDb) + { + var changed = _isProdDb != isProdDb; + _isProdDb = isProdDb; + + if (changed) IsProdDbChanged?.Invoke(isProdDb); + } + private static IConfiguration? _appConfiguration = null; public static IConfiguration AppConfiguration => _appConfiguration ??= GetAppSettingsConfiguration(); diff --git a/AyCode.Services/SignalRs/README.md b/AyCode.Services/SignalRs/README.md index 7b2cfc2..c815ede 100644 --- a/AyCode.Services/SignalRs/README.md +++ b/AyCode.Services/SignalRs/README.md @@ -27,3 +27,13 @@ Custom binary SignalR protocol, client infrastructure, message tagging, and seri ### Serialization & Pooling - **`SignalRSerializationHelper.cs`** — Static helpers: `SerializeToBinary()`, `DeserializeFromBinary()`, compressed JSON variants. - **`SignalRRequestModel.cs`** — Poolable (`IResettable`) request tracking model with `ObjectPool` for reuse. + +### Data Source + +> **Full specification:** `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md` + +- **`AcSignalRDataSource.cs`** — Abstract generic real-time collection (`AcSignalRDataSource`) implementing `IList`, `IList`, `IReadOnlyList`. Consumer of the transport above (not part of it) — wraps CRUD-tag communication behind a regular list. Transport-agnostic by design; the current implementation is SignalR-coupled. + - **Change tracking:** `TrackingItem` wraps each modified item with `TrackingState` + `OriginalValue` (rollback snapshot). `ChangeTracking` manages the tracked list. + - **Loading:** `LoadDataSource()` (sync-wait), `LoadDataSourceAsync()` (async callback), `LoadItem(id)` (single). Raw-`byte[]` path uses `AcBinaryDeserializer.PopulateMerge` into `IAcObservableCollection` (batch UI notification via `BeginUpdate`/`EndUpdate`); `SerializerType` selects Binary vs JSON+GZip. + - **Saving:** `SaveChanges()` / `SaveChangesAsync()` iterate tracked items, post each via the state's CRUD tag, roll back on failure; `SaveItem()` for individual saves. `IsSyncing` + `OnSyncingStateChanged` expose sync state. +- **`TrackingItemHelpers.cs`** — `JsonClone` / `ReflectionClone` deep-clone helpers producing the `OriginalValue` rollback snapshots. diff --git a/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_TODO.md b/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_TODO.md index cee50dd..a7dd3ce 100644 --- a/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_TODO.md +++ b/AyCode.Services/docs/SIGNALR_DATASOURCE/SIGNALR_DATASOURCE_TODO.md @@ -2,6 +2,8 @@ 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. + ## Priority legend - **P0** blocker · **P1** important · **P2** nice-to-have · **P3** idea diff --git a/docs/adr/0002-environment-infrastructure-to-framework.md b/docs/adr/0002-environment-infrastructure-to-framework.md new file mode 100644 index 0000000..bf3e560 --- /dev/null +++ b/docs/adr/0002-environment-infrastructure-to-framework.md @@ -0,0 +1,35 @@ +# ADR 0002: Environment infrastructure lifted to the framework (BaseUrl, server data-environment, env badge) — cross-reference + +## Status + +Accepted (2026-07-15) — cross-reference; the full decision record lives in the consumer repo (see below). + +## Cross-reference + +**Primary ADR:** `FruitBankHybridApp/docs/adr/0002-environment-infrastructure-to-framework.md` +([relative link](../../../../../Mango/Source/FruitBankHybridApp/docs/adr/0002-environment-infrastructure-to-framework.md) — workspace layout dependent) + +Same NNNN as the primary by design (adr-author skill, multi-repo rule: "under the same NNNN if possible") — +this is why 0002 is used here instead of max+1. + +## Scope on this repo's side + +The decision moves app-agnostic environment infrastructure from the consumer (FruitBank) into this framework, +executed stepwise (per-step review): + +- **AyCode.Core** — `AcEnv.BaseUrl` (✅ done, 2026-07-15) and `AcEnv.IsProdDb` (+ `Set`/`Hydrated`/`Changed` event; planned): + single-source, fail-fast state; axis-separated naming (`IsProdDb` = data environment ≠ `IsProdBuild` = build config). +- **AyCode.Services** — framework-reserved tags in `AcSignalRTags` (query 90020 / announce 90021, GridLayout-roaming + precedent) + environment machinery in `AcSignalRClientBase` (first-connect gate, `EnsureEnvironmentAsync`, + fail-closed raw query, announce interception; **opt-in** via `EnvironmentFeatureEnabled => false` default). +- **AyCode.Services.Server** — hub-base push-on-connect announce from `AcEnv.IsProdDb` (best-effort). +- **AyCode.Blazor.Components** — `IAcAppTitleService` + browser implementation + `` component. + +Consumer-side remains: domain constants, DB-catalog classification call-site, query endpoint implementation +(framework tag), MAUI title implementation, app name. Consumer facades delegate to `AcEnv` (zero-break migration). + +## Related + +- Related ADRs: primary record (see Cross-reference above). +- Related TODOs/Issues: `ACCORE-LOG-T-N2D8` (AcEnv.SetConfiguration synergy), `ACCORE-LOG-T-D7S3`, + `ACCORE-SBP-T-T5R2`, `ACBLAZOR-GRID-T-E3B6`. diff --git a/docs/adr/0004-signalr-datasource-server-side-filtering.md b/docs/adr/0004-signalr-datasource-server-side-filtering.md new file mode 100644 index 0000000..d7ac0a8 --- /dev/null +++ b/docs/adr/0004-signalr-datasource-server-side-filtering.md @@ -0,0 +1,89 @@ +# ADR 0004: Server-side filtering for SignalR DataSource via DevExpress CriteriaOperator string + `[SignalRFilterText]` hub injection + +## Status + +Proposed (2026-07-15) + +> **Note:** Proposed / refinable. The design was settled across a multi-turn design discussion (this ADR captures it in place of the `adr-author` interview). Implementation-level details (filter-service whitelist specifics, function-translation caveats) are expected to sharpen during execution and this ADR will be updated. + +## Context + +MgGrid grids currently load the **full** dataset from the server (`GetAll`) into an `AcObservableCollection`, bound directly to DevExpress `DxGrid.Data`; DevExpress then filters / sorts / pages **in the browser** over the whole set. For large tables (thousands / tens-of-thousands of rows) this is inefficient. FruitBank works around it with a hardcoded **15-day** server window (`Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/CustomOrderSignalREndpoint.cs`), which also blocks access to older data. + +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. +- 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. + +**In scope:** the universal client→server filter mechanism for the SignalR DataSource + MgGrid — **v1 = filtering**. +**Out of scope:** server-side **paging** (v2 — `skip`/`take`/`count`); full removal of `AcExpressionNode` from the serializers (separate effort); the change-tracking / rollback model (untouched — v1 stays in the load-all model, just server-filtered). + +## Decision + +Adopt a **criteria-string** server-side filter for the SignalR DataSource. The DevExpress grid filter travels as a `CriteriaOperator` **string** (bounded query DSL), applied server-side to `IQueryable`; `AcExpressionNode` is **not** used for grid filtering. + +| Concern | Decision | Layer | +|---|---|---| +| Filter representation | DevExpress `CriteriaOperator` **string** (`ToString()` client → `Parse()` server), NOT a serialized expression tree | — | +| Wire carriage | Rides in the always-sent `SignalParams` metadata (`SignalParams.FilterText`), NOT a positional method param | AyCode.Services (`ISignalParams`) | +| Server binding | Hub (`AcWebSignalRHubBase`) injects `SignalParams.FilterText` into a param marked `[SignalRFilterText]`, resolved **once** from the cached `AcMethodInfoModel.ParamInfos` (zero per-call reflection) | AyCode.Services.Server | +| Mismatch diagnostic | If a `FilterMode.Server` grid sends `FilterText` but the method has **no** `[SignalRFilterText]` 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 | +| 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.FilterText` 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 `[SignalRFilterText] string? filterText` + one filter-service call. No endpoint-signature-position fragility, no per-endpoint duplication of filter logic | consumer plugin | +| Scope | **v1 = filtering** (load-all model, change-tracking untouched). **v2 = paging** (`skip`/`take`/`count` + `{items,total}` envelope) extends the same `SignalParams` channel later | — | + +## Consequences + +**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. +- **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 `[SignalRFilterText]` triggers a server warning (a real misconfiguration signal), never a silent no-filter drop — and `FilterMode.Local` grids never false-trigger it. +- Efficient: filter applied to `IQueryable` before materialization → SQL `WHERE`. +- Unblocks large grids and (with v2) all-data access, retiring the 15-day hack. +- Change-tracking untouched in v1 (still load-all, just a smaller server-filtered set). + +**Negative:** +- Adds a `DevExpress.Data` dependency to the server plugin (currently DevExpress-free server-side; it has **DevExtreme**, a different product line). `DevExpress.Data` is UI-agnostic and runs in the Razor/MVC plugin — not a Blazor dependency. +- **Field-whitelist is mandatory — DevExpress does NOT provide it.** The criteria DSL bounds the *shape* to a WHERE predicate (no arbitrary code — see Positive), but `CriteriaOperator` places **no restriction on which columns / relations / functions** a criteria may reference. Within the WHERE surface a tampered filter string can still: (a) filter on a non-displayed / sensitive column as a **blind-injection oracle** (`[PasswordHash] LIKE 'a%'` → rows-or-not leaks the value character by character); (b) navigate **relations** (`[Customer.CreditCard]`) into other tables; (c) use expensive functions / aggregate subqueries (**DoS**). The per-entity whitelist (allowed filterable columns + relation depth + function set; un-translatable features rejected, not client-eval'd) is the boundary DevExpress leaves to us. +- Opt-in discipline (narrowed): a **missing** `[SignalRFilterText]` attribute is now caught by the mismatch warning; but an attribute **present with the injected value unused** (developer forgot the filter-service call) is still a silent no-filter. +- **One grid-level policy knob (`FilterMode`)**: minimal ceremony (an enum with a sensible default), but a genuine config surface. It is grid *intent*, not a server-capability claim, so it does not reintroduce the `HasFilterTextParam` drift; and it removes the false-positive warning for intentionally local grids. +- SQL-translation caveats: some criteria functions may not translate via linq2db → **function whitelist**; complexity / timeout caps needed (leading-wildcard `Contains` on a large unindexed table = full scan / DoS); parse failure must **hard-reject** (never "no filter → return everything"). +- `MgGridSignalRDataSource` (currently unwired) needs finishing, and `MgGridBase` diverting off direct `DxGrid.Data` binding onto it — but only for the eventual ServerPaged case; **v1 filtering can ride the existing load path** (server returns a smaller filtered set into the same collection). + +**Follow-ups required:** +- File SIGDS + GRID TODO entries: `[SignalRFilterText]` attribute + hub injection + **mismatch warning-log** (AyCode.Services.Server); `SignalParams.FilterText` field (AyCode.Services); filter-service + per-entity whitelist (AyCode.Services.Server); MgGrid `FilterMode` (Local / Server) + criteria serialization into `SignalParams.FilterText` on `Server` (AyCode.Blazor); add `DevExpress.Data` to the server plugin. +- **Enabled extension** (own follow-up, not core to this ADR): MgGrid declarative **quick-filter templates** — field-bound `DxTagBox` (or similar) slots in the grid container that auto-populate from distinct values, produce a `CriteriaOperator` merged into the grid filter, and ride the same `FilterMode` path. 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`) — open / questionable point:** DevExpress `SearchText` (the search box above the grid) is a **separate feature** from `FilterCriteria`, currently filtering locally over the bound in-memory `Data`. Whether DevExpress folds it into `GridCustomDataSourceItemsOptions.FilterCriteria` (→ search rides the criteria-string path for free) or needs a thin `SearchText → OR-Contains criteria` adapter over searchable columns is **unverified** — confirm at implementation. 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. +- v2 paging ADR (`skip`/`take`/`count` + `{items,total}` envelope) when large-list pressure requires it. +- `AyCode.Core/docs/adr/README.md` index row for 0004. + +## Alternatives considered + +- **`AcExpressionNode` (serialized expression tree)** (rejected): serialize the DevExpress filter as a full LINQ expression tree and execute server-side (the existing test-only path). Rejected — executes arbitrary client-supplied `Expression.Call` server-side (RCE-shaped injection); sandboxing arbitrary expression trees is effectively intractable. Being deprecated independently. +- **DevExtreme `DataSourceLoader`** (rejected for the Blazor grids): the server already references `DevExtreme.AspNet.Data`, with a commented-out `DataSourceLoader.Load` prototype (`ShippingController.cs:110-127`). + - *Fit:* DevExtreme is the JS-widget product line for the nopCommerce **admin Razor** grids — a different UI stack from the FruitBank **Blazor WASM** app grids. + - *Cost:* using it for the Blazor grids means translating the DevExpress-Blazor `CriteriaOperator` model into DevExtreme `loadOptions` — an impedance mismatch. Reasonable for the nop admin Razor grids; wrong for the Blazor app grids. +- **Positional `string? filterText` parameter per endpoint** (rejected): append `filterText` as a trailing positional param. + - *Cons:* with a variable-length `ContextIds` object[], "which position" is fragile; and it is per-endpoint signature editing. The `SignalParams` + attribute-injection removes both problems. +- **Naming convention (`filterText` param name) instead of an attribute** (rejected in favor of the attribute): bind by a consistently-named param + developer discipline. Rejected — silent failure on typo / rename; `[SignalRFilterText]` is self-documenting, rename-safe, and its param index precomputes into the cached method model. +- **Client-side opt-in flag (`HasFilterTextParam` on MgGrid)** (rejected): a per-grid bool **claiming** whether the endpoint has the filter param. Rejected — duplicates the server's authoritative cached reflection (drift-prone), and a forgotten flag = silent no-filter. Superseded by the `FilterMode` (Local / Server) **policy** enum + server mismatch warning: `FilterMode` declares the grid's own intent (not a server claim), the server's reflection stays the single authority on capability, and a `Server`-declared grid hitting an unsupported endpoint surfaces as a precise warning. The distinction is intent-vs-capability-claim, not "flag vs no-flag". +- **Framework choke-point auto-applies to `IQueryable` results** (deferred, not chosen for v1): the hub inspects the method result and, if `IQueryable`, applies filter + page + count generically → zero endpoint changes. + - *Cost:* requires endpoints to return `IQueryable` (deferred) instead of `List` — a bigger contract change with an `IQueryable`-lifetime concern (connection alive until the framework materializes). + - Rejected for v1 (the per-endpoint attribute opt-in, still returning `List`, is lower-risk and gradual). Revisit for v2 paging, where the framework must own `skip`/`take`/`count` anyway. + +## Related + +- SIGDS topic (the DataSource this extends): [`AyCode.Services/docs/SIGNALR_DATASOURCE/README.md`](../../AyCode.Services/docs/SIGNALR_DATASOURCE/README.md) + its `_ISSUES` / `_TODO` companions. +- Transport (SignalParams, hub dispatch, `AcWebSignalRHubBase`): [`AyCode.Services/docs/SIGNALR/README.md`](../../AyCode.Services/docs/SIGNALR/README.md) and [`AyCode.Services.Server/docs/SIGNALR/README.md`](../../AyCode.Services.Server/docs/SIGNALR/README.md). +- Deprecation context (the path being replaced): `AcExpressionNode` in [`AyCode.Core/Serializers/Expressions/README.md`](../../AyCode.Core/Serializers/Expressions/README.md) + `AcSerializerCommon.ApplyQueryFromNode` / `ExecuteQueryFromNode`. +- Cross-repo (client): AyCode.Blazor `MgGridBase` / `MgGridSignalRDataSource` (MGGRID topic) — no `docs/adr/` folder there yet; cross-ref to be added. +- Related TODOs: to be filed (`ACCORE-SIGDS-T-*` + `ACBLAZOR-GRID-T-*`) — see Follow-ups. +- v2 paging: a forthcoming ADR when server-side paging lands. diff --git a/docs/adr/README.md b/docs/adr/README.md index 8c6053e..6094823 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -17,7 +17,9 @@ See [`.github/skills/adr-author/SKILL.md`](../../.github/skills/adr-author/SKILL | ID | Title | Status | |---------------------------------------------------------|----------------------------------------------------------------------------------------------------|-----------------------| | [0001](0001-user-bearer-token-flow.md) | User bearer-token authentication flow (HTTP + SignalR + tag dispatch) | Accepted (2026-04-25) | -| [0003](0003-acbinary-streaming-receive-architecture.md) | AcBinary streaming receive — AsyncPipeReaderInput unified primitive and transport-agnostic helpers | Proposed (2026-04-27) | +| [0002](0002-environment-infrastructure-to-framework.md) | Environment infrastructure lifted to the framework (BaseUrl, IsProdDb, env badge) — cross-ref to FruitBankHybridApp ADR 0002 | Accepted (2026-07-15) | +| [0003](0003-acbinary-streaming-receive-architecture.md) | AcBinary streaming receive — AsyncPipeReaderInput unified primitive and transport-agnostic helpers | Accepted (2026-05-03) | +| [0004](0004-signalr-datasource-server-side-filtering.md) | Server-side filtering for SignalR DataSource via DevExpress CriteriaOperator string + `[SignalRFilterText]` hub injection | Proposed (2026-07-15) | ## Related