diff --git a/CSharpDB.slnx b/CSharpDB.slnx
index 83d8c108..6882d38d 100644
--- a/CSharpDB.slnx
+++ b/CSharpDB.slnx
@@ -21,6 +21,7 @@
+
@@ -53,6 +54,7 @@
+
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 00000000..8d072b1f
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,6 @@
+
+
+
+ $(DefaultItemExcludes);artifacts/**;**/artifacts/**;.tmp/**;**/.tmp/**;tmp/**;**/tmp/**;temp/**;**/temp/**;publish/**;**/publish/**
+
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 00000000..cacfd2c6
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/README.md b/README.md
index c92e7732..c3e157f3 100644
--- a/README.md
+++ b/README.md
@@ -210,6 +210,8 @@ The native library exports 20 C functions. See the [Native Library Reference](ht
| [Architecture Guide](https://csharpdb.com/architecture.html) | Engine design deep dive |
| [Tools & Ecosystem](https://csharpdb.com/docs/ecosystem.html) | APIs, hosts, designers, and integrations |
| [EF Core Provider](https://csharpdb.com/docs/entity-framework-core.html) | Embedded EF Core 10 provider guide |
+| [Trusted C# Callbacks](docs/trusted-csharp-functions/README.md) | Register in-process C# functions, commands, and validation rules for SQL, forms, reports, and pipelines |
+| [Trusted C# Host Sample](samples/trusted-csharp-host/README.md) | VS Code-ready C# host project for trusted functions, commands, validation rules, and form actions |
| [Admin UI Guide](https://csharpdb.com/docs/admin-ui.html) | Querying, schema, pipelines, forms, reports, and storage |
| [CSharpDB.Client](src/CSharpDB.Client/README.md) | Unified client API and transports |
| [Pipelines](https://csharpdb.com/docs/pipelines.html) | ETL package model and visual designer |
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index dd327245..4a614773 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,320 @@
# What's New
+## v3.6.0
+
+v3.6.0 adds trusted, in-process C# scalar functions and commands across
+CSharpDB's user-facing expression and automation surfaces. Host applications
+can now register C# callbacks when opening or hosting a database, then call
+those callbacks from SQL, SQL-backed triggers and procedures, Admin Forms
+formulas/events/actions, Admin Reports calculated text and preview lifecycle
+events, and pipeline filter/derive/hook expressions.
+
+The release also adds tableless scalar `SELECT` support, common built-in scalar
+functions, Admin callback catalog metadata, SQL autocomplete for built-ins and
+tableless-safe host callbacks, and local Admin artifact cleanup to keep
+incremental builds fast.
+
+### Trusted C# Scalar Functions
+
+- Added the shared `DbFunctionRegistry`, `DbFunctionRegistryBuilder`,
+ `DbScalarFunctionDelegate`, and `DbScalarFunctionOptions` public model in
+ `CSharpDB.Primitives`.
+- Added `DatabaseOptions.Functions` plus `ConfigureFunctions(...)` so embedded
+ hosts can register scalar functions when opening file-backed, in-memory, or
+ hybrid databases.
+- SQL expression evaluation now resolves registered scalar functions in
+ projections, filters, ordering expressions, `INSERT`/`UPDATE` expressions,
+ trigger bodies, and stored SQL procedure bodies.
+- Direct clients can pass trusted functions through `DirectDatabaseOptions`;
+ HTTP and gRPC clients still do not serialize delegates and can only call
+ functions registered inside the remote host process.
+- Admin Forms formulas and Admin Reports calculated expressions can use the
+ same registry while preserving existing arithmetic and aggregate behavior.
+- Pipeline filter and derived-column expressions can call registered functions;
+ package definitions store expressions plus generated automation metadata, but
+ never C# function bodies.
+- Scalar callback registration now carries `CanRunWithoutFrom` metadata so
+ hosts can identify functions that are safe to discover in tableless
+ `SELECT ...` contexts.
+- Added the usage guide at `docs/trusted-csharp-functions/README.md`.
+
+### Tableless SELECT And Built-In Scalar Functions
+
+- SQL now supports scalar `SELECT` statements without a `FROM` clause through a
+ single-row planner source.
+- Tableless statements such as `SELECT Date();`, `SELECT abs(1123.34);`, and
+ `SELECT Slugify('Hello World');` can execute without inventing a dummy table
+ when the expression does not need row context.
+- Added a central built-in scalar dispatcher for common text, date/time,
+ numeric, conversion, and null helpers, including functions such as `ABS`,
+ `DATE`, `DATESERIAL`, `DATEADD`, `DATEDIFF`, `LEN`, `UCASE`, `LCASE`,
+ `ROUND`, `IFNULL`, and `NZ`.
+- Query planning now infers built-in scalar return types where possible.
+- Query paging and Admin result serialization now handle the internal
+ tableless single-row source.
+- BLOB procedure parameters can now round-trip through tableless
+ `SELECT @payload;` rather than failing on the old unsupported-path
+ assumption.
+
+### Admin Callback Catalog And Formula UX
+
+- The Admin navigation now groups callbacks under `Callbacks / Internal` and
+ `Callbacks / External`.
+- Internal callbacks show built-in formula functions separately from registered
+ host callbacks, so the list remains navigable as the built-in surface grows.
+- External callbacks show host-registered/user-created callbacks such as sample
+ functions and automation commands.
+- Callback details now surface whether a scalar callback is marked for
+ tableless `SELECT`.
+- SQL editor completion now suggests built-in scalar functions and host
+ callbacks marked with `CanRunWithoutFrom`.
+- Admin Forms formulas now have an Access-style function catalog/helper and
+ domain-function support for common form expressions.
+
+### Trusted Commands And Form Events
+
+- Added the shared `DbCommandRegistry`, `DbCommandRegistryBuilder`,
+ `DbCommandDelegate`, `DbCommandContext`, `DbCommandResult`, and
+ `DbCommandOptions` public model in `CSharpDB.Primitives`.
+- `DbCommandOptions` now includes `Timeout` and `IsLongRunning`, and
+ `DbCommandRegistryBuilder.AddAsyncCommand(...)` registers `Task`-based host
+ callbacks without manual `ValueTask` wrapping.
+- Command timeouts cancel the command invocation token and surface as command
+ failures through the existing Forms, Reports, and Pipelines dispatch paths;
+ external cancellation is still propagated as cancellation.
+- Admin Forms can now store form-level event bindings that reference trusted
+ command names instead of storing C# source.
+- The Forms data-entry runtime dispatches `OnOpen`, `OnLoad`, `BeforeInsert`,
+ `AfterInsert`, `BeforeUpdate`, `AfterUpdate`, `BeforeDelete`, and
+ `AfterDelete`.
+- `BeforeInsert`, `BeforeUpdate`, and `BeforeDelete` can cancel the requested
+ write by returning `DbCommandResult.Failure(...)`; after-events report errors
+ without attempting to roll back a completed write.
+- Command context arguments include current record fields converted to
+ `DbValue`; metadata includes the Forms surface, form id/name, table name, and
+ event name.
+- `AddCSharpDbAdminForms(...)` now has a command-registration overload for
+ trusted host applications.
+- The Admin Forms designer preserves and edits form-level event bindings
+ instead of dropping automation metadata during save.
+- Added a command button control that invokes a trusted host command on click,
+ passing current record fields, optional configured arguments, and form
+ metadata to the command callback.
+- Added control-level Admin Forms event bindings for `OnClick`, `OnChange`,
+ `OnGotFocus`, and `OnLostFocus`, so ordinary controls can invoke trusted
+ host commands without being command buttons.
+- The Forms property inspector now edits selected-control event bindings using
+ the same registered-command picker and JSON argument editor as form-level
+ events.
+- Added shared declarative action sequence metadata with `RunCommand`,
+ `SetFieldValue`, `ShowMessage`, and `Stop` steps for Admin Forms automation.
+ Form and control event bindings can now be command-only,
+ action-sequence-only, or a command followed by an action sequence.
+- Added built-in rendered-form actions for `NewRecord`, `SaveRecord`,
+ `DeleteRecord`, `RefreshRecords`, `PreviousRecord`, `NextRecord`, and
+ `GoToRecord`, so command buttons and control events can drive common form
+ workflows without host C# callbacks.
+- Action sequence steps can now include a simple condition such as
+ `Status = 'Ready'`, `Amount > 0`, or `IsActive`; false conditions skip that
+ step, while malformed conditions fail through the normal step failure path.
+- Forms can now store reusable named action sequences on `FormDefinition` and
+ invoke them from event/button sequences with `RunActionSequence`, including
+ optional per-call arguments and a nesting guard for recursive loops.
+- The form-event and selected-control event editors now include a visual
+ action-sequence editor for adding, ordering, removing, and configuring
+ command, reusable sequence, field, message, stop, built-in record actions,
+ and per-step conditions.
+- The Forms property inspector now includes a reusable action-sequence library
+ editor at the form level, and event action editors can pick those named
+ sequences while preserving missing names for portable metadata.
+- The action-sequence editor uses registered-command pickers when commands are
+ available, preserves missing command names for portable form metadata, and
+ keeps JSON editing limited to optional argument payloads.
+- Action sequences store names, arguments, field targets, and literal values
+ only. They do not store C# source, serialize delegates, or run untrusted code.
+- Added shared command argument conversion helpers so Forms, Reports, and
+ Pipelines pass host command arguments with the same `DbValue` conversion
+ rules.
+- Admin Reports can now bind `OnOpen`, `BeforeRender`, and `AfterRender`
+ preview lifecycle events to trusted commands. The preview service passes
+ report/source metadata plus row, truncation, page, and schema-drift metrics.
+- `AddCSharpDbAdminReports(...)` now has a command-registration overload for
+ trusted host applications.
+- Pipeline packages can now include trusted command hooks for `OnRunStarted`,
+ `OnBatchCompleted`, `OnRunSucceeded`, and `OnRunFailed`. Package JSON stores
+ hook names, arguments, and generated automation metadata only; command bodies
+ remain host-registered code.
+- Pipeline hook failures fail the run through `PipelineRunResult`; failure-hook
+ errors are appended to the failed run summary instead of recursively
+ dispatching more failure hooks.
+- Admin Forms command buttons now refresh their executing/disabled state before
+ and after async command work, so long-running trusted commands give visible
+ runtime feedback in the form surface.
+
+### Stored Automation Metadata
+
+- Added shared `DbAutomationMetadata`, command references, and scalar-function
+ references so portable definitions can declare the trusted host callbacks
+ they expect without storing C# code.
+- Admin Forms, Admin Reports, and pipeline packages now regenerate automation
+ metadata during repository save/load or package serialization/deserialization.
+ Older JSON without automation metadata is backfilled on read.
+- Form metadata captures trusted form events, command buttons, selected-control
+ events, reusable action sequences, action-sequence `RunCommand` steps, and
+ computed-formula scalar functions.
+- Report metadata captures preview lifecycle command bindings and calculated
+ text scalar functions.
+- Pipeline package metadata captures command hooks and scalar functions used by
+ filter and derived-column expressions; package validation reports stale
+ automation manifests so packages can be re-exported.
+
+### Developer Experience
+
+- Added `samples/trusted-csharp-host`, a VS Code-ready C# host project for
+ writing and debugging trusted C# callbacks in ordinary application code.
+- The sample registers a trusted scalar function, calls it from SQL, registers
+ a trusted command, and runs an Admin Forms action sequence that sets a field
+ before invoking that command.
+- The sample includes local `.vscode` launch/tasks files so developers can open
+ the sample folder, press `F5`, and set breakpoints inside callback code.
+- The direct Admin launcher now cleans stale Admin artifact snapshots, builds
+ once, and runs with `--no-build` so old generated artifacts do not slow
+ startup.
+- Added repo-level MSBuild cleanup for `src/CSharpDB.Admin/artifacts` and
+ default excludes for generated artifact folders.
+- Added the async I/O batching follow-up note at
+ `docs/query-and-durable-write-performance/async-io-batching-follow-up.md`.
+
+### Behavior And Safety
+
+- Function names are case-insensitive SQL identifiers, and registration rejects
+ duplicate user names or collisions with reserved built-ins such as `TEXT`,
+ `COUNT`, `SUM`, `AVG`, `MIN`, and `MAX`.
+- Arity is validated before invocation, missing SQL functions fail with the
+ existing unknown scalar function path, and thrown delegate exceptions are
+ wrapped with the function name before normal statement/transaction rollback.
+- `NullPropagating = true` returns `NULL` without invoking the delegate when
+ any argument is `NULL`; otherwise `DbValue.Null` is passed explicitly.
+- V1 remains scalar-only, synchronous, trusted, and in-process. It does not
+ persist C# source, sandbox code, load database-owned plugin assemblies,
+ marshal delegates over HTTP/gRPC, or add aggregate/table-valued/procedure
+ UDFs.
+- Query planning keeps custom functions on the residual expression path in V1:
+ no index pushdown, generated columns, constant folding, or cost assumptions
+ are inferred from user functions.
+
+### Tests And Benchmarks
+
+- Added registry and SQL coverage for case-insensitive lookup, duplicate and
+ built-in collision rejection, null propagation, deterministic metadata,
+ missing functions, thrown functions, rollback behavior, triggers, and stored
+ SQL procedures.
+- Added direct-client, Admin Forms, Admin Reports, pipeline validation, and
+ pipeline runtime tests for registered scalar functions.
+- Added command-registry, form-event dispatcher, event JSON round-trip, and
+ Forms data-entry tests for create/update/delete event dispatch and
+ before-event cancellation.
+- Added designer-state tests for action sequences, plus command-button and
+ control-event tests covering event binding preservation and registered
+ command invocation from rendered forms.
+- Added Forms action-sequence tests for event dispatch, mutable record updates,
+ command button action-only clicks, and JSON round-tripping.
+- Added report-event dispatcher and preview lifecycle tests, pipeline hook
+ serialization/validation/orchestrator tests, and shared command argument
+ conversion tests.
+- Added automation metadata tests covering manifest extraction, JSON
+ round-tripping, repository persistence/backfill, pipeline package
+ import/export, and stale package metadata validation.
+- Added async command and timeout coverage for the command registry, Admin
+ Forms dispatcher, Admin Reports dispatcher, and pipeline hook orchestration.
+- Added Forms built-in action tests covering rendered command-button dispatch,
+ next/previous/go-to navigation, and create/save/refresh/delete workflows.
+- Added conditional action tests for skip/run behavior, condition failure,
+ rendered built-in action skipping, metadata propagation, and JSON
+ round-tripping.
+- Added parser, planner, SQL execution, direct-client, procedure, query paging,
+ and Admin completion tests for tableless `SELECT`, built-in scalar functions,
+ BLOB parameter round-tripping, and tableless-safe callback autocomplete.
+- Added Admin callback catalog tests for tableless callback metadata.
+- Added Admin Forms formula evaluator tests for the built-in function catalog
+ and Access-style formula helpers.
+- Same-machine affected benchmark comparison against the pre-feature HEAD
+ baseline showed no material regression in the main write/query guardrails:
+
+| Suite | Worst current change | Best current change |
+|-------|---------------------:|--------------------:|
+| Insert | `+3.76%` | `-3.38%` |
+| Join | `+6.65%` | `-6.93%` |
+| PointLookup | `+5.15%` | `-9.25%` |
+| QueryPlanCache | `+1.62%` | `-4.45%` |
+| ScanProjection | `+0.20%` | `-18.12%` |
+| TriggerDispatch | `+0.77%` | `-4.52%` |
+| BatchEvaluation | `+10.53%` | `-10.36%` |
+
+The one notable row was the synthetic BatchEvaluation delegate
+filter/projection case at `+10.53%`; its paired specialized path improved by
+`-10.36%`, allocations were unchanged, and the affected guardrail suites were
+otherwise neutral to improved.
+
+### Validation
+
+- `git status --short --branch`
+- `dotnet restore CSharpDB.slnx`
+- `.\scripts\Test-NoLegacyCoreReferences.ps1`
+ - Passed through the script's PowerShell fallback after the local packaged
+ `rg.exe` could not be launched normally in this desktop environment.
+- `dotnet build CSharpDB.slnx -c Release --no-restore`
+ - Passed with `0` warnings and `0` errors.
+- `dotnet test CSharpDB.slnx -c Release --no-build -m:1 -- RunConfiguration.DisableParallelization=true`
+ - Non-parallel unit test run passed with `1,663` tests.
+- Phase 5 local validation used `dotnet build CSharpDB.slnx --no-restore -m:1`
+ and `dotnet test CSharpDB.slnx --no-build -m:1 -- RunConfiguration.DisableParallelization=true`
+ - Debug non-parallel unit test run passed with `1,703` tests after adding
+ automation metadata coverage.
+- Phase 6A async-command hardening validation used
+ `dotnet build CSharpDB.slnx --no-restore -m:1` and
+ `dotnet test CSharpDB.slnx --no-build -m:1 -- RunConfiguration.DisableParallelization=true`
+ - Debug non-parallel unit test run passed with `1,709` tests.
+- Phase 6B built-in form action validation used
+ `dotnet build CSharpDB.slnx --no-restore -m:1` and
+ `dotnet test CSharpDB.slnx --no-build -m:1 -- RunConfiguration.DisableParallelization=true`
+ - Debug non-parallel unit test run passed with `1,712` tests.
+- Phase 6C conditional form action validation used
+ `dotnet build CSharpDB.slnx --no-restore -m:1` and
+ `dotnet test CSharpDB.slnx --no-build -m:1 -- RunConfiguration.DisableParallelization=true`
+ - Debug non-parallel unit test run passed with `1,715` tests.
+- `dotnet pack` smoke for the release workflow packages with
+ `-p:Version=3.6.0`
+ - Produced `11` local packages:
+ `CSharpDB`, `CSharpDB.Client`, `CSharpDB.Data`, `CSharpDB.Engine`,
+ `CSharpDB.EntityFrameworkCore`, `CSharpDB.Execution`,
+ `CSharpDB.Pipelines`, `CSharpDB.Primitives`, `CSharpDB.Sql`,
+ `CSharpDB.Storage`, and `CSharpDB.Storage.Diagnostics`.
+- `.\scripts\Publish-CSharpDbDaemonRelease.ps1 -Version 3.6.0 -Runtime win-x64 -OutputRoot artifacts\daemon-release-local`
+ - Produced `csharpdb-daemon-v3.6.0-win-x64.zip` and `SHA256SUMS.txt`.
+- Latest tableless/callback stabilization validation used
+ `dotnet test .\CSharpDB.slnx -m:1 --no-restore -v:minimal /nr:false /p:UseSharedCompilation=false /p:TestTfmsInParallel=false -- RunConfiguration.DisableParallelization=true`
+ - Debug non-parallel unit test run passed with `1,877` tests.
+
+### Review Notes
+
+- The highest-risk runtime changes are in expression evaluation and planner
+ plumbing: custom functions are intentionally kept off the index-pushdown and
+ batch-fast-path planning assumptions in V1.
+- Remote hosts must register functions in the daemon/API host process; direct
+ clients can register functions locally through `DirectDatabaseOptions`, but
+ callback delegates are never serialized over HTTP or gRPC.
+- Admin Forms and Reports use the shared registries, but their formula and
+ automation surfaces remain narrower than SQL or stored macro systems:
+ formulas stay expression-focused, command hooks invoke host-owned code by
+ name, and declarative action sequences store only limited action metadata
+ rather than executable scripts in database metadata.
+- `SELECT ...` without `FROM` is represented internally as a single-row source
+ and is intended for scalar expressions that do not need row context.
+- `CanRunWithoutFrom` is currently discovery metadata for the Admin catalog and
+ SQL editor autocomplete; it is not yet a hard runtime denial gate for manually
+ typed tableless callback calls.
+
## v3.5.0
v3.5.0 focuses on the collection binary payload fast path, generated
diff --git a/docs/admin-collections-ui/README.md b/docs/admin-collections-ui/README.md
new file mode 100644
index 00000000..57e11a15
--- /dev/null
+++ b/docs/admin-collections-ui/README.md
@@ -0,0 +1,59 @@
+# Admin Collections UI
+
+## Summary
+
+The Admin app now treats document collections as first-class objects alongside
+tables, views, forms, and reports. The first implementation is a browser and
+JSON editor built on the existing collection client APIs:
+
+- list collection names
+- browse documents by page
+- fetch one document by exact key
+- create or update a document
+- delete a document
+
+This deliberately avoids new engine, HTTP, gRPC, or client contracts. Collection
+path-index management, document-content search, collection rename, and
+collection drop remain future work.
+
+## User Experience
+
+- Object Explorer has a `Collections` filter chip and group.
+- The collection group menu supports `New Collection...` and `Refresh`.
+- Collection item menus support `Open`, `New Document...`, and `Copy Name`.
+- The command palette includes `New Collection` plus one entry for each existing
+ collection.
+- Each collection opens in a dedicated `collection:{name}` tab using the same
+ toolbar, pager, grid, and detail-panel language as table data tabs.
+
+## Collection Tab Behavior
+
+- The grid shows row number, document key, JSON kind, and a compact preview.
+- The detail panel shows indented JSON for the selected document.
+- Existing document keys are read-only.
+- New documents require a nonblank key and valid JSON before save is enabled.
+- Save writes through `PutDocumentAsync(collectionName, key, document)`.
+- Delete writes through `DeleteDocumentAsync(collectionName, key)` after
+ confirmation.
+- Successful writes notify Admin change listeners and refresh the current page.
+- Exact-key lookup uses `GetDocumentAsync(collectionName, key)`.
+
+## Defaults
+
+- Default page size is `25`.
+- Supported page sizes are `10`, `25`, `50`, and `100`.
+- Collection names use the same simple identifier shape as the direct client:
+ `^[A-Za-z_][A-Za-z0-9_]*$`.
+- Deleting all documents leaves the collection itself in place because there is
+ no drop-collection API today.
+- Generated collection model metadata is not surfaced; documents are edited as
+ raw JSON.
+
+## Verification
+
+Run the focused Admin checks after collection UI changes:
+
+```powershell
+dotnet build src/CSharpDB.Admin/CSharpDB.Admin.csproj
+dotnet test tests/CSharpDB.Admin.Forms.Tests/CSharpDB.Admin.Forms.Tests.csproj
+```
diff --git a/docs/admin-forms-access-parity/README.md b/docs/admin-forms-access-parity/README.md
index 64070b14..f04845df 100644
--- a/docs/admin-forms-access-parity/README.md
+++ b/docs/admin-forms-access-parity/README.md
@@ -22,6 +22,16 @@ The current forms surface already includes:
- schema-change warnings
- designer undo/redo, copy/paste, duplicate, layers, alignment, tab order, and
mobile/tablet/desktop breakpoint editing
+- trusted host command registration for form lifecycle events
+- designer editing for form-level event bindings
+- command button controls that invoke trusted host commands
+- trusted command-backed selected-control events
+- declarative form action sequences for run-command, reusable sequence,
+ set-field, show-message, stop, built-in navigation, save, delete, refresh,
+ and go-to-record steps
+- conditional action steps and reusable named form action sequences
+- visual designer editing for form and selected-control action sequences
+- generated automation metadata for export/import host callback requirements
## Added Review Findings
@@ -96,9 +106,9 @@ Expected fix:
| Feature | Status | Notes |
| --- | --- | --- |
-| Command button control | Planned | Add buttons that can run form actions. |
-| Action model | Planned | Support actions such as open form, save, delete, navigate, apply filter, clear filter, run SQL/procedure, and show message. |
-| Event hooks | Planned | Add form/control events such as on load, before save, after save, before field change, after field change, and button click. |
+| Command button control | Partial | Trusted command buttons can invoke host-registered C# commands, action-only click sequences, reusable action sequences, and built-in rendered-form navigation/save/delete actions; richer button styling and command presets remain future work. |
+| Action model | Partial | Declarative action sequences support run-command, reusable sequence, set-field, show-message, stop, built-in navigation/save/delete/refresh/go-to, simple per-step conditions, visual designer editing, and generated automation metadata for form and selected-control events; open form, apply filter, clear filter, run SQL/procedure, loops, and conditional UI rules remain future work. |
+| Event hooks | Partial | Form lifecycle events, command-button clicks, and selected control events can call trusted commands; additional Access-style events remain future work. |
| Conditional UI rules | Planned | Add visible/enabled/read-only expressions for controls. |
### Phase 5: Broader Control and Property Coverage
@@ -124,3 +134,17 @@ workflows." The highest leverage model changes are:
- form-mode model
Those foundations should be added before expanding the control palette too far.
+
+## Developer Extensibility
+
+Custom form controls can now be registered without changing saved form JSON. See
+[Form Control Extensibility](form-control-extensibility.md) for the registry API,
+component contexts, generic property schema, and the sample rating control.
+
+Host-owned validation callbacks can be registered for field-level and form-level
+save checks. See
+[Trusted Validation Rules](../trusted-csharp-functions/validation-rules.md) for
+registration, designer metadata, policy, diagnostics, and sample code.
+
+For Access-style expression functions and macro/action candidates, see
+[Access-Style Functions and Macros](access-style-functions-and-macros.md).
diff --git a/docs/admin-forms-access-parity/access-style-functions-and-macros.md b/docs/admin-forms-access-parity/access-style-functions-and-macros.md
new file mode 100644
index 00000000..32f29b80
--- /dev/null
+++ b/docs/admin-forms-access-parity/access-style-functions-and-macros.md
@@ -0,0 +1,244 @@
+# Access-Style Functions and Macros
+
+This note captures the Access-parity function and macro set we want available
+in Admin Forms. The goal is not to clone every Access/VBA surface area at once.
+The goal is to include the small, familiar built-in set that makes form
+expressions, validation, conditional formatting, buttons, and simple workflows
+feel productive without requiring host code for every common task.
+
+## Design Direction
+
+- Keep simple expression functions built into the formula engine.
+- Keep user-facing form actions as declarative Admin Form actions where possible.
+- Route dangerous or host-specific actions through trusted callbacks, policy, and
+ diagnostics.
+- Treat database-owned C# code modules as the later `RunCode` target.
+- Preserve the existing saved form wire shape by storing action and expression
+ settings in metadata/property bags.
+
+Implementation note: Admin Forms formulas now include the expression functions
+listed below as built-ins. Macro/action commands remain roadmap items unless
+already covered by the existing form action model.
+
+## Included Expression Functions
+
+### Null and Conditional
+
+These should be first because they appear constantly in form defaults,
+calculated controls, validation messages, and visibility/enabled rules.
+
+| Function | Purpose | Priority |
+| --- | --- | --- |
+| `Nz(value, fallback)` | Replace null/empty values with a fallback. | V1 |
+| `IsNull(value)` | Test for null. | V1 |
+| `IsEmpty(value)` | Test for empty/unset values where applicable. | V1 |
+| `IIf(condition, trueValue, falseValue)` | Inline conditional. | V1 |
+| `Switch(condition1, value1, ...)` | Multi-branch conditional. | V2 |
+| `Choose(index, value1, value2, ...)` | Pick from positional values. | V2 |
+
+### Text
+
+| Function | Purpose | Priority |
+| --- | --- | --- |
+| `Len(value)` | Text length. | V1 |
+| `Left(value, count)` | Left substring. | V1 |
+| `Right(value, count)` | Right substring. | V1 |
+| `Mid(value, start, count)` | Middle substring. | V1 |
+| `Trim(value)` | Trim both ends. | V1 |
+| `LTrim(value)` | Trim left. | V2 |
+| `RTrim(value)` | Trim right. | V2 |
+| `UCase(value)` | Uppercase text. | V1 |
+| `LCase(value)` | Lowercase text. | V1 |
+| `InStr(value, search)` | Find substring position. | V1 |
+| `Replace(value, search, replacement)` | Replace text. | V1 |
+| `StrComp(left, right, comparison)` | Compare strings. | V2 |
+| `Val(value)` | Parse leading numeric text. | V2 |
+
+### Date and Time
+
+| Function | Purpose | Priority |
+| --- | --- | --- |
+| `Date()` | Current date. | V1 |
+| `Time()` | Current time. | V1 |
+| `Now()` | Current date/time. | V1 |
+| `Year(value)` | Extract year. | V1 |
+| `Month(value)` | Extract month number. | V1 |
+| `Day(value)` | Extract day of month. | V1 |
+| `Hour(value)` | Extract hour. | V2 |
+| `Minute(value)` | Extract minute. | V2 |
+| `Second(value)` | Extract second. | V2 |
+| `DateAdd(interval, amount, value)` | Add date/time interval. | V1 |
+| `DateDiff(interval, start, end)` | Difference between dates. | V1 |
+| `DatePart(interval, value)` | Extract date/time part. | V2 |
+| `DateSerial(year, month, day)` | Construct date. | V2 |
+| `TimeSerial(hour, minute, second)` | Construct time. | V2 |
+| `Weekday(value)` | Day of week number. | V2 |
+| `MonthName(month)` | Month display name. | V2 |
+
+### Number and Conversion
+
+| Function | Purpose | Priority |
+| --- | --- | --- |
+| `Abs(value)` | Absolute value. | V1 |
+| `Round(value, digits)` | Round number. | V1 |
+| `Int(value)` | Floor-like integer conversion. | V1 |
+| `Fix(value)` | Truncate toward zero. | V2 |
+| `Sgn(value)` | Sign of number. | V2 |
+| `CStr(value)` | Convert to string. | V1 |
+| `CInt(value)` | Convert to integer. | V1 |
+| `CLng(value)` | Convert to long integer. | V2 |
+| `CDbl(value)` | Convert to double. | V1 |
+| `CBool(value)` | Convert to boolean. | V1 |
+| `CDate(value)` | Convert to date/time. | V1 |
+| `Format(value, format)` | Format date/number/text. | V2 |
+
+### Domain Aggregates
+
+Domain aggregate functions are important for Access familiarity, but they need a
+careful implementation because they read other rows/tables during form
+evaluation.
+
+| Function | Purpose | Priority |
+| --- | --- | --- |
+| `DLookup(expr, domain, criteria)` | Read one value from a table/query. | V2 |
+| `DCount(expr, domain, criteria)` | Count matching rows. | V2 |
+| `DSum(expr, domain, criteria)` | Sum matching rows. | V2 |
+| `DAvg(expr, domain, criteria)` | Average matching rows. | V2 |
+| `DMin(expr, domain, criteria)` | Minimum matching value. | V2 |
+| `DMax(expr, domain, criteria)` | Maximum matching value. | V2 |
+
+V2 should enforce query/table access through the same callback and diagnostics
+boundary used for trusted extensions where relevant. These functions should also
+have row limits and clear error handling so formula evaluation cannot become an
+unbounded database workload.
+
+## Included Macro and Action Commands
+
+### Record Actions
+
+These map cleanly to existing form runtime behavior and should remain
+declarative actions rather than host callbacks.
+
+| Action | Purpose | Priority |
+| --- | --- | --- |
+| `NewRecord` | Start a new record. | V1 |
+| `SaveRecord` | Save the current record. | V1 |
+| `DeleteRecord` | Delete the current record. | V1 |
+| `UndoRecord` | Revert unsaved edits. | V1 |
+| `RefreshRecord` | Reload the current record. | V1 |
+| `Requery` | Reload the form record source. | V1 |
+| `GoToRecord` | Navigate to a specific record. | V1 |
+| `FindRecord` | Search/navigate by criteria. | V2 |
+| `NextRecord` | Navigate forward. | V1 |
+| `PreviousRecord` | Navigate backward. | V1 |
+
+### Form, Window, and Report Actions
+
+| Action | Purpose | Priority |
+| --- | --- | --- |
+| `OpenForm` | Open another saved form. | V1 |
+| `CloseForm` | Close current or named form. | V1 |
+| `OpenReport` | Open a saved report. | V2 |
+| `CloseReport` | Close a report surface. | V2 |
+| `PreviewReport` | Open report preview. | V2 |
+| `PrintReport` | Print or export through report pipeline. | V2 |
+
+### Filter and Sort Actions
+
+| Action | Purpose | Priority |
+| --- | --- | --- |
+| `ApplyFilter` | Apply a form filter. | V1 |
+| `ClearFilter` | Clear current filter. | V1 |
+| `SetOrderBy` | Apply sort order. | V1 |
+| `ClearOrderBy` | Clear sort order. | V1 |
+| `SearchRecords` | Search over configured searchable fields. | V2 |
+
+### UI and Control Actions
+
+These are needed for Access-style command buttons and conditional workflows.
+
+| Action | Purpose | Priority |
+| --- | --- | --- |
+| `SetValue` | Set a field/control value. | V1 |
+| `SetProperty` | Set visible/enabled/read-only/text/style properties. | V1 |
+| `SetFocus` | Move focus to a control. | V1 |
+| `EnableControl` | Set enabled state. | V1 |
+| `DisableControl` | Clear enabled state. | V1 |
+| `ShowControl` | Set visible state. | V1 |
+| `HideControl` | Clear visible state. | V1 |
+| `LockControl` | Set read-only state. | V1 |
+| `UnlockControl` | Clear read-only state. | V1 |
+| `MsgBox` | Show a message dialog. | V1 |
+| `InputBox` | Prompt for a value. | V2 |
+
+### Flow Actions
+
+| Action | Purpose | Priority |
+| --- | --- | --- |
+| `If` / `Else` | Conditional action branching. | V1 |
+| `StopMacro` | Stop the current action sequence. | V1 |
+| `RunMacro` | Run a named action sequence. | V1 |
+| `RunActionSequence` | Existing explicit reusable sequence action. | V1 |
+| `OnError` | Configure failure handling. | V2 |
+
+### Data and Query Actions
+
+These must be gated carefully because they can mutate data outside the current
+form record.
+
+| Action | Purpose | Priority |
+| --- | --- | --- |
+| `OpenQuery` | Open a saved query result. | V2 |
+| `RunQuery` | Execute a saved query. | V2 trusted |
+| `RunProcedure` | Execute a saved procedure. | V2 trusted |
+| `RunSQL` | Execute SQL text. | Later trusted |
+| `ExportData` | Export records. | Later trusted |
+| `ImportData` | Import records. | Later trusted |
+
+### Code Bridge Actions
+
+| Action | Purpose | Priority |
+| --- | --- | --- |
+| `RunCommand` | Invoke host-registered trusted command callback. | Existing |
+| `RunCode` | Invoke database-owned C# code module function. | Later |
+
+`RunCode` should wait for the database code modules work. It should compile and
+execute database-owned C# through a trusted build/runtime model, not arbitrary
+source text embedded directly inside form JSON.
+
+### Temp and Session Variables
+
+| Action | Purpose | Priority |
+| --- | --- | --- |
+| `SetTempVar` | Store a session-scoped value. | V2 |
+| `RemoveTempVar` | Remove one session value. | V2 |
+| `RemoveAllTempVars` | Clear session values. | V2 |
+
+Temp variables should be scoped to the current user/session and be available to
+form expressions, filters, and action sequences.
+
+## Recommended Implementation Order
+
+1. Add core formula functions: `Nz`, `IIf`, `Date`, `Now`, common text helpers,
+ numeric helpers, and conversions.
+2. Add declarative form actions: `MsgBox`, `SetValue`, `SetProperty`,
+ `SetFocus`, `ApplyFilter`, `ClearFilter`, `Requery`, `OpenForm`, and
+ `CloseForm`.
+3. Add domain aggregates with clear access checks, row limits, and diagnostics.
+4. Add temp/session variables and make them visible to expressions and actions.
+5. Add `RunCode` after database code modules exist.
+6. Add import/export/file/app-launch style actions only as trusted operations.
+
+## Notes on Access Compatibility
+
+Microsoft Access exposes many functions and macro commands. CSharpDB should use
+the familiar names where it makes sense, but implementation should follow the
+CSharpDB security and diagnostics model. Anything that can call host code,
+modify unrelated data, access files, or run arbitrary SQL should go through an
+explicit trusted boundary.
+
+Useful Microsoft references:
+
+- [Access functions by category](https://support.microsoft.com/en-us/office/access-functions-by-category-b8b136c3-2716-4d39-94a2-658ce330ed83)
+- [Introduction to macros](https://support.microsoft.com/en-gb/office/introduction-to-macros-a39c2a26-e745-4957-8d06-89e0b435aac3)
+- [Access macro commands](https://learn.microsoft.com/en-ie/office/client-developer/access/desktop-database-reference/macro-commands)
diff --git a/docs/admin-forms-access-parity/form-control-extensibility.md b/docs/admin-forms-access-parity/form-control-extensibility.md
new file mode 100644
index 00000000..178bea87
--- /dev/null
+++ b/docs/admin-forms-access-parity/form-control-extensibility.md
@@ -0,0 +1,154 @@
+# Form Control Extensibility
+
+Admin Forms controls are persisted as `ControlDefinition.ControlType` plus a
+free-form `PropertyBag`. The extensibility registry turns that existing wire
+shape into a developer API: a host can add designer toolbox entries, placement
+defaults, property editing, designer previews, and runtime rendering without
+changing saved form JSON.
+
+## Registration
+
+Register built-ins with `AddCSharpDbAdminForms()`, then add custom controls with
+`AddCSharpDbAdminFormControls(...)`:
+
+```csharp
+using CSharpDB.Admin.Forms.Models;
+using CSharpDB.Admin.Forms.Services;
+
+builder.Services.AddCSharpDbAdminForms();
+builder.Services.AddCSharpDbAdminFormControls(controls =>
+{
+ controls.Add(new FormControlDescriptor
+ {
+ ControlType = "rating",
+ DisplayName = "Rating",
+ ToolboxGroup = "Custom",
+ IconText = "*",
+ DefaultWidth = 220,
+ DefaultHeight = 48,
+ SupportsBinding = true,
+ ParticipatesInTabOrder = true,
+ DefaultProps = new Dictionary
+ {
+ ["max"] = 5,
+ ["displayMode"] = "buttons",
+ },
+ DesignerPreviewComponentType = typeof(RatingDesignerPreview),
+ RuntimeComponentType = typeof(RatingRuntimeControl),
+ PropertyEditorComponentType = typeof(RatingPropertyEditor),
+ });
+});
+```
+
+The same registration must exist anywhere the form is designed or rendered.
+Unknown control types are preserved in form metadata and render as placeholders.
+
+## Descriptor Fields
+
+`FormControlDescriptor` defines the designer and runtime contract:
+
+- `ControlType`: persisted control type string.
+- `DisplayName`, `ToolboxGroup`, `IconText`, `Description`: toolbox and labels.
+- `DefaultWidth`, `DefaultHeight`, `DefaultProps`: new-control placement.
+- `SupportsBinding`: whether placement and the inspector create a field binding.
+- `ParticipatesInTabOrder`: whether the control joins tab-order editing.
+- `PropertyDescriptors`: generic property fields for simple custom props.
+- `DesignerPreviewComponentType`: optional Blazor preview component.
+- `RuntimeComponentType`: optional Blazor data-entry component.
+- `PropertyEditorComponentType`: optional custom property editor.
+- `ReplaceBuiltInRuntime`: lets a host explicitly replace a built-in runtime
+ renderer while keeping the built-in designer metadata.
+
+Duplicate custom control types fail when the registry is resolved. Built-in
+runtime replacement also fails unless `ReplaceBuiltInRuntime = true`.
+
+## Component Contexts
+
+Custom components receive a single `Context` parameter.
+
+Designer previews use `FormControlDesignContext`:
+
+```csharp
+[Parameter, EditorRequired]
+public FormControlDesignContext Context { get; set; } = default!;
+```
+
+Runtime controls use `FormControlRuntimeContext`:
+
+```csharp
+[Parameter, EditorRequired]
+public FormControlRuntimeContext Context { get; set; } = default!;
+```
+
+The runtime context includes the form, control metadata, current record, bound
+field/value, resolved choices, enabled/read-only state, validation error,
+tab-index, `SetValueAsync`, and `DispatchEventAsync`.
+
+Custom property editors use `FormControlPropertyContext`:
+
+```csharp
+[Parameter, EditorRequired]
+public FormControlPropertyContext Context { get; set; } = default!;
+```
+
+Use `Context.SetPropertyAsync(name, value)` to write into the control
+`PropertyBag`.
+
+## Generic Property Schema
+
+For simple controls, skip a custom property editor and define
+`PropertyDescriptors`:
+
+```csharp
+PropertyDescriptors =
+[
+ new FormControlPropertyDescriptor
+ {
+ Name = "max",
+ Label = "Maximum Rating",
+ Editor = FormControlPropertyEditor.Number,
+ DefaultValue = 5,
+ HelpText = "Allowed values are 1 through 10.",
+ },
+ new FormControlPropertyDescriptor
+ {
+ Name = "displayMode",
+ Label = "Display Mode",
+ Editor = FormControlPropertyEditor.Select,
+ Options =
+ [
+ new FormControlPropertyOption("buttons", "Buttons"),
+ new FormControlPropertyOption("compact", "Compact"),
+ ],
+ },
+];
+```
+
+Generic editors support `Text`, `TextArea`, `Number`, `Checkbox`, and `Select`.
+
+## Sample Rating Control
+
+The Admin host includes a compiled sample custom control at:
+
+- `src/CSharpDB.Admin/Components/Samples/FormControls/RatingDesignerPreview.razor`
+- `src/CSharpDB.Admin/Components/Samples/FormControls/RatingRuntimeControl.razor`
+- `src/CSharpDB.Admin/Components/Samples/FormControls/RatingPropertyEditor.razor`
+- `src/CSharpDB.Admin/Components/Samples/FormControls/SampleRatingControlRegistration.cs`
+
+It is disabled by default. Enable it for local testing with:
+
+```powershell
+$env:AdminForms__EnableSampleControls = 'true'
+dotnet run --project src\CSharpDB.Admin --urls http://127.0.0.1:61818
+```
+
+The sample registers `sampleRating` under the `Custom` toolbox group. It binds to
+a scalar field, writes the selected numeric rating through `SetValueAsync`, and
+dispatches click events with runtime arguments.
+
+## Current Limits
+
+V1 custom controls are leaf controls. They can be placed inside existing
+`tabControl` pages, but custom containers do not own or render child controls
+yet. Custom components must be compiled into the host app or referenced
+assemblies; form JSON never loads arbitrary component assemblies.
diff --git a/docs/admin-reports-access-parity/README.md b/docs/admin-reports-access-parity/README.md
index 1195d992..f03f1956 100644
--- a/docs/admin-reports-access-parity/README.md
+++ b/docs/admin-reports-access-parity/README.md
@@ -23,6 +23,8 @@ The current reports surface already includes:
- preview pagination with page headers and footers
- print support through the browser
- schema-drift warnings
+- trusted command-backed preview lifecycle events for `OnOpen`,
+ `BeforeRender`, and `AfterRender`
## Added Review Findings
@@ -120,7 +122,7 @@ Expected fix:
| Feature | Status | Notes |
| --- | --- | --- |
-| Email/report delivery | Planned | Export and attach reports, with host-provided delivery hooks. |
+| Email/report delivery | Planned | Export and attach reports; trusted `AfterRender` commands provide an initial host callback but not a full delivery pipeline. |
| Scheduled reports | Research | Run recurring reports and store generated artifacts. |
| Report artifact history | Research | Store generated report snapshots for auditing and re-download. |
| Large-report cancellation | Planned | Add cancellation/progress for long render/export jobs. |
diff --git a/docs/api-sharding.md b/docs/api-sharding.md
new file mode 100644
index 00000000..11bf2a06
--- /dev/null
+++ b/docs/api-sharding.md
@@ -0,0 +1,190 @@
+# API-Level Sharding Plan
+
+This is a research and design proposal. API-level sharding is not a shipped
+CSharpDB feature today.
+
+## Goal
+
+The first target for sharding is write throughput. CSharpDB's durable commit
+path is isolated to a database file and its WAL, so multiple independent
+database files can spread write pressure across separate commit paths while the
+API decides where each request belongs.
+
+The recommended v1 shape is a routing layer above the existing client and
+daemon surfaces, not a distributed pager or storage-engine rewrite.
+
+## Recommended V1 Design
+
+- Run one warm `ICSharpDbClient` per shard.
+- Treat each shard as a normal CSharpDB database file with its own `.db` file,
+ WAL, checkpoints, maintenance lifecycle, and storage options.
+- Add a shard catalog that maps a stable shard key such as `tenantId` or
+ `accountId` to a shard identity and database path or endpoint.
+- Require a shard key for writes and point reads.
+- Keep write transactions scoped to one shard.
+- Do not support cross-shard transactions in v1.
+- Add read-only fan-out later for admin, diagnostics, and reporting workloads
+ that explicitly need all shards.
+
+Conceptually:
+
+```text
+HTTP/gRPC/API request
+ |
+ v
+Shard key extraction
+ |
+ v
+IShardRouter
+ |
+ +--> shard-a.db / ICSharpDbClient
+ +--> shard-b.db / ICSharpDbClient
+ +--> shard-c.db / ICSharpDbClient
+```
+
+## Proposed Interfaces
+
+These names describe the intended public shape. They are not implemented yet.
+
+```csharp
+public interface IShardRouter
+{
+ ValueTask ResolveAsync(
+ string shardKey,
+ CancellationToken cancellationToken = default);
+}
+
+public sealed record ShardRoute(
+ string ShardId,
+ string DataSource,
+ ICSharpDbClient Client);
+
+public interface IShardedCSharpDbClient
+{
+ ValueTask GetClientAsync(
+ string shardKey,
+ CancellationToken cancellationToken = default);
+}
+```
+
+Two request shapes are worth considering:
+
+- Header-based routing: `X-CSharpDB-Shard-Key: tenant-0042`
+- Route-based routing: `/api/{tenantId}/tables/...`
+
+Header-based routing preserves the existing URL layout. Route-based routing is
+more visible and easier to test manually. Either way, the API should fail fast
+when a sharded operation does not include a shard key.
+
+## Shard Key Guidance
+
+Choose the shard key before implementation. Changing it later usually requires
+moving most or all data.
+
+Good shard keys are:
+
+- Stable and immutable.
+- High-cardinality, such as tenant, account, organization, or workspace IDs.
+- Present on the dominant write and point-read requests.
+- Shared by related tables and collections so normal application workflows stay
+ on a single shard.
+
+Avoid shard keys that are:
+
+- Auto-incrementing IDs.
+- Sequential timestamps.
+- Booleans or low-cardinality enums.
+- Values users commonly edit.
+- Attributes that do not appear in normal request filters.
+
+For multitenant data, prefer colocating related tenant data in the same shard.
+That means tenant-owned tables and collections should include the tenant key,
+and API calls should route by that same key.
+
+## Query And Transaction Rules
+
+V1 should make single-shard behavior explicit:
+
+- `INSERT`, `UPDATE`, `DELETE`, document writes, point reads, and explicit
+ transactions require one shard key and execute on one shard.
+- Raw SQL execution requires a shard key unless the endpoint is explicitly
+ marked as an all-shards read-only operation.
+- Cross-shard joins are out of scope for v1.
+- Cross-shard transactions are out of scope for v1.
+- Global reads should use an explicit fan-out API that returns per-shard
+ results and partial-failure metadata.
+
+This keeps the write-throughput feature honest: each shard can commit
+independently without a distributed transaction coordinator.
+
+## Shard Catalog
+
+Start with a small catalog that can be loaded at daemon/API startup:
+
+```json
+{
+ "strategy": "lookup",
+ "shards": [
+ { "id": "shard-a", "dataSource": "data/shard-a.db" },
+ { "id": "shard-b", "dataSource": "data/shard-b.db" }
+ ],
+ "routes": [
+ { "key": "tenant-0001", "shardId": "shard-a" },
+ { "key": "tenant-0002", "shardId": "shard-b" }
+ ]
+}
+```
+
+A lookup strategy is the best first step because it supports deliberate tenant
+placement and later tenant movement. A later version can introduce virtual
+shards:
+
+```text
+tenantId -> virtual shard -> physical shard
+```
+
+Virtual shards make rebalancing less disruptive because a new physical shard can
+take ownership of some virtual shards without changing application routing code.
+
+## Operations
+
+Sharding multiplies operational work. Each operation should report per-shard
+status instead of hiding failures behind one aggregate result.
+
+- Backup: back up every shard independently and record the shard id, database
+ path, timestamp, and result.
+- Restore: restore one shard at a time unless an explicit all-shards restore
+ workflow is added.
+- Reindex, vacuum, checkpoint, and inspect: support one shard or all shards with
+ per-shard output.
+- Schema changes: apply DDL to all shards through an orchestrated migration
+ command and capture success/failure per shard.
+- Monitoring: aggregate size, WAL, checkpoint, error, latency, and throughput
+ metrics by shard.
+- Rebalancing: move one tenant or virtual shard at a time, block or redirect
+ writes during the move, validate row/document counts, update the catalog, and
+ refresh router caches.
+
+## Open Questions
+
+- Should the first catalog live in JSON configuration, a control CSharpDB
+ database, or both?
+- Should API routing prefer headers, route prefixes, or support both?
+- Should raw SQL require a shard key only, or should the API also validate that
+ the SQL filters by the shard key?
+- Should fan-out reads return one combined result set, per-shard result sets, or
+ both?
+- How should Admin UI expose shard selection and all-shards maintenance tasks?
+
+## Suggested Rollout
+
+1. Add configuration models for shard definitions and routing strategy.
+2. Add an `IShardRouter` implementation backed by the configured catalog.
+3. Add a sharded client wrapper that opens and owns one warm `ICSharpDbClient`
+ per shard.
+4. Add shard-key extraction to API/daemon endpoints for writes and point reads.
+5. Add one-shard maintenance operations, then all-shards maintenance with
+ per-shard results.
+6. Add benchmarks comparing one database file versus multiple routed shards for
+ independent tenant write workloads.
+
diff --git a/docs/configuration.md b/docs/configuration.md
index c0c40866..64f77182 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -45,6 +45,7 @@ await using var db = await Database.OpenHybridAsync("mydata.db",
```
DatabaseOptions
├── ImplicitInsertExecutionMode
+├── Functions
├── StorageEngineOptions
│ ├── DurabilityMode
│ ├── DurableGroupCommit
@@ -86,6 +87,7 @@ Top-level database composition and execution-shape configuration.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `ImplicitInsertExecutionMode` | `ImplicitInsertExecutionMode` | `Serialized` | Controls whether shared auto-commit `INSERT` statements stay behind the legacy database write gate or run as isolated `WriteTransaction` commits. This does not disable the explicit multi-writer `WriteTransaction` APIs. |
+| `Functions` | `DbFunctionRegistry` | `DbFunctionRegistry.Empty` | Trusted in-process scalar functions available to SQL and embedded expression surfaces. See [Trusted C# Scalar Functions](trusted-csharp-functions/README.md). |
| `StorageEngineOptions` | `StorageEngineOptions` | default instance | Storage engine durability, pager, WAL, and checkpoint settings |
| `StorageEngineFactory` | `IStorageEngineFactory` | `DefaultStorageEngineFactory` | Factory used to compose the backing storage engine |
diff --git a/docs/query-and-durable-write-performance/README.md b/docs/query-and-durable-write-performance/README.md
index 2f35ce79..5f14e77c 100644
--- a/docs/query-and-durable-write-performance/README.md
+++ b/docs/query-and-durable-write-performance/README.md
@@ -42,7 +42,7 @@ This note tracks the combined optimizer phase-2 and durable-write completion wor
- The remaining phase-4 write-path question is now narrower than "shared auto-commit in general":
- non-insert shared auto-commit fan-in is working
- hot insert auto-commit still needs a dedicated design decision if we want it to coalesce without reopening structural conflict costs
-- Async I/O batching still has room for more auditing outside the WAL hot path, but the main write-path batching pieces are already in place.
+- Async I/O batching still has room for more auditing outside the WAL hot path, but the main write-path batching pieces are already in place. See [Async I/O Batching Follow-Up](async-io-batching-follow-up.md).
## Phase 4 Status
diff --git a/docs/query-and-durable-write-performance/async-io-batching-follow-up.md b/docs/query-and-durable-write-performance/async-io-batching-follow-up.md
new file mode 100644
index 00000000..65c104bb
--- /dev/null
+++ b/docs/query-and-durable-write-performance/async-io-batching-follow-up.md
@@ -0,0 +1,51 @@
+# Async I/O Batching Follow-Up
+
+This note captures the remaining work behind the roadmap item currently marked
+`In Progress`.
+
+## Current Shipped State
+
+The hot storage write path already has the main batching pieces in place:
+
+- WAL commits can append dirty pages through `AppendFramesAndCommitAsync(...)`.
+- Repeated `AppendFrameAsync(...)` calls inside a transaction are staged and emitted as chunked WAL writes during `CommitAsync(...)`.
+- Checkpoint copying batches contiguous page writes back into the main database file.
+- `SaveToFileAsync(...)` and backup-style snapshot copies use `StorageDeviceCopyBatcher`.
+- Vacuum and foreign-key migration rewrites share `BTreeCopyUtility` instead of each owning a separate row-copy loop.
+
+## Remaining Work
+
+The remaining work is an audit and measurement pass, not a missing core WAL batching primitive.
+
+1. Audit non-hot rewrite/export paths:
+ - Backup and restore: `DatabaseBackupCoordinator`, `Database.SaveToFileAsync(...)`, `Pager.SaveToFileAsync(...)`.
+ - Vacuum: `DatabaseMaintenanceCoordinator.CopyDatabaseAsync(...)`.
+ - Foreign-key migration rewrites: `DatabaseForeignKeyMigrationCoordinator.ApplyPlanAsync(...)`.
+ - Storage diagnostics and inspectors: `WalInspector`, `InspectorEngine`, and large sequential read paths.
+ - Admin/UI export helpers only if they prove relevant to large database-file movement.
+
+2. Decide whether `BTreeCopyUtility` should stay a logical row-copy helper or grow batching behavior:
+ - Current behavior is cursor-read plus per-row `InsertAsync(...)`.
+ - Possible follow-up: add row/page-local batching, reusable buffers, or progress hooks.
+ - Avoid direct B-tree page copying unless catalog, root-page, schema, freelist, row-count, and index invariants are explicitly preserved.
+
+3. Add benchmark/diagnostic coverage before optimizing further:
+ - Large backup snapshot.
+ - Large restore staging.
+ - Large vacuum rewrite.
+ - Large foreign-key migration rewrite.
+ - Inspector/diagnostic scans over large DB/WAL files.
+
+4. Define completion criteria:
+ - The audit identifies every large sequential file-copy and logical table-copy path.
+ - Each path is classified as already batched, intentionally unbatched, or worth optimizing.
+ - Any optimized path has before/after timings and no crash-recovery or integrity regression.
+
+## Non-Goals
+
+These are separate from this follow-up:
+
+- Durable group commit policy changes.
+- Hot single-row insert fan-in.
+- Public histogram/planner inspection.
+- Query row-batch execution kernel specialization.
diff --git a/docs/releases/v3.6.0-pr-notes.md b/docs/releases/v3.6.0-pr-notes.md
new file mode 100644
index 00000000..c2e71fca
--- /dev/null
+++ b/docs/releases/v3.6.0-pr-notes.md
@@ -0,0 +1,125 @@
+## Summary
+
+This `v3.6.0` release adds trusted in-process C# callbacks across the main
+CSharpDB expression and automation surfaces, then rounds that work out with
+tableless scalar `SELECT` support, built-in scalar functions, callback catalog
+metadata, and Admin discovery/autocomplete polish.
+
+Host applications can now register trusted scalar functions and commands in
+ordinary C# code. SQL, triggers, procedures, Admin Forms formulas, Admin
+Reports calculated expressions, and pipeline expressions can call registered
+scalar functions. Admin Forms, Admin Reports, and pipelines can also dispatch
+trusted host commands from supported event and hook surfaces without storing C#
+source in database metadata.
+
+The release also adds declarative Admin Forms action sequences, command buttons,
+selected-control events, reusable named actions, Access-style formula helpers,
+and generated automation metadata so portable forms/reports/packages can
+declare which host callbacks they expect.
+
+The final stabilization pass adds `SELECT ...` without `FROM`, built-in scalar
+function evaluation for common text/date/number/null helpers, a
+`CanRunWithoutFrom` callback metadata flag, SQL editor completion for built-ins
+and tableless-safe callbacks, an Internal/External callback catalog split, and
+Admin artifact cleanup so stale generated snapshots do not slow local builds.
+
+## Type of Change
+
+- [x] Bug fix
+- [x] New feature
+- [ ] Breaking change
+- [x] Documentation update
+- [x] Refactor / maintenance
+- [ ] Tests only
+
+## Related Issues
+
+No issue numbers were linked for this release branch. Included work:
+
+- Added trusted scalar function registration through `DbFunctionRegistry` and
+ `DatabaseOptions.Functions`.
+- Added trusted command registration through `DbCommandRegistry`, async command
+ support, command timeouts, and shared command argument conversion.
+- Added Admin Forms form lifecycle events, selected-control events, command
+ buttons, reusable action sequences, built-in record actions, and conditional
+ action steps.
+- Added Admin Reports preview lifecycle command events.
+- Added pipeline run hooks for trusted host commands.
+- Added generated automation metadata for Forms, Reports, and pipeline packages.
+- Added the trusted C# host sample for local callback development and debugging.
+- Added tableless `SELECT` support through a single-row planner source.
+- Added built-in scalar functions and SQL/Admin Forms formula coverage for
+ common Access-style text, date/time, numeric, conversion, and null helpers.
+- Added `CanRunWithoutFrom` metadata for scalar callbacks and used it for SQL
+ editor discovery/autocomplete.
+- Split the Admin callback catalog into Internal and External scopes.
+- Added Admin artifact cleanup through MSBuild and the direct Admin launcher.
+- Added async I/O batching follow-up documentation for the remaining non-hot
+ export/rewrite-path audit.
+
+## Testing
+
+- [x] `dotnet build CSharpDB.slnx`
+- [x] Relevant tests executed
+- [x] Failure-path tests executed (if applicable: cancellation, invalid/unsupported inputs, non-`DbException` paths)
+- [x] Manual verification performed (if applicable)
+
+Validation performed:
+
+- `dotnet restore CSharpDB.slnx`
+- `.\scripts\Test-NoLegacyCoreReferences.ps1`
+ - Passed through the script's PowerShell fallback after the local packaged
+ `rg.exe` could not be launched normally in this desktop environment.
+- `dotnet build CSharpDB.slnx -c Release --no-restore`
+ - Passed with `0` warnings and `0` errors.
+- `dotnet test CSharpDB.slnx -c Release --no-build -m:1 -- RunConfiguration.DisableParallelization=true`
+ - Non-parallel unit test run passed with `1,663` tests.
+- Multiple debug non-parallel validation passes were run during the Forms,
+ Reports, pipeline, command timeout, built-in action, and conditional action
+ phases; the final pre-tableless pass reached `1,715` tests.
+- `dotnet pack` smoke for release workflow packages with `-p:Version=3.6.0`
+ produced `11` local packages.
+- `.\scripts\Publish-CSharpDbDaemonRelease.ps1 -Version 3.6.0 -Runtime win-x64 -OutputRoot artifacts\daemon-release-local`
+ produced `csharpdb-daemon-v3.6.0-win-x64.zip` and `SHA256SUMS.txt`.
+- Latest tableless/callback stabilization validation:
+ - `dotnet test .\CSharpDB.slnx -m:1 --no-restore -v:minimal /nr:false /p:UseSharedCompilation=false /p:TestTfmsInParallel=false -- RunConfiguration.DisableParallelization=true`
+ - Passed with `1,877` tests.
+
+Benchmark validation:
+
+- Same-machine affected benchmark comparison against the pre-feature HEAD
+ baseline showed no material regression in the main write/query guardrails.
+- The only notable row was the synthetic `BatchEvaluation` delegate
+ filter/projection case at `+10.53%`; its paired specialized path improved by
+ `-10.36%`, allocations were unchanged, and the affected guardrail suites were
+ otherwise neutral to improved.
+
+## Checklist
+
+- [x] I followed the project style and conventions.
+- [x] I added or updated tests for behavior changes.
+- [x] I covered both success and failure paths for changed behavior.
+- [x] I updated docs for user-facing changes.
+- [x] I verified no sensitive data was added.
+
+## Notes for Reviewers
+
+- The highest-risk runtime changes are in expression evaluation and planner
+ plumbing. Custom functions are intentionally kept off index-pushdown and
+ batch-fast-path planning assumptions in V1.
+- Remote hosts must register functions and commands in the daemon/API host
+ process. Direct clients can register callbacks locally, but delegates are
+ never serialized over HTTP or gRPC.
+- `SELECT ...` without `FROM` is represented internally as a single-row source.
+ This is intended for scalar expressions and callbacks that do not need row
+ context.
+- `CanRunWithoutFrom` is discovery metadata used by the Admin catalog and SQL
+ autocomplete. It does not yet act as a hard runtime denial gate for manually
+ typed tableless callback calls.
+- Admin Forms and Reports use the shared registries, but their formula and
+ automation surfaces remain intentionally narrower than a stored macro/code
+ system. Form/report/pipeline metadata stores names, arguments, and action
+ definitions, not executable C# source.
+- Built-in scalar functions now cover common helpers, but aggregate UDFs,
+ table-valued UDFs, native plugins, database-owned C# modules, and sandboxed
+ UDFs remain future work.
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 084b48a2..6b95d87a 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -42,7 +42,7 @@ SQL feature parity, provider/tooling compatibility, and ecosystem expansion.
| Feature | Description | Status |
|---------|-------------|--------|
-| **User-defined functions** | Broader built-in scalar function registry (UPPER, ABS, COALESCE, etc.), user-registered C# functions, native plugin extensions | Planned |
+| **User-defined functions and commands** | Trusted in-process C# scalar functions are implemented for SQL, triggers/procedures, direct clients, Admin Forms/Reports, and pipelines; trusted commands now back Admin Forms lifecycle events, command-button clicks, selected control events, Admin Reports render lifecycle events, and pipeline run hooks; Admin Forms now have declarative action sequences for run-command, set-field, show-message, and stop steps. Broader built-in scalar functions, native plugin extensions, aggregate/table-valued UDFs, richer macro flow, additional Access-style control events, and sandboxed UDFs remain future work | Partial |
| **Subqueries** | Scalar subqueries, `IN (SELECT ...)`, `EXISTS (SELECT ...)`, including correlated evaluation in `WHERE`, non-aggregate projection, and `UPDATE`/`DELETE` expressions | Done |
| **`UNION` / `INTERSECT` / `EXCEPT`** | Set operations across SELECT results, including use in top-level queries, views, and CTE query bodies | Done |
| **Window functions** | `ROW_NUMBER()`, `RANK()`, `DENSE_RANK()`, `LEAD()`, `LAG()` | Planned |
@@ -56,8 +56,8 @@ SQL feature parity, provider/tooling compatibility, and ecosystem expansion.
| **NuGet package** | Publish and maintain `CSharpDB.Engine`, `CSharpDB.Data`, `CSharpDB.Client`, and `CSharpDB.Primitives` as the primary NuGet packages | Done |
| **Connection pooling** | Pool underlying direct embedded sessions behind `CSharpDbConnection` to amortize open/close cost | Done |
| **Admin dashboard improvements** | Richer SQL editor UX, query history, deeper diagnostics, and integrated Forms/Reports tooling beyond the core schema/procedure/storage surface | Done |
-| **Admin Forms Access parity** | Close the highest-impact Access-style form gaps: runtime responsive layouts, full inferred validation enforcement, richer record-source/filter/sort models, Layout View, form modes, command/action events, and broader control coverage | Planned |
-| **Admin Reports Access parity** | Close the highest-impact Access-style report gaps: bounded saved-query previews, full report rendering/export, parameter/filter prompts, richer grouping/totals options, Layout View, conditional formatting, subreports, and broader report controls | Planned |
+| **Admin Forms Access parity** | Close the highest-impact Access-style form gaps: runtime responsive layouts, full inferred validation enforcement, richer record-source/filter/sort models, Layout View, form modes, broader action/event coverage, and broader control coverage; trusted command-backed form lifecycle events, command buttons, and selected control events are now started | Partial |
+| **Admin Reports Access parity** | Close the highest-impact Access-style report gaps: bounded saved-query previews, full report rendering/export, parameter/filter prompts, richer grouping/totals options, Layout View, conditional formatting, subreports, and broader report controls; trusted command-backed report preview lifecycle events are now started | Partial |
| **Visual query designer** | Classic Admin query builder with source canvas, join editing, design grid, SQL preview, and saved designer layouts | Done |
| **ETL pipelines** | Built-in package-driven pipeline runtime with validation, dry-run, execute/resume flows, API/CLI/client coverage, run history, and Admin visual designer support | Done |
| **VS Code extension** | Schema explorer, SQL editor with IntelliSense, data browser, table designer, storage diagnostics | Done |
@@ -85,6 +85,7 @@ Advanced features and fundamental architecture enhancements.
| **Group commit / deferred WAL flush** | Done in `v2.9.0`: opt-in `UseDurableCommitBatchWindow(...)` batches durable WAL flushes across contending in-process transactions and remains an expert measure-first knob rather than default behavior | Done |
| **Initial multi-writer support** | Explicit `WriteTransaction` conflict-detected retry flow, shared auto-commit non-insert isolation, and opt-in `ConcurrentWriteTransactions` for shared implicit inserts | Done |
| **Broader multi-writer insert optimization** | Improve hot insert fan-in, row-id reservation, and other high-contention patterns beyond the current initial multi-writer path | Research |
+| **API-level sharding** | Route API/daemon requests across multiple warm CSharpDB database files so independent tenants or shard keys can use separate WAL and commit paths, with v1 focused on single-shard writes and point reads | Research |
| **Replication / change feed** | Stream committed changes for read replicas or event-driven architectures | Research |
| **WebAssembly sandboxed UDFs** | Execute untrusted user-submitted functions in a WASM sandbox with resource limits (fuel, memory caps) via Wasmtime | Research |
@@ -96,7 +97,7 @@ These are known simplifications in the current implementation:
| Area | Limitation |
|------|-----------|
-| **Functions** | Very limited scalar function surface today: built-in `TEXT(expr)` plus aggregate functions; no broader built-in function library or user-defined functions yet |
+| **Functions and automation** | Trusted in-process C# scalar functions are supported when registered by the host; Admin Forms, Admin Reports, and pipelines can invoke trusted host commands from supported event/hook surfaces; Admin Forms support declarative action sequences for run-command, set-field, show-message, and stop steps. Broader built-in scalar functions, aggregate/table-valued UDFs, stored C# modules, remote delegate serialization, additional Access-style control events, richer macro flow, and sandboxed UDFs are not implemented |
| **Query** | Scalar/`IN`/`EXISTS` subqueries are supported, including correlated cases in `WHERE`, non-aggregate projection, and `UPDATE`/`DELETE` expressions; correlated subqueries are not yet supported in `JOIN ON`, `GROUP BY`, `HAVING`, `ORDER BY`, or aggregate projections |
| **Query** | `UNION`, `INTERSECT`, and `EXCEPT` are supported; `UNION ALL` is not implemented yet |
| **Query** | No window functions |
@@ -106,8 +107,8 @@ These are known simplifications in the current implementation:
| **Collections** | `FindByIndexAsync` supports declared field-equality lookups; `FindByPathAsync` and `FindByPathRangeAsync` support path-based queries on indexed paths; `FindAsync` remains a full scan for unindexed predicates |
| **Networking** | `CSharpDB.Daemon` now hosts both REST and gRPC from one process; named pipes remain reserved but are not implemented end to end today |
| **Security** | Remote HTTP and gRPC deployment still rely on external network controls or front-end TLS termination; built-in authentication, authorization, and TLS/mTLS support are still planned |
-| **Admin Forms** | The Forms designer/runtime supports the core generated-form and data-entry path, but still needs Access-parity work for responsive runtime rendering, complete inferred validation, richer form modes, command/action events, advanced filtering/sorting, and broader controls |
-| **Admin Reports** | The Reports designer/runtime supports the core banded preview path, but still needs Access-parity work for bounded saved-query previews, full report output/export, parameters, richer grouping and totals semantics, conditional formatting, subreports, and broader controls |
+| **Admin Forms** | The Forms designer/runtime supports the core generated-form and data-entry path plus initial trusted command-backed automation, including lifecycle events, command buttons, selected control events, and declarative action sequences, but still needs Access-parity work for responsive runtime rendering, complete inferred validation, richer form modes, broader built-in actions, additional events, advanced filtering/sorting, and broader controls |
+| **Admin Reports** | The Reports designer/runtime supports the core banded preview path plus trusted command-backed preview lifecycle events, but still needs Access-parity work for bounded saved-query previews, full report output/export, parameters, richer grouping and totals semantics, conditional formatting, subreports, and broader controls |
| **Text / Multilingual** | Text is stored as UTF-8 and supports all Unicode languages; default semantics remain ordinal, but opt-in `BINARY`, `NOCASE`, `NOCASE_AI`, and `ICU:` collation are implemented for SQL and collection indexes. Dedicated ordered SQL text index optimization remains planned |
| **Concurrency** | The physical WAL commit path is still serialized at the storage boundary. Initial multi-writer support is shipped, but observed gains still depend on conflict shape and whether shared auto-commit `INSERT` is left on the default serialized path |
| **Storage** | No page-level compression |
@@ -182,6 +183,7 @@ Major features already implemented:
- [Internals & Contributing](https://csharpdb.com/docs/internals.html) — How to extend the engine
- [Deployment & Installation Plan](deployment/README.md) — Cross-platform distribution via dotnet tool, Docker, Homebrew, winget, and install scripts
- [Multi-Writer Follow-Up Plan](multi-writer-follow-up-plan.md) — Post-initial multi-writer roadmap, insert-path gaps, and release criteria for broader completion
+- [API-Level Sharding Plan](api-sharding.md) — API/daemon-level routing across multiple database files for write-throughput scaling
- [Query And Durable Write Performance Plan](query-and-durable-write-performance/README.md) — Combined optimizer phase-2 plus durable-write completion plan, shipped state, and remaining benchmark/future-work boundaries
- [Multilingual Text Support Plan](https://csharpdb.com/docs/collation-support.html) — Build on existing Unicode text storage with case-insensitive matching, locale-aware sorting, and `COLLATE` clause support for queries and index definitions
- [Database Encryption Plan](database-encryption/README.md) — Encrypted storage format, key management, migration, and managed-surface rollout
diff --git a/docs/sql-reference.md b/docs/sql-reference.md
index a18c8b28..e2bb355c 100644
--- a/docs/sql-reference.md
+++ b/docs/sql-reference.md
@@ -313,6 +313,11 @@ SELECT COUNT(DISTINCT status), AVG(age) FROM users;
|----------|-----------|---------|-------------|
| `TEXT(expr)` | 1 | TEXT | Converts any value to its text representation |
+Host applications can also register trusted in-process C# scalar functions and call
+them from SQL expression positions such as `SELECT`, `WHERE`, `ORDER BY`,
+`INSERT`, `UPDATE`, trigger bodies, and SQL procedure bodies. See
+[Trusted C# Scalar Functions](trusted-csharp-functions/README.md).
+
---
## Parameters
diff --git a/docs/trusted-csharp-functions/README.md b/docs/trusted-csharp-functions/README.md
new file mode 100644
index 00000000..d7df8bb6
--- /dev/null
+++ b/docs/trusted-csharp-functions/README.md
@@ -0,0 +1,1036 @@
+# Trusted C# Functions And Commands
+
+CSharpDB can call host-registered C# scalar functions from SQL and the embedded expression surfaces that sit on top of the engine. This is the CSharpDB equivalent of an Access-style application function integration: the application owns the C# code, registers it while opening or hosting the database, and users call the function by name in database expressions.
+
+This feature is intentionally trusted and in-process. It does not store C# source code in the database, sandbox user code, load plugin assemblies from database files, or serialize delegates over HTTP or gRPC.
+
+For an end-to-end app-builder walkthrough that combines Admin Forms, collections,
+macro actions, reports, trusted callbacks, and callback readiness, see the
+[Fulfillment Ops Admin Automation tutorial](../tutorials/fulfillment-ops-admin-automation.md).
+
+For Admin Forms save-time business validation, see
+[Trusted Validation Rules](validation-rules.md).
+
+---
+
+## Trusted Commands
+
+CSharpDB also supports trusted host-registered commands for application automation surfaces. Commands are different from scalar functions:
+
+- Scalar functions return a `DbValue` and can be used inside SQL or formulas.
+- Commands return a `DbCommandResult` and are invoked by host-driven events such as Admin Forms lifecycle events, Admin Reports render events, and pipeline run hooks.
+
+Commands are intended for Access-style application automation such as auditing, calling application services, sending notifications, refreshing derived state, coordinating UI workflows, or publishing operational run events. They are trusted in-process callbacks registered by the host application.
+
+```csharp
+using CSharpDB.Admin.Forms.Services;
+using CSharpDB.Primitives;
+
+builder.Services.AddCSharpDbAdminForms(commands =>
+{
+ commands.AddAsyncCommand(
+ "AuditCustomerChange",
+ new DbCommandOptions(
+ Description: "Writes an application audit entry.",
+ Timeout: TimeSpan.FromSeconds(10),
+ IsLongRunning: true),
+ static async (context, ct) =>
+ {
+ long customerId = context.Arguments["Id"].AsInteger;
+ string eventName = context.Metadata["event"];
+
+ await WriteAuditAsync(customerId, eventName, ct);
+ return DbCommandResult.Success();
+ });
+});
+```
+
+Command names are case-insensitive identifiers. Duplicate command names are rejected during registration.
+
+Use `AddCommand(...)` for synchronous or `ValueTask`-returning callbacks and
+`AddAsyncCommand(...)` for `Task` callbacks. Every command
+receives a `CancellationToken`; host code should pass it to I/O calls and stop
+work when cancellation is requested.
+
+`DbCommandOptions.Timeout` is optional. When set, CSharpDB cancels the command
+token if the callback does not finish in time and reports a timeout through the
+same surface-specific failure path as other command errors. `IsLongRunning` is
+metadata for hosts and UI surfaces; it does not move the command out of process
+or run it on a separate scheduler.
+
+Every registered command exposes a `DbHostCallbackDescriptor` through
+`DbCommandDefinition.Descriptor` and `DbCommandRegistry.Callbacks`. The
+descriptor is read-only metadata for policy checks, diagnostics, and Admin
+visibility. It does not sandbox the command.
+
+---
+
+## Trusted Validation Rules
+
+Admin Forms can also call host-registered validation rules before save. Rules
+are registered in the host app with `AddCSharpDbAdminFormValidationRules(...)`
+and referenced from form or control metadata by name. Missing, denied, timed
+out, throwing, or failed validation rules block save and appear in the callback
+diagnostics path.
+
+See [Trusted Validation Rules](validation-rules.md) for registration examples,
+field-level and form-level metadata, policy grants, diagnostics behavior, and
+the runnable sample.
+
+---
+
+## What You Can Register
+
+V1 supports synchronous scalar functions:
+
+```csharp
+public delegate DbValue DbScalarFunctionDelegate(
+ DbScalarFunctionContext context,
+ ReadOnlySpan arguments);
+```
+
+A scalar function receives database values and returns one database value. Supported value types are:
+
+| CSharpDB type | Read with | Return with |
+| --- | --- | --- |
+| `DbType.Integer` | `value.AsInteger` | `DbValue.FromInteger(...)` |
+| `DbType.Real` | `value.AsReal` | `DbValue.FromReal(...)` |
+| `DbType.Text` | `value.AsText` | `DbValue.FromText(...)` |
+| `DbType.Blob` | `value.AsBlob` | `DbValue.FromBlob(...)` |
+| `DbType.Null` | `value.IsNull` | `DbValue.Null` |
+
+Functions are registered with:
+
+```csharp
+using CSharpDB.Engine;
+using CSharpDB.Primitives;
+
+var options = new DatabaseOptions()
+ .ConfigureFunctions(functions =>
+ {
+ functions.AddScalar(
+ "Slugify",
+ arity: 1,
+ options: new DbScalarFunctionOptions(
+ ReturnType: DbType.Text,
+ IsDeterministic: true,
+ NullPropagating: true),
+ invoke: static (_, args) =>
+ DbValue.FromText(args[0].AsText.ToLowerInvariant().Replace(' ', '-')));
+ });
+```
+
+Open the database with those options:
+
+```csharp
+await using var db = await Database.OpenAsync("app.db", options);
+```
+
+For tests or transient data:
+
+```csharp
+await using var db = await Database.OpenInMemoryAsync(options);
+```
+
+---
+
+## Complete Example
+
+```csharp
+using CSharpDB.Engine;
+using CSharpDB.Primitives;
+
+static string Slugify(string text)
+{
+ return text.Trim().ToLowerInvariant().Replace(' ', '-');
+}
+
+var options = new DatabaseOptions()
+ .ConfigureFunctions(functions =>
+ {
+ functions.AddScalar(
+ "Slugify",
+ arity: 1,
+ options: new DbScalarFunctionOptions(
+ ReturnType: DbType.Text,
+ IsDeterministic: true,
+ NullPropagating: true),
+ invoke: static (_, args) => DbValue.FromText(Slugify(args[0].AsText)));
+
+ functions.AddScalar(
+ "IsEven",
+ arity: 1,
+ options: new DbScalarFunctionOptions(
+ ReturnType: DbType.Integer,
+ IsDeterministic: true,
+ NullPropagating: true),
+ invoke: static (_, args) =>
+ DbValue.FromInteger(args[0].AsInteger % 2 == 0 ? 1 : 0));
+ });
+
+await using var db = await Database.OpenAsync("app.db", options);
+
+await db.ExecuteAsync("""
+ CREATE TABLE articles (
+ id INTEGER PRIMARY KEY,
+ title TEXT NOT NULL,
+ slug TEXT
+ );
+ """);
+
+await db.ExecuteAsync("INSERT INTO articles VALUES (1, 'Hello World', Slugify('Hello World'))");
+await db.ExecuteAsync("INSERT INTO articles VALUES (2, 'Second Post', Slugify('Second Post'))");
+
+await using var result = await db.ExecuteAsync("""
+ SELECT id, Slugify(title)
+ FROM articles
+ WHERE IsEven(id) = 1
+ ORDER BY Slugify(title);
+ """);
+```
+
+### VS Code Host Project Workflow
+
+For a runnable C# host project, open
+`samples/trusted-csharp-host` in VS Code. The sample includes `.vscode` launch
+and task files so a developer can press `F5`, set breakpoints inside the
+registered callbacks, and watch SQL and Admin Forms automation invoke ordinary
+C# code.
+
+```powershell
+dotnet run --project samples\trusted-csharp-host\TrustedCSharpHostSample.csproj
+```
+
+The sample demonstrates:
+
+- `DatabaseOptions.ConfigureFunctions(...)` for a trusted scalar function.
+- SQL calling the function by name.
+- `DbCommandRegistry` for a trusted host command.
+- An Admin Forms `DbActionSequence` that sets a field and then runs the
+ command.
+
+The VS Code story stays host-owned: VS Code is the editor/debugger for the C#
+host project, while database metadata stores names and declarative action data
+only.
+
+### End-To-End Developer Handoff
+
+The production workflow is a handoff between an app builder and a host
+developer:
+
+1. The app builder creates database metadata that references a callback by name.
+ Examples include a formula such as `=Slugify([Name])` or a form action step
+ such as `RunCommand` with command name `SendOpsDigest`.
+2. Admin records only the callback name, arity/kind metadata, action arguments,
+ and reference location. It does not store C# source.
+3. The callback catalog compares referenced names with the callbacks registered
+ by the current host. Missing names appear as missing callback readiness.
+4. The app builder copies the generated registration stub from Admin and gives
+ it to the host developer.
+5. The host developer implements the C# function or command in the host
+ project, registers it during startup, and debugs it in VS Code like normal
+ application code.
+6. The host is restarted. Admin refreshes callback readiness, and the reference
+ changes from missing to registered/allowed when the name, kind, and arity
+ match.
+
+For Admin itself, host-owned demo callbacks are registered in
+`src/CSharpDB.Admin/Services/AdminHostCallbacks.cs`. A scalar function is
+registered with `DbFunctionRegistry`:
+
+```csharp
+functions.AddScalar(
+ "Slugify",
+ arity: 1,
+ options: new DbScalarFunctionOptions(
+ ReturnType: DbType.Text,
+ IsDeterministic: true,
+ NullPropagating: true),
+ invoke: static (_, args) => DbValue.FromText(Slugify(args[0].AsText)));
+```
+
+A command callback is registered with `DbCommandRegistry`:
+
+```csharp
+commands.AddCommand(
+ "SendOpsDigest",
+ new DbCommandOptions("Sends a fulfillment operations digest."),
+ static context =>
+ {
+ string source = context.Arguments.TryGetValue("source", out DbValue value)
+ ? value.AsText
+ : "unknown";
+
+ // Call host-owned services here.
+ return DbCommandResult.Success($"Digest requested by {source}.");
+ });
+```
+
+That C# code belongs to the host application. The database still stores only the
+name `SendOpsDigest` and any declarative action arguments.
+
+### Stored Procedures In The Mix
+
+Stored procedures are different from trusted callbacks. They are database-owned
+SQL definitions with parameter metadata. They are useful when the logic should
+stay inside CSharpDB and can be expressed as SQL:
+
+| Need | Prefer |
+| --- | --- |
+| Reusable multi-statement table work | Stored procedure |
+| Transactional updates plus follow-up result sets | Stored procedure |
+| A form button that runs reviewed database logic | `RunProcedure` |
+| External API, email, filesystem, queue, or host service call | Trusted command callback |
+| Custom scalar calculation inside SQL expressions | Trusted scalar function |
+| UI-only behavior such as filtering or control state | Declarative macro action |
+
+A stored procedure can be executed directly through the client API:
+
+```csharp
+ProcedureExecutionResult result = await client.ExecuteProcedureAsync(
+ "AllocateOrder",
+ new Dictionary
+ {
+ ["orderId"] = 7005,
+ ["allocatedBy"] = "Wave Planner",
+ ["note"] = "Allocated from a reviewed procedure.",
+ });
+```
+
+Admin's SQL editor also accepts `EXEC` as an Admin command surface:
+
+```sql
+EXEC AllocateOrder @orderId = 7005, @allocatedBy = 'Wave Planner';
+EXEC RefreshOperationalStats;
+EXEC tutorial_OpenOrderSnapshot { "status": "released" };
+```
+
+Use `RunProcedure` from form metadata only when the rendered host enables
+procedure actions. Use `RunCommand` when the same button needs host-owned C#.
+When both are needed, run the procedure first for database work and the command
+second for the external side effect.
+
+---
+
+## Registration Rules
+
+Function names are SQL identifiers:
+
+- They must start with a letter or `_`.
+- Remaining characters must be letters, digits, or `_`.
+- Lookup is case-insensitive, so `Slugify`, `slugify`, and `SLUGIFY` refer to the same function.
+- A user function name can only be registered once. V1 does not support overloads by arity.
+- Reserved built-ins cannot be overridden. Current reserved names are `TEXT`, `COUNT`, `SUM`, `AVG`, `MIN`, and `MAX`.
+- `arity` must match the number of arguments used by the expression.
+
+Registration failures throw immediately so host applications fail at startup instead of later during a query.
+
+`ConfigureFunctions` sets the function registry for the returned `DatabaseOptions`. If you chain multiple option helpers, keep all function registrations in one `ConfigureFunctions` call or assign a single `DbFunctionRegistry` to `DatabaseOptions.Functions`.
+
+---
+
+## Function Options
+
+Each function can include `DbScalarFunctionOptions`:
+
+```csharp
+new DbScalarFunctionOptions(
+ ReturnType: DbType.Text,
+ IsDeterministic: true,
+ NullPropagating: true,
+ Description: "Formats a URL slug.",
+ AdditionalCapabilities:
+ [
+ new DbExtensionCapabilityRequest(DbExtensionCapability.Clock)
+ ])
+```
+
+| Option | Meaning |
+| --- | --- |
+| `ReturnType` | Optional metadata describing the expected return type. |
+| `IsDeterministic` | Marks the function as returning the same output for the same inputs. V1 exposes the metadata but does not use it for constant folding or index planning. |
+| `NullPropagating` | If any argument is `NULL`, CSharpDB returns `NULL` without invoking the delegate. |
+| `Description` | Optional human-readable text for host tools and Admin visibility. |
+| `AdditionalCapabilities` | Optional capability metadata beyond the implicit `ScalarFunctions` capability. This is for CSharpDB policy mediation and visibility, not a .NET sandbox. |
+| `Metadata` | Optional host-defined descriptor metadata. |
+
+Without `NullPropagating`, `DbValue.Null` is passed to the delegate and the function decides what to do.
+
+```csharp
+functions.AddScalar(
+ "CoalesceText",
+ arity: 2,
+ options: new DbScalarFunctionOptions(DbType.Text),
+ invoke: static (_, args) =>
+ args[0].IsNull ? args[1] : args[0]);
+```
+
+Registered scalar functions expose `DbScalarFunctionDefinition.Descriptor` and
+`DbFunctionRegistry.Callbacks`. The descriptor always uses
+`DbExtensionRuntimeKind.HostCallback`, records the callback kind, name, arity,
+return type, deterministic/null behavior, and includes the implicit
+`ScalarFunctions` capability plus any additional capabilities declared in the
+options.
+
+Commands expose the same descriptor shape through `DbCommandDefinition`.
+Command descriptors include the implicit `Commands` capability plus any
+additional capabilities declared in `DbCommandOptions`, along with description,
+timeout, and long-running metadata.
+
+Hosts can evaluate descriptor capabilities with `DbExtensionPolicyEvaluator`:
+
+```csharp
+DbHostCallbackDescriptor descriptor = commandRegistry.Callbacks.Single();
+DbExtensionPolicyDecision decision = DbExtensionPolicyEvaluator.Evaluate(
+ descriptor,
+ new DbExtensionPolicy(
+ AllowExtensions: true,
+ Grants:
+ [
+ new DbExtensionCapabilityGrant(
+ DbExtensionCapability.Commands,
+ DbExtensionCapabilityGrantStatus.Granted),
+ new DbExtensionCapabilityGrant(
+ DbExtensionCapability.ReadDatabase,
+ DbExtensionCapabilityGrantStatus.Granted),
+ ]),
+ DbExtensionHostMode.Embedded);
+```
+
+Policy evaluation controls what CSharpDB-mediated APIs should allow. It does
+not restrict arbitrary in-process .NET calls made by trusted host code.
+
+---
+
+## SQL Usage
+
+Registered scalar functions can be used in non-aggregate SQL expression positions:
+
+```sql
+SELECT Slugify(title) FROM articles;
+SELECT * FROM articles WHERE IsEven(id) = 1;
+SELECT * FROM articles ORDER BY Slugify(title);
+INSERT INTO articles VALUES (3, 'New Title', Slugify('New Title'));
+UPDATE articles SET slug = Slugify(title) WHERE slug IS NULL;
+```
+
+They also work in trigger bodies and SQL procedure bodies because those paths execute through the same SQL expression evaluator:
+
+```sql
+CREATE TABLE article_audit (article_id INTEGER, slug TEXT);
+
+CREATE TRIGGER articles_ai AFTER INSERT ON articles
+BEGIN
+ INSERT INTO article_audit VALUES (NEW.id, Slugify(NEW.title));
+END;
+```
+
+Custom functions stay on the residual expression path in V1:
+
+- No index pushdown is inferred from a custom function.
+- No generated-column or expression-index behavior is added.
+- No constant folding or cost assumptions are made from custom function metadata.
+
+That keeps existing query and storage paths unchanged unless a query actually calls a registered function.
+
+---
+
+## Direct Client Usage
+
+Direct clients pass functions through `DirectDatabaseOptions`:
+
+```csharp
+using CSharpDB.Client;
+using CSharpDB.Engine;
+using CSharpDB.Primitives;
+
+await using var client = CSharpDbClient.Create(new CSharpDbClientOptions
+{
+ DataSource = "app.db",
+ DirectDatabaseOptions = new DatabaseOptions()
+ .ConfigureFunctions(functions =>
+ {
+ functions.AddScalar(
+ "AddOne",
+ 1,
+ new DbScalarFunctionOptions(DbType.Integer, IsDeterministic: true, NullPropagating: true),
+ static (_, args) => DbValue.FromInteger(args[0].AsInteger + 1));
+ }),
+});
+
+await client.ExecuteSqlAsync("CREATE TABLE numbers (value INTEGER);");
+await client.ExecuteSqlAsync("INSERT INTO numbers VALUES (41);");
+
+var result = await client.ExecuteSqlAsync("SELECT AddOne(value) FROM numbers;");
+```
+
+`DirectDatabaseOptions` is only valid for direct transport. It is rejected for HTTP and gRPC clients because delegates cannot be serialized to another process.
+
+---
+
+## Remote Host Usage
+
+HTTP and gRPC clients cannot send C# delegates. Remote SQL can call a custom function only when that function is registered inside the host process that owns the database.
+
+The practical rule is:
+
+- Embedded or direct client: register functions in `DatabaseOptions` or `DirectDatabaseOptions`.
+- Remote client: register functions where the daemon, API host, or application server opens the database.
+- Pipeline packages, report definitions, form definitions, procedures, and SQL text store function names and expressions only. They do not store C# function bodies.
+- Admin Forms, Admin Reports, and pipeline packages also store generated `automation` metadata that lists required trusted commands and scalar functions by name, surface, and location. This is an import/export contract for hosts; it is not executable code.
+
+---
+
+## Admin Forms
+
+Admin Forms computed formulas can call registered scalar functions when the formula evaluator receives a `DbFunctionRegistry`.
+
+```csharp
+using CSharpDB.Admin.Forms.Evaluation;
+using CSharpDB.Primitives;
+
+var functions = DbFunctionRegistry.Create(builder =>
+{
+ builder.AddScalar(
+ "Tax",
+ 1,
+ new DbScalarFunctionOptions(DbType.Real, IsDeterministic: true, NullPropagating: true),
+ static (_, args) => DbValue.FromReal(args[0].AsReal * 0.0825));
+});
+
+double? tax = FormulaEvaluator.Evaluate(
+ "=Tax(Subtotal)",
+ fieldResolver: name => name == "Subtotal" ? 100.00 : null,
+ functions: functions);
+```
+
+Forms formulas are numeric formulas. A custom function used from `FormulaEvaluator.Evaluate` should return `INTEGER` or `REAL`; other return types evaluate to `null` in that surface. Existing aggregate formulas such as `=SUM(OrderItems.LineTotal)` remain built-in form behavior and are not replaced by custom scalar functions.
+
+Admin Forms can also bind lifecycle events to trusted commands. Form definitions store event names and command names only; the C# command bodies stay registered in the host process.
+
+```csharp
+var form = existingForm with
+{
+ EventBindings =
+ [
+ new FormEventBinding(FormEventKind.OnOpen, "AuditFormOpen"),
+ new FormEventBinding(FormEventKind.BeforeInsert, "ValidateCustomerCreate"),
+ new FormEventBinding(FormEventKind.AfterUpdate, "AuditCustomerChange"),
+ ],
+};
+```
+
+Supported form-level events in this slice are:
+
+| Event | When it runs |
+| --- | --- |
+| `OnOpen` | After the form definition and source table are resolved, before records load. |
+| `OnLoad` | After the initial record page loads. |
+| `BeforeInsert` | Before a new record is inserted. Returning `DbCommandResult.Failure(...)` cancels the insert. |
+| `AfterInsert` | After a new record is inserted. |
+| `BeforeUpdate` | Before an existing record is updated. Returning failure cancels the update. |
+| `AfterUpdate` | After an existing record is updated. |
+| `BeforeDelete` | Before the current record is deleted. Returning failure cancels the delete. |
+| `AfterDelete` | After the current record is deleted. |
+
+Command context arguments include the current record fields converted to `DbValue`. Static arguments configured on the event binding override same-named record fields. Metadata includes `surface`, `formId`, `formName`, `tableName`, and `event`.
+
+The Admin Forms designer preserves form event bindings and exposes them in the property inspector when no control is selected. If the host has registered trusted commands, the designer shows those command names; otherwise it stores the command name typed by the designer. The same editor can attach a visual action sequence to the event.
+
+Admin Forms also include control-level trusted command events. Form controls store event names, command names, and optional JSON arguments in the form definition. At runtime, the renderer invokes the registered host command with the current record fields plus event-specific arguments.
+
+```csharp
+var textBox = existingTextBox with
+{
+ EventBindings =
+ [
+ new ControlEventBinding(
+ ControlEventKind.OnChange,
+ "NormalizeCustomerName",
+ new Dictionary { ["source"] = "name-textbox" }),
+ new ControlEventBinding(ControlEventKind.OnLostFocus, "ValidateCustomerName"),
+ ],
+};
+```
+
+Supported control events in this slice are:
+
+| Event | When it runs |
+| --- | --- |
+| `OnClick` | When a label or command button is clicked. |
+| `OnChange` | After an input, checkbox, radio, select, lookup, or textarea updates its bound field. |
+| `OnGotFocus` | When an interactive control receives focus. |
+| `OnLostFocus` | When an interactive control loses focus. |
+
+Control event metadata includes the Forms metadata plus `event`, `controlId`, `controlType`, and `fieldName` for bound controls. Arguments include current record fields and event details such as `fieldName`, `value`, and `oldValue` for field changes. Static arguments configured on the event binding override same-named runtime arguments.
+
+The Admin Forms designer exposes selected-control event bindings in the property inspector. If the host has registered trusted commands, the designer shows those command names; otherwise it stores the command name typed by the designer. Selected-control events use the same visual action-sequence editor as form lifecycle events.
+
+Admin Forms also include a command button control. Command buttons store a display label, a command name, and optional JSON arguments in the form definition. At runtime, clicking the button invokes the registered host command with the current record fields plus the configured arguments. Command buttons can also use `ControlEventKind.OnClick` bindings, which allows a button to be driven entirely by the shared control-event model.
+
+```csharp
+var button = new ControlDefinition(
+ "btn-ship",
+ "commandButton",
+ new Rect(24, 320, 160, 34),
+ Binding: null,
+ Props: new PropertyBag(new Dictionary
+ {
+ ["text"] = "Ship Order",
+ ["commandName"] = "ShipOrder",
+ ["commandArguments"] = new Dictionary
+ {
+ ["source"] = "form-button",
+ },
+ }),
+ ValidationOverride: null);
+```
+
+Command button direct-command metadata includes the same form metadata as lifecycle events, plus `event = "Click"`, `controlId`, and `controlType`.
+
+---
+
+## Declarative Admin Forms Action Sequences
+
+Admin Forms event bindings can also store small declarative action sequences.
+This is the first Access-style macro layer for CSharpDB Forms: the form stores
+action metadata, while any executable C# still lives in host-registered trusted
+commands.
+
+```csharp
+using CSharpDB.Admin.Forms.Models;
+using CSharpDB.Primitives;
+
+var shipButton = existingButton with
+{
+ EventBindings =
+ [
+ new ControlEventBinding(
+ ControlEventKind.OnClick,
+ CommandName: string.Empty,
+ ActionSequence: new DbActionSequence(
+ [
+ new DbActionStep(
+ DbActionKind.SetFieldValue,
+ Target: "Status",
+ Value: "Shipped"),
+ new DbActionStep(
+ DbActionKind.RunCommand,
+ CommandName: "AuditOrderStatus",
+ Condition: "Status = 'Shipped'",
+ Arguments: new Dictionary
+ {
+ ["source"] = "ship-button",
+ }),
+ new DbActionStep(
+ DbActionKind.ShowMessage,
+ Message: "Order marked as shipped."),
+ ],
+ Name: "ShipButtonActions")),
+ ],
+};
+```
+
+The action set is intentionally small and form-focused:
+
+| Action | Behavior |
+| --- | --- |
+| `RunCommand` | Invokes a host-registered trusted command by name. |
+| `RunActionSequence` | Invokes a reusable named form action sequence stored on the form definition. |
+| `SetFieldValue` | Updates a target field in the current mutable form record. |
+| `ShowMessage` | Sends a message when the current Forms surface provides a command/message callback. |
+| `Stop` | Ends the current sequence successfully. |
+| `NewRecord` | Starts a new record in the rendered form. |
+| `SaveRecord` | Saves the current rendered record through the normal form save path. |
+| `DeleteRecord` | Deletes the current persisted rendered record through the normal form delete path. |
+| `RefreshRecords` | Reloads the current record/page while preserving the current primary key when possible. |
+| `PreviousRecord` | Moves the rendered form to the previous record. |
+| `NextRecord` | Moves the rendered form to the next record. |
+| `GoToRecord` | Navigates to a primary-key value from `Value`, `Arguments["value"]`, `Arguments["recordId"]`, `Arguments["primaryKey"]`, or the field named by `Target`. |
+
+Reusable action sequences are stored once on the form and invoked by name from
+form events, control events, or command buttons:
+
+```csharp
+var form = existingForm with
+{
+ ActionSequences =
+ [
+ new DbActionSequence(
+ [
+ new DbActionStep(DbActionKind.SetFieldValue, Target: "Status", Value: "Ready"),
+ new DbActionStep(DbActionKind.RunCommand, CommandName: "AuditReady"),
+ ],
+ Name: "PrepareReadyStatus"),
+ ],
+ EventBindings =
+ [
+ new FormEventBinding(
+ FormEventKind.BeforeUpdate,
+ string.Empty,
+ ActionSequence: new DbActionSequence(
+ [
+ new DbActionStep(
+ DbActionKind.RunActionSequence,
+ SequenceName: "PrepareReadyStatus",
+ Arguments: new Dictionary { ["source"] = "before-update" }),
+ ])),
+ ],
+};
+```
+
+`RunActionSequence` arguments become runtime arguments for the nested sequence,
+so nested `RunCommand` steps receive current record fields, binding arguments,
+caller-supplied sequence arguments, and their own step arguments. Recursive
+sequence loops fail with a nesting-limit error instead of running forever.
+
+Action sequences can be attached to form lifecycle bindings or selected-control
+bindings. A binding can contain only a command, only an action sequence, or a
+command followed by an action sequence:
+
+```csharp
+var form = existingForm with
+{
+ EventBindings =
+ [
+ new FormEventBinding(
+ FormEventKind.BeforeInsert,
+ "ValidateOrder",
+ ActionSequence: new DbActionSequence(
+ [
+ new DbActionStep(
+ DbActionKind.SetFieldValue,
+ Target: "Status",
+ Value: "Draft"),
+ ])),
+ ],
+};
+```
+
+The Admin Forms property inspector exposes action sequences with a visual
+editor on form-level and selected-control event bindings. Designers can add a
+sequence, name it, add command, reusable-sequence, field, message, stop, and
+built-in record steps, reorder or remove steps, choose registered commands or
+reusable sequences when available, and set per-step conditions and
+`StopOnFailure`. The form-level property inspector also includes a reusable
+action-sequence library editor. JSON editing remains only for optional binding,
+`RunCommand`, or `RunActionSequence` argument payloads.
+
+For `RunCommand`, command arguments are built from current record fields,
+binding arguments, runtime event arguments, and step arguments, with later
+sources overriding earlier ones. Command metadata includes the Forms metadata
+plus `actionKind`, `actionStep`, optional `actionSequence`, and optional
+`actionCondition`.
+
+Every action step can include a `Condition`. Empty conditions run the step.
+False conditions skip only that step. Malformed conditions fail through the
+normal step failure path, so `StopOnFailure = false` can allow a later step to
+continue.
+
+Supported condition syntax is intentionally small:
+
+| Syntax | Example |
+| --- | --- |
+| Truthy value | `IsActive` |
+| Equality | `Status = 'Ready'` or `[Status] == "Ready"` |
+| Inequality | `Status <> 'Closed'` or `Status != 'Closed'` |
+| Numeric comparison | `Amount > 0`, `Quantity <= 10` |
+| Null comparison | `ClosedAt = null` |
+
+Condition values are resolved from current record fields, binding arguments,
+runtime event arguments, and step arguments using the same later-wins order as
+command arguments. A leading `=` is accepted for macro-style conditions, for
+example `=Status = 'Ready'`.
+
+When forms are saved through `DbFormRepository` or exported through
+`FormAutomationMetadata.NormalizeForExport(...)`, the definition's `automation`
+metadata is regenerated from form events, command buttons, selected-control
+events, reusable action sequences, action-sequence `RunCommand` steps, and
+computed-control formulas. Older form JSON without automation metadata is
+backfilled when it is loaded.
+
+`SetFieldValue` can update mutable records in form lifecycle events such as
+`BeforeInsert` and `BeforeUpdate`, and it can update the current rendered record
+from control events or command-button clicks.
+
+Built-in record actions require a rendered Admin Forms data-entry runtime.
+They are intended for command buttons and selected-control events. Headless
+form lifecycle dispatch can still run `SetFieldValue`, `ShowMessage`, `Stop`,
+and `RunCommand`, but it reports a failure if a sequence asks for rendered-form
+navigation or save/delete actions.
+
+Action sequences do not include loops, stored C# source, database-owned
+plugins, or remote delegate serialization. Rendered Admin form runtimes support
+direct SQL and procedure actions only when the host explicitly enables those
+capabilities.
+
+---
+
+## Admin Reports
+
+Admin Reports preview rendering accepts the same registry through `DefaultReportPreviewService`:
+
+```csharp
+using CSharpDB.Admin.Reports.Services;
+using CSharpDB.Primitives;
+
+var previewService = new DefaultReportPreviewService(
+ dbClient,
+ sourceProvider,
+ functions);
+```
+
+Numeric calculated expressions can call numeric-returning functions:
+
+```text
+=Tax([Subtotal])
+```
+
+Calculated text can use a scalar function as the whole expression, including text-returning functions:
+
+```text
+=FormatInvoiceLabel([InvoiceNumber], [CustomerName])
+```
+
+Report aggregate formulas such as `=SUM([Subtotal])` remain built-in report behavior.
+
+Admin Reports can also bind preview-render lifecycle events to trusted commands. Report definitions store event names, command names, and optional static arguments only; the C# command bodies stay registered by the host process.
+
+```csharp
+using CSharpDB.Admin.Reports.Models;
+
+var report = existingReport with
+{
+ EventBindings =
+ [
+ new ReportEventBinding(ReportEventKind.OnOpen, "AuditReportOpen"),
+ new ReportEventBinding(ReportEventKind.BeforeRender, "PrepareReportContext"),
+ new ReportEventBinding(ReportEventKind.AfterRender, "PublishReportRendered"),
+ ],
+};
+```
+
+Supported report events are:
+
+| Event | When it runs |
+| --- | --- |
+| `OnOpen` | After the report source is resolved, before preview rows are loaded. |
+| `BeforeRender` | After preview rows are loaded and capped, before pagination and calculated text rendering. |
+| `AfterRender` | After preview pages are produced, before the preview result is returned. |
+
+Command context arguments include render metrics such as `rowCount`, `loadedRowCount`, `rowTruncated`, `pageCount`, `isTruncated`, and `hasSchemaDrift` depending on the event. Static arguments configured on the binding override same-named runtime arguments. Metadata includes `surface = AdminReports`, `reportId`, `reportName`, `sourceKind`, `sourceName`, and `event`.
+
+When reports are saved through `DbReportRepository` or exported through
+`ReportAutomationMetadata.NormalizeForExport(...)`, the definition's
+`automation` metadata is regenerated from report event bindings and calculated
+text expressions. Older report JSON without automation metadata is backfilled
+when it is loaded.
+
+Register report commands through the reports service registration overload:
+
+```csharp
+using CSharpDB.Admin.Reports.Services;
+using CSharpDB.Primitives;
+
+builder.Services.AddCSharpDbAdminReports(commands =>
+{
+ commands.AddCommand("PublishReportRendered", static context =>
+ {
+ string reportName = context.Metadata["reportName"];
+ long pageCount = context.Arguments["pageCount"].AsInteger;
+
+ PublishReportMetric(reportName, pageCount);
+ return DbCommandResult.Success();
+ });
+});
+```
+
+---
+
+## Pipelines
+
+Pipelines can call registered scalar functions in filter expressions and derived-column expressions when the runner or component factory is constructed with a registry.
+
+```csharp
+using CSharpDB.Client.Pipelines;
+using CSharpDB.Pipelines.Models;
+using CSharpDB.Primitives;
+
+var functions = DbFunctionRegistry.Create(builder =>
+{
+ builder.AddScalar(
+ "NormalizeStatus",
+ 1,
+ new DbScalarFunctionOptions(DbType.Text, IsDeterministic: true, NullPropagating: true),
+ static (_, args) => DbValue.FromText(args[0].AsText.Trim().ToLowerInvariant()));
+});
+
+var runner = new CSharpDbPipelineRunner(client, functions);
+
+var package = new PipelinePackageDefinition
+{
+ Name = "active-customers",
+ Version = "1.0.0",
+ Source = new PipelineSourceDefinition
+ {
+ Kind = PipelineSourceKind.CsvFile,
+ Path = "customers.csv",
+ HasHeaderRow = true,
+ },
+ Transforms =
+ [
+ new PipelineTransformDefinition
+ {
+ Kind = PipelineTransformKind.Filter,
+ FilterExpression = "NormalizeStatus(status) == 'active'",
+ },
+ new PipelineTransformDefinition
+ {
+ Kind = PipelineTransformKind.Derive,
+ DerivedColumns =
+ [
+ new PipelineDerivedColumn
+ {
+ Name = "status_key",
+ Expression = "NormalizeStatus(status)",
+ },
+ ],
+ },
+ ],
+ Destination = new PipelineDestinationDefinition
+ {
+ Kind = PipelineDestinationKind.JsonFile,
+ Path = "active-customers.json",
+ },
+};
+
+await runner.RunPackageAsync(package);
+```
+
+Pipeline package JSON stores expressions such as `NormalizeStatus(status)` plus generated `automation` metadata listing the required scalar function names. The C# delegate must be registered by the process that runs the package.
+
+Pipelines can also invoke trusted commands from run hooks. Hook definitions are serialized with the package, but they store only the hook event, command name, optional static arguments, and generated automation metadata:
+
+```csharp
+var commands = DbCommandRegistry.Create(builder =>
+{
+ builder.AddCommand("NotifyPipeline", static context =>
+ {
+ string pipelineName = context.Metadata["pipelineName"];
+ string status = context.Arguments["status"].AsText;
+ long rowsWritten = context.Arguments["rowsWritten"].AsInteger;
+
+ NotifyOps(pipelineName, status, rowsWritten);
+ return DbCommandResult.Success();
+ });
+});
+
+var runner = new CSharpDbPipelineRunner(client, functions, commands);
+
+var package = new PipelinePackageDefinition
+{
+ Name = "active-customers",
+ Version = "1.0.0",
+ Source = new PipelineSourceDefinition
+ {
+ Kind = PipelineSourceKind.CsvFile,
+ Path = "customers.csv",
+ },
+ Destination = new PipelineDestinationDefinition
+ {
+ Kind = PipelineDestinationKind.JsonFile,
+ Path = "active-customers.json",
+ },
+ Hooks =
+ [
+ new PipelineCommandHookDefinition
+ {
+ Event = PipelineCommandHookEvent.OnRunSucceeded,
+ CommandName = "NotifyPipeline",
+ Arguments = new Dictionary
+ {
+ ["channel"] = "ops",
+ },
+ },
+ ],
+};
+```
+
+Supported pipeline hook events are:
+
+| Event | When it runs |
+| --- | --- |
+| `OnRunStarted` | After package validation and run logging, before components are created. |
+| `OnBatchCompleted` | After each source batch is transformed/written, metrics and checkpoints are updated, and reject limits pass. |
+| `OnRunSucceeded` | After destination completion and before the successful run is logged as completed. |
+| `OnRunFailed` | When the orchestrator is about to return a failed `PipelineRunResult`. |
+
+Hook arguments include `runId`, `pipelineName`, `pipelineVersion`, `mode`, `event`, `status`, `rowsRead`, `rowsWritten`, `rowsRejected`, and `batchesCompleted`. Batch hooks also include `batchNumber`, `startingRowNumber`, and `batchRowCount`. Failure hooks include `errorSummary`. Metadata includes `surface = Pipelines`, `pipelineName`, `pipelineVersion`, `runId`, `mode`, and `event`.
+
+`PipelinePackageSerializer` refreshes the `automation` manifest when packages are serialized, saved, deserialized, or loaded from disk. `PipelinePackageValidator` accepts older packages without a manifest, but if a manifest is present and no longer matches the package expressions/hooks, validation reports stale automation metadata so the package can be re-exported.
+
+`Validate` mode does not invoke command hooks, so package validation stays side-effect free. Missing command registration or a failing hook with `StopOnFailure = true` fails the run normally. For `OnRunFailed`, hook failures are appended to the failed run's error summary instead of recursively dispatching more failure hooks.
+
+---
+
+## Error Handling
+
+Missing SQL functions fail with the existing unknown scalar function error. Function exceptions are wrapped with the function name and the surrounding statement follows normal rollback behavior.
+
+```csharp
+functions.AddScalar(
+ "RequirePositive",
+ 1,
+ new DbScalarFunctionOptions(DbType.Integer, NullPropagating: true),
+ static (context, args) =>
+ {
+ long value = args[0].AsInteger;
+ if (value <= 0)
+ throw new ArgumentOutOfRangeException(context.FunctionName, "Value must be positive.");
+
+ return DbValue.FromInteger(value);
+ });
+```
+
+For SQL write statements, a failing function aborts the statement. If the statement is inside a transaction, normal transaction rollback rules apply.
+
+Admin Forms formulas intentionally return `null` for invalid formulas, unsupported function return types, missing functions, division by zero, or exceptions. Pipeline functions throw runtime errors unless the pipeline error mode handles the affected row.
+
+Trusted command failures are surface-specific. Form before-events can cancel writes, report event failures fail preview rendering, and pipeline hook failures produce a failed `PipelineRunResult` unless the binding sets `StopOnFailure = false`. Timed-out commands are reported as command failures; caller-requested cancellation still propagates as cancellation instead of being converted to a failure message.
+
+Forms action-sequence failures follow the same binding-level `StopOnFailure`
+rule. Step-level `StopOnFailure = false` lets a later step continue after that
+step fails; otherwise the sequence reports the failure to the surrounding form
+or control event.
+
+---
+
+## Performance Guidance
+
+Custom functions run only when an expression calls them. Queries and writes that do not use custom functions stay on the existing paths.
+
+For low overhead:
+
+- Prefer `NullPropagating = true` when a function naturally returns null for null input.
+- Avoid database calls, blocking I/O, sleeps, and long network calls inside delegates.
+- For command callbacks that call application services, prefer `AddAsyncCommand(...)`, honor the provided cancellation token, and set a timeout that matches the user-facing workflow.
+- Keep delegates thread-safe. A function may be called by concurrent queries in the same host process.
+- Capture immutable services or thread-safe services in closures when application integration is needed.
+- Use `IsDeterministic = true` for accurate metadata, but do not rely on V1 to optimize from it.
+
+---
+
+## Current Limitations
+
+V1 does not support:
+
+- Aggregate UDFs.
+- Table-valued UDFs.
+- Stored C# source code or database-owned compiled modules.
+- Sandboxed execution.
+- Async scalar delegates.
+- Passing a database handle into the function context.
+- Sending delegates over HTTP, gRPC, or pipeline package files.
+- Optimizer pushdown, expression indexes, generated columns, or constant folding based on custom function metadata.
+- Additional Access-style control events such as double-click, key, mouse, timer, and dirty/current events.
+- Richer macro/action scripts with loops, reusable UI rule presets, additional event surfaces, or database-owned executable code.
diff --git a/docs/trusted-csharp-functions/access-style-macro-actions.md b/docs/trusted-csharp-functions/access-style-macro-actions.md
new file mode 100644
index 00000000..21411eb1
--- /dev/null
+++ b/docs/trusted-csharp-functions/access-style-macro-actions.md
@@ -0,0 +1,116 @@
+# Access-Style Macro Actions
+
+Phase 8 extends Admin Forms action sequences with Access-style UI and data actions. The action metadata still stays declarative: host applications own executable C# callbacks, SQL/procedure opt-in policy, database connections, and navigation behavior.
+
+## Supported Actions
+
+| Action | Runtime behavior |
+| --- | --- |
+| `openForm` | Resolves a form by id or name and asks the host to open it. Arguments can include `mode`, `recordId`, `primaryKey`, `id`, `filter`, or `where`. |
+| `closeForm` | Asks the host to close the active form entry tab or surface. |
+| `applyFilter` | Filters the current form record list when `target` is `form`; filters a rendered `datagrid` control when `target` is that control id. |
+| `clearFilter` | Clears the form or data-grid filter selected by `target`. |
+| `runSql` | Executes SQL only when the host enables SQL actions. `@name` parameters are resolved from action arguments. |
+| `runProcedure` | Executes a named database procedure only when the host enables procedure actions. `target` is the procedure name; action arguments become procedure arguments. |
+| `setControlProperty` | Overrides rendered control properties such as `visible`, `enabled`, `readOnly`, `text`, `placeholder`, and bound `value`. |
+| `setControlVisibility`, `setControlEnabled`, `setControlReadOnly` | Short forms for the corresponding `setControlProperty` calls. |
+
+Existing actions such as `setFieldValue`, `runCommand`, `runActionSequence`, `newRecord`, `saveRecord`, `deleteRecord`, `refreshRecords`, `previousRecord`, `nextRecord`, and `goToRecord` continue to work in the same action sequence model.
+
+## Filter Expressions
+
+Filters use the same bracketed field expression style as conditions:
+
+```json
+{
+ "kind": "applyFilter",
+ "target": "ordersGrid",
+ "value": "[Status] = @status AND [Total] > @minimum",
+ "arguments": {
+ "status": "Open",
+ "minimum": 100
+ }
+}
+```
+
+Use `target: "form"` for the parent form list. Use a DataGrid control id for child-row filtering.
+
+## Open Form Arguments
+
+`openForm` carries navigation arguments to the host:
+
+```json
+{
+ "kind": "openForm",
+ "target": "Orders Entry",
+ "arguments": {
+ "recordId": "$record.Id",
+ "mode": "view",
+ "filter": "[Status] = 'Open'"
+ }
+}
+```
+
+The built-in admin tab host forwards those values to `DataEntry` as initial state. `mode: "new"` starts a writable form on a new record. `recordId` navigates to the requested primary key after load. `filter` or `where` applies an initial form filter.
+
+## SQL And Procedure Actions
+
+Use `runProcedure` when the workflow should invoke reusable database-owned SQL,
+for example allocating an order, receiving a purchase order, processing a
+return, or returning a packaged operational snapshot. A procedure body can run
+multiple SQL statements in one execution and can return follow-up result sets.
+
+```json
+{
+ "kind": "runProcedure",
+ "target": "AllocateOrder",
+ "arguments": {
+ "orderId": 7005,
+ "allocatedBy": "Wave Planner",
+ "note": "Allocated from form action sequence."
+ },
+ "stopOnFailure": true
+}
+```
+
+Use `runCommand` instead when the workflow needs host-owned C# behavior such as
+email, queues, external APIs, filesystem access, or other services. A common
+sequence is `runProcedure` for database updates followed by `runCommand` for a
+host notification.
+
+Rendered Admin form runtimes can leave `runSql` and `runProcedure` disabled by
+policy. That keeps database-mutating actions explicit at the host boundary even
+though procedure definitions themselves are database metadata.
+
+## Conditional UI Rules
+
+Form-level `rules` apply control property effects whenever their condition is true:
+
+```json
+{
+ "ruleId": "archived-state",
+ "condition": "[Status] = 'Archived'",
+ "effects": [
+ { "controlId": "statusBox", "property": "readOnly", "value": true },
+ { "controlId": "archiveButton", "property": "enabled", "value": false }
+ ]
+}
+```
+
+The designer property inspector includes a rules editor and a validation panel for action/rule readiness.
+
+## Diagnostics
+
+Subscribe to `FormActionDiagnostics.Listener` to observe action execution:
+
+```csharp
+using CSharpDB.Admin.Forms.Contracts;
+
+using IDisposable subscription = FormActionDiagnostics.Listener.Subscribe(observer);
+```
+
+Events use `FormActionDiagnostics.InvocationEventName` and carry `FormActionInvocationDiagnostic`, including action kind, target, form id, event name, action sequence name, step index, elapsed time, success state, cancellation state, result message, exception message, and metadata.
+
+## Sample
+
+See `samples/trusted-csharp-host/access-style-macro-form.json` for a form manifest that combines open form, data-grid filtering, SQL execution, control property changes, and conditional UI rules.
diff --git a/docs/trusted-csharp-functions/database-code-modules-vscode-plan.md b/docs/trusted-csharp-functions/database-code-modules-vscode-plan.md
new file mode 100644
index 00000000..1a473417
--- /dev/null
+++ b/docs/trusted-csharp-functions/database-code-modules-vscode-plan.md
@@ -0,0 +1,664 @@
+# Database Code Modules With VS Code Sync Plan
+
+## Summary
+
+Add database-owned C# code modules that can be saved with a CSharpDB database,
+compiled by CSharpDB, and invoked by Admin Forms events. Editing and debugging
+should happen in VS Code, not in the Admin browser UI.
+
+This is the Access-style "code travels with the database" model, but with C#
+and normal developer tooling. Admin remains the app-builder and runtime surface.
+VS Code remains the serious code editor and debugger.
+
+The core implementation must be a public .NET API, not a CLI-first design.
+
+## Product Position
+
+CSharpDB will have three separate automation lanes:
+
+| Lane | Who Owns Code | Where It Lives | Use For |
+| --- | --- | --- | --- |
+| Declarative macros/actions | Database metadata | Form JSON/database metadata | No-code app behavior, navigation, field updates, command orchestration |
+| Host callbacks | Host application | C# host project startup registration | External services, compiled integrations, application-owned code |
+| Database code modules | Database | Database module metadata, synced to files for editing | Access-like form/business logic that travels with the database |
+
+Database code modules are not a replacement for host callbacks. Host callbacks
+remain the right path for application services, email, queues, filesystem,
+network calls, or privileged host operations.
+
+## Goals
+
+- Store C# source modules in the database.
+- Let Admin create form/control event stubs.
+- Let VS Code edit synced `.cs` files with normal C# tooling.
+- Sync source between the database and a local workspace folder.
+- Compile modules with Roslyn and return diagnostics.
+- Surface diagnostics in Admin and VS Code.
+- Execute trusted compiled modules from Admin Forms runtime events.
+- Keep the core reusable through a public API based on `ICSharpDbClient`.
+- Avoid Monaco/CodeMirror bloat in Admin.
+- Keep CLI optional; it is not the primary architecture.
+
+## Non-Goals For V1
+
+- No full in-browser IDE.
+- No Monaco dependency for Admin code editing.
+- No arbitrary hidden execution from untrusted databases.
+- No remote dynamic assembly loading from form JSON.
+- No debugger implemented inside Admin.
+- No replacement for host callback integrations.
+- No direct unmanaged/native code from modules.
+- No unrestricted file/network/process APIs from database-owned code.
+
+## High-Level Architecture
+
+```text
+CSharpDB.CodeModules
+ public .NET API
+ module metadata model
+ import/export/sync services
+ Roslyn build/diagnostic services
+ trusted runtime contracts
+ uses ICSharpDbClient
+
+CSharpDB.Admin
+ lists modules
+ creates event stubs
+ shows trust/build/diagnostic state
+ invokes compiled trusted module procedures at runtime
+ uses CSharpDB.CodeModules directly
+
+VS Code Extension
+ module tree
+ open/sync files
+ Problems diagnostics
+ build command
+ debug harness command
+ talks to .NET sidecar/language server
+
+.NET Sidecar / Language Server
+ references CSharpDB.CodeModules
+ exposes JSON-RPC to VS Code extension
+ owns local file watching and DB sync
+```
+
+The CLI is optional future work. If added, it should be a thin wrapper over
+`CSharpDB.CodeModules`, not the source of truth.
+
+## Project Layout
+
+```text
+src/
+ CSharpDB.CodeModules/
+ CSharpDB.CodeModules.csproj
+ Models/
+ Storage/
+ Sync/
+ Compilation/
+ Runtime/
+ Diagnostics/
+
+ CSharpDB.Admin/
+ uses CSharpDB.CodeModules
+
+vscode-extension/
+ src/
+ extension.ts
+ moduleTree.ts
+ diagnostics.ts
+ sidecarClient.ts
+
+ sidecar/
+ CSharpDB.CodeModules.LanguageServer.csproj
+```
+
+The sidecar can live under `vscode-extension/sidecar` or `src/` depending on
+packaging. It should not duplicate code-module logic.
+
+## Public API
+
+The core API should be usable by Admin, tests, VS Code sidecar, and host apps.
+
+```csharp
+public sealed class CSharpDbCodeModuleClient
+{
+ public CSharpDbCodeModuleClient(ICSharpDbClient client);
+
+ public Task> ListAsync(
+ CodeModuleListRequest? request = null,
+ CancellationToken ct = default);
+
+ public Task GetAsync(
+ string moduleId,
+ CancellationToken ct = default);
+
+ public Task UpsertAsync(
+ CodeModuleDefinition module,
+ CodeModuleWriteOptions? options = null,
+ CancellationToken ct = default);
+
+ public Task DeleteAsync(
+ string moduleId,
+ CodeModuleDeleteOptions? options = null,
+ CancellationToken ct = default);
+
+ public Task ExportAsync(
+ CodeModuleExportRequest request,
+ CancellationToken ct = default);
+
+ public Task ImportAsync(
+ CodeModuleImportRequest request,
+ CancellationToken ct = default);
+
+ public Task BuildAsync(
+ CodeModuleBuildRequest request,
+ CancellationToken ct = default);
+
+ public Task TrustAsync(
+ CodeModuleTrustRequest request,
+ CancellationToken ct = default);
+}
+```
+
+### Key Models
+
+```csharp
+public sealed record CodeModuleDefinition(
+ string ModuleId,
+ string Name,
+ CodeModuleKind Kind,
+ string Language,
+ string Source,
+ CodeModuleScope Scope,
+ IReadOnlyDictionary Metadata,
+ string? SourceHash = null,
+ string? LastBuiltHash = null,
+ CodeModuleTrustState TrustState = CodeModuleTrustState.Untrusted,
+ IReadOnlyList? EventBindings = null);
+
+public enum CodeModuleKind
+{
+ Standard = 0,
+ Form = 1,
+ Report = 2,
+ Shared = 3,
+}
+
+public sealed record CodeModuleEventBinding(
+ string OwnerKind,
+ string OwnerId,
+ string EventName,
+ string ProcedureName,
+ string ModuleId);
+```
+
+`Language` should be `"csharp"` in V1. The field exists so the wire shape can
+evolve later.
+
+## Database Storage
+
+V1 can use system tables instead of embedding code directly inside form JSON.
+Forms can reference module event procedures by id/name.
+
+Suggested tables:
+
+```sql
+CREATE TABLE IF NOT EXISTS _admin_code_modules (
+ module_id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ kind TEXT NOT NULL,
+ language TEXT NOT NULL,
+ source TEXT NOT NULL,
+ source_hash TEXT NOT NULL,
+ last_built_hash TEXT,
+ trust_state TEXT NOT NULL,
+ metadata_json TEXT,
+ created_utc TEXT NOT NULL,
+ updated_utc TEXT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS _admin_code_module_bindings (
+ binding_id TEXT PRIMARY KEY,
+ module_id TEXT NOT NULL,
+ owner_kind TEXT NOT NULL,
+ owner_id TEXT NOT NULL,
+ event_name TEXT NOT NULL,
+ procedure_name TEXT NOT NULL,
+ metadata_json TEXT,
+ FOREIGN KEY (module_id) REFERENCES _admin_code_modules(module_id)
+);
+
+CREATE TABLE IF NOT EXISTS _admin_code_module_builds (
+ build_id TEXT PRIMARY KEY,
+ module_set_hash TEXT NOT NULL,
+ status TEXT NOT NULL,
+ diagnostics_json TEXT,
+ artifact_hash TEXT,
+ built_utc TEXT NOT NULL
+);
+```
+
+Open question: whether compiled assemblies should be persisted in the database.
+For V1, prefer not storing binaries until the runtime model is settled. Store
+source, diagnostics, and hashes first. Compile on host startup or on first use.
+
+## Exported Workspace Shape
+
+VS Code should edit normal files generated from database modules.
+
+```text
+.csharpdb-code/
+ csharpdb.codeproj.json
+ forms/
+ Customers/
+ CustomersForm.cs
+ modules/
+ InventoryRules.cs
+ SharedValidation.cs
+ obj/
+ generated/
+ CSharpDbCodeModuleRuntime.g.cs
+ FormContracts.g.cs
+ .vscode/
+ tasks.json
+ launch.json
+```
+
+Manifest example:
+
+```json
+{
+ "database": "C:\\data\\app.db",
+ "formatVersion": 1,
+ "modules": [
+ {
+ "moduleId": "forms.customers",
+ "path": "forms/Customers/CustomersForm.cs",
+ "sourceHash": "sha256:...",
+ "kind": "Form",
+ "ownerKind": "Form",
+ "ownerId": "customers-form"
+ }
+ ]
+}
+```
+
+The manifest is required for conflict detection and stable module ids.
+
+## C# Module Shape
+
+Generated form module example:
+
+```csharp
+using CSharpDB.CodeModules.Runtime;
+
+namespace CSharpDB.DatabaseCode.Forms;
+
+public sealed class CustomersFormModule : FormCodeModule
+{
+ public void Form_BeforeUpdate(FormBeforeUpdateContext context)
+ {
+ if (Me.Status == "Closed" && Me.ClosedDate is null)
+ {
+ context.Cancel = true;
+ context.Message = "Closed date is required.";
+ }
+ }
+
+ public void btnShip_Click(FormControlEventContext context)
+ {
+ Me.Status = "Shipped";
+ DoCmd.SaveRecord();
+ }
+}
+```
+
+This is C#, but with generated helper APIs that feel like Access:
+
+- `Me`
+- `Controls`
+- strongly typed field properties when schema is known
+- `DoCmd`
+- event context objects
+- validation/cancel semantics
+
+## Runtime Contracts
+
+`CSharpDB.CodeModules.Runtime` should define small, controlled contracts:
+
+```csharp
+public abstract class FormCodeModule
+{
+ protected dynamic Me { get; }
+ protected IFormCommandApi DoCmd { get; }
+ protected IFormControlApi Controls { get; }
+}
+
+public sealed class FormBeforeUpdateContext
+{
+ public bool Cancel { get; set; }
+ public string? Message { get; set; }
+}
+
+public interface IFormCommandApi
+{
+ void SaveRecord();
+ void NewRecord();
+ void Refresh();
+ void OpenForm(string formName, object? filter = null);
+ void RunAction(string actionName);
+ ValueTask RunCommandAsync(string commandName, object? args = null);
+}
+```
+
+The runtime API should expose form-safe operations only. Host integrations still
+go through trusted host callbacks.
+
+## Compilation
+
+Use Roslyn in `CSharpDB.CodeModules.Compilation`.
+
+Build inputs:
+
+- module source files
+- generated runtime contracts
+- generated form schema wrappers
+- allowed framework references
+- CSharpDB runtime contract assembly references
+
+Build outputs:
+
+- success/failure
+- diagnostics with file/module/line/column
+- module set hash
+- optional in-memory assembly
+- optional debug harness project
+
+Diagnostics model:
+
+```csharp
+public sealed record CodeModuleDiagnostic(
+ string ModuleId,
+ string? FilePath,
+ CodeModuleDiagnosticSeverity Severity,
+ string Code,
+ string Message,
+ int StartLine,
+ int StartColumn,
+ int EndLine,
+ int EndColumn);
+```
+
+VS Code maps these to the Problems panel. Admin shows them in the module detail
+view.
+
+## Trust And Security
+
+Database-owned C# is powerful. V1 must fail closed.
+
+Trust states:
+
+| State | Meaning |
+| --- | --- |
+| `Untrusted` | Source exists but cannot execute. |
+| `TrustedForThisMachine` | User explicitly trusted this DB/module set locally. |
+| `TrustedBySignature` | Future signed module/package trust. |
+| `Revoked` | Explicitly blocked. |
+
+Execution requirements:
+
+- Database must be trusted.
+- Module source hash must match the trusted hash.
+- Build diagnostics must have no errors.
+- Runtime policy must allow module execution.
+- Host must opt into code module execution.
+
+Admin should show clear trust prompts. It should never silently execute code
+from a database file just because it contains modules.
+
+## VS Code Extension
+
+The VS Code extension should be a UI and sync shell, not the implementation
+owner.
+
+Commands:
+
+- `CSharpDB: Connect Database`
+- `CSharpDB: Export Code Modules`
+- `CSharpDB: Import Code Modules`
+- `CSharpDB: Sync Current File To Database`
+- `CSharpDB: Watch Code Modules`
+- `CSharpDB: Build Code Modules`
+- `CSharpDB: Create Form Event Stub`
+- `CSharpDB: Open Module From Database`
+- `CSharpDB: Generate Debug Harness`
+
+Views:
+
+- Code Modules tree
+- Forms and event procedures
+- Build diagnostics
+- Trust state
+
+The extension talks to a .NET sidecar/language server over JSON-RPC. The
+sidecar references `CSharpDB.CodeModules` and uses the public API.
+
+## Sidecar Protocol
+
+The protocol should be simple JSON-RPC:
+
+```text
+codeModules/connect
+codeModules/list
+codeModules/export
+codeModules/import
+codeModules/watch
+codeModules/build
+codeModules/upsert
+codeModules/createEventStub
+codeModules/trust
+```
+
+The sidecar owns:
+
+- loading CSharpDB assemblies
+- talking to `ICSharpDbClient`
+- file watching
+- conflict detection
+- Roslyn builds
+- diagnostics translation
+
+## Admin UX
+
+Admin should not become the main editor. It should provide:
+
+- Code Modules tab.
+- module list and details.
+- source read-only or simple textarea fallback.
+- build/trust status.
+- diagnostics history.
+- create event stub button from form designer.
+- "Open in VS Code" command when extension/deep link support exists.
+
+Form designer integration:
+
+- select form/control event.
+- choose "Create Code Module Handler".
+- Admin generates `Form_BeforeUpdate`, `btnSave_Click`, etc.
+- event binding points to module id and procedure name.
+- module appears in Code Modules tab.
+
+## Import/Export And Conflict Handling
+
+Every sync uses hashes:
+
+- database source hash
+- exported file hash
+- manifest hash
+
+Import should detect:
+
+- file changed, DB unchanged: import cleanly.
+- DB changed, file unchanged: update file on export.
+- both changed: conflict result, no overwrite by default.
+
+Conflict result includes:
+
+- module id
+- db hash
+- file hash
+- paths
+- suggested resolution
+
+## Debugging Strategy
+
+V1 should not promise live Admin breakpoints. Instead:
+
+1. Generate a debug harness project.
+2. Reference `CSharpDB.CodeModules.Runtime`.
+3. Generate sample event contexts from form metadata.
+4. Let VS Code run/debug the harness normally.
+
+Later live debugging can attach to Admin or a dedicated module host, but that
+requires a runtime/debug adapter design.
+
+## Runtime Execution
+
+Admin Forms runtime flow:
+
+1. Event fires.
+2. Runtime resolves event binding to module id/procedure.
+3. Trust policy is checked.
+4. Module set is compiled or loaded from cache.
+5. Procedure is invoked with a typed context.
+6. Result maps back to existing form event behavior:
+ - cancel save
+ - set error/message
+ - mutate current record
+ - dispatch allowed macro/actions
+
+Failures:
+
+- untrusted: block execution with clear message
+- missing module/procedure: block event with diagnostics
+- compile error: block event with diagnostics
+- exception: fail event and record diagnostics
+
+## Relationship To Host Callbacks
+
+Database modules should call host services only through explicit host callbacks:
+
+```csharp
+await DoCmd.RunCommandAsync("SendOpsDigest", new { OrderId = Me.Id });
+```
+
+This keeps external side effects behind host registration and policy.
+
+## Testing Plan
+
+Core API tests:
+
+- create/list/get/update/delete modules
+- module metadata round-trip
+- event binding round-trip
+- import/export manifest generation
+- import conflict detection
+- hash stability
+- build success/failure diagnostics
+- trust enforcement
+
+Admin tests:
+
+- create event stub from form/control event
+- module appears in catalog
+- untrusted module blocks execution
+- trusted module executes
+- compile errors shown in diagnostics
+- form save cancellation from code module
+
+VS Code sidecar tests:
+
+- JSON-RPC list/export/import/build
+- file watcher sync
+- diagnostics mapping
+- conflict reporting
+
+Runtime tests:
+
+- form before update cancel
+- button click mutation
+- missing procedure
+- compile failure
+- thrown exception
+- host callback bridge through `DoCmd.RunCommandAsync`
+
+## Phased Implementation
+
+### Phase 1: Core Storage And API
+
+- Add `CSharpDB.CodeModules`.
+- Add module models.
+- Add system-table storage through `ICSharpDbClient`.
+- Add list/get/upsert/delete.
+- Add tests.
+
+### Phase 2: Import/Export Sync
+
+- Add file layout and manifest.
+- Add export/import APIs.
+- Add conflict detection.
+- Add tests with temp directories.
+
+### Phase 3: Roslyn Build Diagnostics
+
+- Add C# compile service.
+- Add generated runtime references.
+- Return diagnostics.
+- Store last build status/hash.
+
+### Phase 4: Admin Catalog And Stub Generation
+
+- Add Code Modules tab.
+- Add module detail/status.
+- Add form/control event stub generation.
+- Add binding metadata.
+
+### Phase 5: VS Code Sidecar And Extension
+
+- Add sidecar JSON-RPC process.
+- Add VS Code module tree.
+- Add export/import/watch/build commands.
+- Map diagnostics to Problems panel.
+
+### Phase 6: Trusted Runtime Execution
+
+- Add trust state and prompts.
+- Add form event execution path.
+- Add runtime contexts.
+- Add diagnostics.
+
+### Phase 7: Debug Harness
+
+- Generate harness project.
+- Add VS Code launch/task config.
+- Add sample event contexts.
+
+## Open Questions
+
+- Store compiled assemblies in DB or cache only on disk?
+- Exact trust UX and where local trust is stored.
+- Whether form modules should be partial classes generated around user code.
+- How strongly typed `Me` should be in V1.
+- Whether remote databases can execute modules or only edit/sync them.
+- How module runtime policy composes with existing callback policy.
+- Whether code modules can reference other modules in V1.
+- How to version module runtime contracts.
+
+## Recommendation
+
+Proceed with `CSharpDB.CodeModules` as the core. Do not build an Admin Monaco
+editor for V1. Do not make the CLI a required layer.
+
+Use Admin for module discovery, trust, event stub creation, build status, and
+runtime execution. Use VS Code plus a .NET sidecar for editing, sync,
+diagnostics, and debug harness workflows.
diff --git a/docs/trusted-csharp-functions/validation-rules.md b/docs/trusted-csharp-functions/validation-rules.md
new file mode 100644
index 00000000..ce461b52
--- /dev/null
+++ b/docs/trusted-csharp-functions/validation-rules.md
@@ -0,0 +1,233 @@
+# Trusted Validation Rules
+
+Admin Forms can run host-registered validation callbacks before a record is
+saved. The form stores only the validation rule name, fallback message, and JSON
+parameters. The C# callback body is compiled into the host application and
+registered during startup.
+
+Validation rules are trusted in-process callbacks. They are intended for
+business checks that do not belong in generic field metadata, such as
+cross-field validation, tenant-specific policies, or checks against host-owned
+services.
+
+## Register Rules
+
+Register rules with `AddCSharpDbAdminFormValidationRules(...)`:
+
+```csharp
+using CSharpDB.Admin.Forms.Services;
+using CSharpDB.Primitives;
+
+builder.Services.AddCSharpDbAdminFormValidationRules(rules =>
+{
+ rules.AddRule(
+ "CustomerNamePolicy",
+ new DbValidationRuleOptions(
+ Description: "Rejects placeholder customer names.",
+ Timeout: TimeSpan.FromSeconds(2)),
+ static (context, ct) =>
+ {
+ string text = context.Value.IsNull ? string.Empty : context.Value.AsText;
+ string blockedText = context.Parameters.TryGetValue("blockedText", out DbValue configured)
+ && !configured.IsNull
+ ? configured.AsText
+ : "test";
+
+ DbValidationRuleResult result = text.Contains(blockedText, StringComparison.OrdinalIgnoreCase)
+ ? DbValidationRuleResult.Failure(
+ context.FallbackMessage ?? "Customer name is not allowed.",
+ context.FieldName,
+ context.RuleName)
+ : DbValidationRuleResult.Success();
+ return ValueTask.FromResult(result);
+ });
+});
+```
+
+Rule names are case-insensitive identifiers. Duplicate names fail during
+registration so the host fails fast at startup.
+
+## Field-Level Rules
+
+Field-level rules are attached to a bound control through
+`ValidationOverride.AddRules`. The runtime context includes the current field
+value, field name, control id, full record, parameters, and metadata.
+
+```csharp
+new ControlDefinition(
+ "customer-name",
+ "text",
+ new Rect(24, 72, 240, 32),
+ new BindingDefinition("Name", "TwoWay"),
+ PropertyBag.Empty,
+ new ValidationOverride(
+ DisableInferredRules: false,
+ AddRules:
+ [
+ new ValidationRule(
+ "CustomerNamePolicy",
+ "Use the real customer name, not a placeholder.",
+ new Dictionary
+ {
+ ["blockedText"] = "test",
+ }),
+ ],
+ DisableRuleIds: []));
+```
+
+The fallback message is used when the callback returns a failure without a more
+specific message. Parameters are converted to `DbValue` and are available through
+`context.Parameters`.
+
+## Form-Level Rules
+
+Form-level rules live on `FormDefinition.ValidationRules`. Use them for
+cross-field checks and global save policies. A form-level callback can return
+field-specific failures or global failures. A failure with `FieldName = null` or
+an empty field name is shown as a form-level error.
+
+```csharp
+builder.Services.AddCSharpDbAdminFormValidationRules(rules =>
+{
+ rules.AddRule(
+ "CustomerReadyForInsert",
+ static context =>
+ {
+ string status = context.Record.TryGetValue("Status", out DbValue value)
+ && !value.IsNull
+ ? value.AsText
+ : string.Empty;
+
+ return string.Equals(status, "Ready", StringComparison.OrdinalIgnoreCase)
+ ? DbValidationRuleResult.Success()
+ : DbValidationRuleResult.Failure(
+ [
+ new DbValidationFailure(
+ "Status",
+ "Customer status must be Ready before save.",
+ context.RuleName),
+ ],
+ "Customer record is not ready.");
+ });
+});
+```
+
+Attach the rule to the form:
+
+```csharp
+var form = existingForm with
+{
+ ValidationRules =
+ [
+ new ValidationRule(
+ "CustomerReadyForInsert",
+ "Customer status must be Ready before save.",
+ new Dictionary()),
+ ],
+};
+```
+
+## Runtime Context
+
+Every rule receives `DbValidationRuleContext`:
+
+| Property | Meaning |
+| --- | --- |
+| `RuleName` | The registered callback name. |
+| `Scope` | `Field` or `Form`. |
+| `Record` | Full current record as `IReadOnlyDictionary`. |
+| `Parameters` | JSON parameters from form metadata as `DbValue`s. |
+| `Metadata` | Surface, owner, location, correlation id, and form details. |
+| `FormId`, `FormName`, `TableName` | Current form source metadata. |
+| `ControlId`, `FieldName`, `Value` | Field-level context; null/default for form-level rules. |
+| `FallbackMessage` | Designer-provided fallback message. |
+
+Validation callbacks are asynchronous:
+
+```csharp
+public delegate ValueTask DbValidationRuleDelegate(
+ DbValidationRuleContext context,
+ CancellationToken ct);
+```
+
+Pass the cancellation token to host I/O. If a rule uses host services, capture
+thread-safe services in the registration closure or register the rule from a
+host-owned composition root.
+
+## Policy
+
+Validation rules request the `DbExtensionCapability.ValidationRules`
+capability. `DbExtensionPolicies.DefaultHostCallbackPolicy` grants validation
+rules by default. If the host uses a custom policy, it must grant that
+capability:
+
+```csharp
+builder.Services.AddSingleton(new DbExtensionPolicy(
+ AllowExtensions: true,
+ Grants:
+ [
+ new DbExtensionCapabilityGrant(
+ DbExtensionCapability.ValidationRules,
+ DbExtensionCapabilityGrantStatus.Granted,
+ Exports: ["CustomerNamePolicy", "CustomerReadyForInsert"]),
+ ],
+ DefaultTimeout: TimeSpan.FromSeconds(5),
+ RequireSignature: true,
+ AllowedHostModes: DbExtensionHostMode.Embedded));
+```
+
+Scoped grants can use `Exports`, `Tables`, and `Scope`. Deny grants take
+precedence over allows when both match a callback request.
+
+## Failure Behavior
+
+Validation rules fail closed. Save is blocked when a rule is:
+
+- not registered
+- denied by policy
+- timed out
+- canceled by the validation runtime
+- throwing an exception
+- returning a failed result
+
+The Admin callbacks tab shows registered validation rules, saved references,
+policy decisions, and diagnostics history. Missing references mean saved form
+metadata names a rule that the current host has not registered.
+
+## Generated Stubs
+
+Forms export automation metadata for validation rule references. The callback
+catalog can generate starter registrations:
+
+```csharp
+public static void Register(
+ DbFunctionRegistryBuilder functions,
+ DbCommandRegistryBuilder commands,
+ DbValidationRuleRegistryBuilder validationRules)
+{
+ validationRules.AddRule(
+ "CustomerNamePolicy",
+ new DbValidationRuleOptions(
+ Description: "TODO: describe validation rule."),
+ static async (context, ct) =>
+ {
+ await ValueTask.CompletedTask;
+ return DbValidationRuleResult.Success();
+ });
+}
+```
+
+The generated code is a handoff artifact. Keep the rule implementation in the
+host project, not in form JSON or database metadata.
+
+## Runnable Sample
+
+The trusted host sample registers scalar functions, commands, and validation
+rules:
+
+```powershell
+dotnet run --project samples\trusted-csharp-host\TrustedCSharpHostSample.csproj
+```
+
+The sample exports form automation metadata, validates that the referenced
+callbacks are registered, prints generated stubs, and runs a validation demo.
diff --git a/docs/tutorials/fulfillment-ops-admin-automation.md b/docs/tutorials/fulfillment-ops-admin-automation.md
new file mode 100644
index 00000000..39ee9886
--- /dev/null
+++ b/docs/tutorials/fulfillment-ops-admin-automation.md
@@ -0,0 +1,837 @@
+# Fulfillment Ops Admin Automation Tutorial
+
+This tutorial walks through a realistic CSharpDB Admin scenario for an app
+builder. You will use the fulfillment demo database to inspect operational
+tables, work with JSON document collections, review Access-style forms and
+reports, run stored procedures, build a small form automation workflow, and
+inspect the trusted host callbacks that make executable behavior visible
+without storing C# code in the database.
+
+The goal is not just to click through Admin. The goal is to understand the
+product model:
+
+- tables and collections hold operational data
+- stored procedures package reusable SQL work inside the database
+- forms and reports provide Access-style application surfaces
+- macro actions are saved as database metadata
+- trusted callbacks are C# code owned and registered by the host application
+- callback readiness tells app builders whether saved metadata can run in the
+ current host
+
+## Scenario
+
+You are building an internal fulfillment console for an operations team. The
+team needs to:
+
+- review open orders
+- create or review shipments
+- receive purchase orders
+- process returns
+- inspect scanner-session JSON captured by warehouse devices
+- inspect webhook payloads received from external systems
+- run repeatable SQL workflows such as allocation, receiving, and status
+ snapshots
+- run a few host-owned automation hooks from buttons and form events
+- verify that every callback referenced by forms, reports, or automation is
+ registered by the current Admin host
+
+CSharpDB Admin fits this scenario because it combines database browsing,
+document inspection, form design, reports, declarative macro actions, and
+stored procedure execution with trusted C# callback visibility in one tool.
+
+## Prerequisites
+
+Use a copy of the demo database for tutorial work. The tutorial includes create,
+edit, and delete steps for disposable objects.
+
+From the repository root:
+
+```powershell
+Copy-Item `
+ -LiteralPath .\src\CSharpDB.Admin\fulfillment-hub-demo.db `
+ -Destination .\src\CSharpDB.Admin\fulfillment-hub-tutorial.db `
+ -Force
+```
+
+Start Admin against the copied database:
+
+```powershell
+$env:ConnectionStrings__CSharpDB = 'Data Source=fulfillment-hub-tutorial.db'
+dotnet run --project .\src\CSharpDB.Admin\CSharpDB.Admin.csproj --urls http://localhost:62818
+```
+
+Open:
+
+```text
+http://localhost:62818/
+```
+
+The Admin header should show a connected database. If it still shows
+`fulfillment-hub-demo.db`, stop Admin and restart it with the environment
+variable above.
+
+Important trust boundary:
+
+- trusted callbacks are ordinary in-process C# code registered by the host
+- database metadata can reference callback names, but does not store callback
+ implementations
+- CSharpDB does not use WASM, source scanning, or in-process database-owned
+ plugin assemblies for this workflow
+
+## Tour The Admin App
+
+Start in the Object Explorer on the left.
+
+You should see these high-level groups:
+
+- **User Tables**
+- **Collections**
+- **Forms**
+- **Reports**
+- **Procedures**
+- **Callbacks**
+
+Use the filter chips near the top of Object Explorer to focus the list. The
+tutorial uses these objects:
+
+| Area | Objects |
+| --- | --- |
+| Tables | `orders`, `shipments`, `returns`, `products`, `inventory_positions`, `purchase_orders` |
+| Collections | `scanner_sessions`, `webhook_archive` |
+| Forms | `Order Workbench`, `Purchase Order Receiving`, `Return Intake` |
+| Reports | `Low Stock Watch`, `Open Order Queue`, `Shipment Manifest` |
+| Procedures | `AllocateOrder`, `CreateShipment`, `ReceivePurchaseOrder`, `RecordReturn`, `RefreshOperationalStats` |
+| Callbacks | `Slugify`, `EchoAutomationEvent` |
+
+If Object Explorer does not show a new object after a write, click the refresh
+button in the Object Explorer header.
+
+## Browse Operational Tables
+
+1. Click **Tables** in Object Explorer.
+2. Open `orders`.
+3. Page through the order rows.
+4. Open `shipments`.
+5. Open `returns`.
+6. Open `inventory_positions`.
+
+Notice the table browser pattern:
+
+- each table opens in its own tab
+- the toolbar contains refresh and paging controls
+- the grid is for row browsing
+- the detail or edit area is for record-level work
+
+This is the relational side of the fulfillment app. The next sections use
+forms, procedures, collections, reports, and callbacks to build an application
+workflow on top of these tables.
+
+## Use The Existing Forms
+
+1. Click **Forms** in Object Explorer.
+2. Open `Order Workbench`.
+3. Use the data-entry runtime to move through order records.
+4. Open `Purchase Order Receiving`.
+5. Open `Return Intake`.
+
+These forms are the Access-style app surfaces. An app builder can design forms
+over tables and views, then attach event bindings and action sequences.
+
+Use the designer entry for `Order Workbench` when you want to edit layout or
+actions. Use the data-entry entry when you want to run the form as an operator
+would.
+
+## Use Stored Procedures For Reusable Database Work
+
+Stored procedures are database-owned SQL workflows. They are the right tool
+when the behavior is still database work:
+
+| Use a stored procedure when... | Use something else when... |
+| --- | --- |
+| the logic is a reusable set of SQL statements | the user is running one ad hoc query |
+| multiple table updates should succeed or fail together | the workflow needs external APIs, files, email, queues, or services |
+| the operation needs named parameters and defaults | the workflow only changes form UI state |
+| the operation should return follow-up result sets after it writes | the behavior must be host-owned C# |
+
+In this model:
+
+- procedures store SQL and parameter metadata in the database
+- procedure execution runs inside one transaction
+- `@parameter` references in the SQL body are bound from Args JSON or `EXEC`
+ arguments
+- procedures can read and write tables, write audit/event rows, and return one
+ or more result sets
+- procedures do not store C# source and are not host callbacks
+
+### Inspect And Run A Read-Only Procedure
+
+1. Click **Procedures** in Object Explorer.
+2. Open `RefreshOperationalStats`.
+3. Review the body SQL. It returns table stats, shortage-watch rows, and the
+ open order board.
+4. Confirm the procedure has no parameters.
+5. In **Args JSON**, keep:
+
+ ```json
+ {}
+ ```
+
+6. Click **Run**.
+
+Expected result:
+
+- the execution summary reports success
+- the tab lists each statement in the procedure body
+- result grids appear for the `SELECT` statements
+
+You can also run the same procedure from the SQL editor:
+
+```sql
+EXEC RefreshOperationalStats;
+```
+
+### Inspect Write Procedures Before Running Them
+
+Open these procedures and read their body SQL and parameter lists:
+
+- `AllocateOrder`
+- `ReceivePurchaseOrder`
+- `CreateShipment`
+- `RecordReturn`
+
+These are good stored-procedure candidates because they coordinate several
+related table changes and then return review data. For example, `AllocateOrder`
+updates inventory reservations, updates order-line allocation state, updates
+the order, writes an `ops_events` row, and returns follow-up rows for review.
+
+Run write procedures only against the copied tutorial database. They are meant
+to demonstrate operational workflows, but they still mutate real rows in that
+copy.
+
+### Create A Tutorial Read-Only Procedure
+
+Create one disposable procedure so you can practice the editor without changing
+operational data.
+
+1. Right-click **Procedures** and choose **New Procedure...**, or use the
+ command palette item **New Procedure**.
+2. Set **Name** to:
+
+ ```text
+ tutorial_OpenOrderSnapshot
+ ```
+
+3. Set **Description** to:
+
+ ```text
+ Tutorial read-only snapshot of order board rows by status.
+ ```
+
+4. Leave **Enabled** checked.
+5. Set **Body SQL** to:
+
+ ```sql
+ SELECT order_number,
+ customer_name,
+ warehouse_code,
+ order_status,
+ priority_code,
+ total_amount
+ FROM order_fulfillment_board
+ WHERE order_status = @status
+ ORDER BY required_ship_date, priority_code DESC, order_number;
+ ```
+
+6. Add one parameter:
+
+ | Name | Type | Required | Default | Description |
+ | --- | --- | --- | --- | --- |
+ | `status` | `TEXT` | unchecked | `released` | Order status to show. |
+
+7. Click **Save**.
+8. In **Args JSON**, enter:
+
+ ```json
+ {
+ "status": "released"
+ }
+ ```
+
+9. Click **Run** and review the result grid.
+
+The same procedure can be run from the SQL editor with either JSON args:
+
+```sql
+EXEC tutorial_OpenOrderSnapshot { "status": "allocated" };
+```
+
+or SQL-style args:
+
+```sql
+EXEC tutorial_OpenOrderSnapshot @status = 'allocated';
+```
+
+### When Procedures Meet Forms
+
+Forms can reference stored procedures through the `RunProcedure` macro action.
+Use that when a button or form event should invoke a reviewed database workflow,
+such as allocation, receiving, or a read-only snapshot. The action target is the
+procedure name, and the action arguments become procedure arguments.
+
+Use `RunCommand` instead when the workflow must leave the database and call
+host-owned C# services. A common pattern is:
+
+1. `RunProcedure` performs the database work.
+2. `RunCommand` tells host-owned C# to send a notification, publish a message,
+ or call an external service.
+
+The default Admin rendered form host may keep `RunProcedure` disabled unless
+the host explicitly opts in. The procedure editor and SQL editor can still run
+procedures directly.
+
+## Use Collections For Operational JSON
+
+Collections are for JSON documents that are better kept as document payloads
+than flattened relational rows. In this demo they represent warehouse scanner
+sessions and webhook payload archives.
+
+### Inspect `scanner_sessions`
+
+1. Click **Collections** in Object Explorer.
+2. Open `scanner_sessions`.
+3. Confirm the collection tab shows a paged document grid.
+4. Select a document.
+5. Review the indented JSON in the detail panel.
+6. Change the page size to `10`, `25`, `50`, or `100`.
+7. Use the exact-key lookup if you know a document key.
+
+The demo data may fit on a single page. The important behavior is that the tab
+uses the same paged browsing model for two documents as it does for thousands.
+
+The grid shows:
+
+- row number
+- document key
+- JSON kind
+- compact preview
+
+The detail panel shows the selected document as formatted JSON.
+
+### Create And Delete A Disposable Document
+
+Use a tutorial-only key so cleanup is obvious.
+
+1. In `scanner_sessions`, click **New Document**.
+2. Enter this key:
+
+ ```text
+ tutorial_scanner_session
+ ```
+
+3. Enter this JSON:
+
+ ```json
+ {
+ "sessionId": "tutorial_scanner_session",
+ "warehouse": "SEA-01",
+ "operator": "tutorial",
+ "startedAt": "2026-05-01T09:00:00Z",
+ "events": [
+ {
+ "kind": "scan",
+ "sku": "TUTORIAL-SKU",
+ "quantity": 1
+ }
+ ],
+ "status": "review"
+ }
+ ```
+
+4. Confirm **Save** is enabled only while the JSON is valid.
+5. Click **Save**.
+6. Select the saved document and confirm the preview updates.
+7. Click **Delete**.
+8. Confirm the delete prompt.
+
+If you intentionally break the JSON, for example by removing a closing brace,
+Admin keeps **Save** disabled and shows a validation message.
+
+### Inspect `webhook_archive`
+
+1. Open `webhook_archive`.
+2. Select a document.
+3. Review the payload structure.
+4. Review the page controls and page-size selector. If your copied database has
+ more webhook documents than one page can hold, move between pages.
+
+This is the document side of the fulfillment app. The app builder can inspect
+payloads without needing a new transport contract or a custom JSON viewer.
+
+## Build An Access-Style Workflow
+
+This section adds a tutorial-only workflow to `Order Workbench`. The workflow
+demonstrates the macro/action model. It stores action metadata in the form; it
+does not store executable C# code in the database.
+
+Use a copied tutorial database before making these edits.
+
+### Add Tutorial Controls
+
+1. Open the `Order Workbench` designer.
+2. Add a label near the top of the form.
+3. Set the label text to:
+
+ ```text
+ Tutorial review mode is active.
+ ```
+
+4. Copy the generated read-only control ID from the property inspector and
+ write it down as ``.
+5. Add a command button.
+6. Set the button text to:
+
+ ```text
+ Tutorial Review
+ ```
+
+7. Copy its generated control ID as ``.
+8. Add a second command button.
+9. Set the button text to:
+
+ ```text
+ Clear Tutorial Filter
+ ```
+
+10. Copy its generated control ID as ``.
+
+The current designer generates control IDs and shows them as read-only. Use the
+recorded IDs anywhere this tutorial asks for a target control. These controls
+are tutorial-owned because their visible text and action sequence names are
+tutorial-specific, so the cleanup step is still easy.
+
+### Add The Review Action Sequence
+
+Select the **Tutorial Review** button. Add an `OnClick` event, then add an
+action sequence named:
+
+```text
+tutorial_prepare_review
+```
+
+Add these steps in order:
+
+| Step | Action | Target | Value or settings |
+| ---: | --- | --- | --- |
+| 1 | `SetControlVisibility` | `` | `true` |
+| 2 | `SetControlEnabled` | `` | `true` |
+| 3 | `SetControlReadOnly` | `` | `true` |
+| 4 | `ApplyFilter` | `form` | `Status <> 'Closed'` |
+| 5 | `OpenForm` | `Purchase Order Receiving` | arguments JSON shown below |
+| 6 | `RunCommand` | empty | command name: `EchoAutomationEvent`; arguments JSON shown below |
+| 7 | `ShowMessage` | empty | `Tutorial review mode is active.` |
+
+For step 5, use JSON in the arguments field:
+
+```json
+{
+ "mode": "browse",
+ "filter": "Status <> 'Closed'"
+}
+```
+
+For step 6, use JSON in the arguments field:
+
+```json
+{
+ "source": "tutorial_review_button"
+}
+```
+
+Save the form.
+
+Run the data-entry form and click **Tutorial Review**.
+
+Expected result:
+
+- the banner becomes visible
+- the clear-filter button becomes enabled
+- the review button becomes read-only or disabled for editing where supported
+- the current form applies the `Status <> 'Closed'` filter
+- `Purchase Order Receiving` opens in a new tab with the supplied mode/filter
+- `EchoAutomationEvent` runs as a trusted host command
+- the form shows the tutorial message
+
+If the filter fails, check that the form's source has a `Status` field. If your
+copy of the form uses a different field name, change the filter to a field that
+exists on the form source.
+
+### Add The Clear Filter Sequence
+
+Select the **Clear Tutorial Filter** button. Add an `OnClick` event, then add
+an action sequence named:
+
+```text
+tutorial_clear_review
+```
+
+Add these steps:
+
+| Step | Action | Target | Value or settings |
+| ---: | --- | --- | --- |
+| 1 | `ClearFilter` | `form` | empty |
+| 2 | `SetControlVisibility` | `` | `false` |
+| 3 | `SetControlReadOnly` | `` | `false` |
+| 4 | `ShowMessage` | empty | `Tutorial review mode cleared.` |
+
+Save the form, run data entry, and click **Clear Tutorial Filter**.
+
+Expected result:
+
+- the form filter is cleared
+- the banner is hidden
+- the review button becomes editable again where supported
+- a confirmation message appears
+
+### Optional Power Actions: SQL And Procedures
+
+The action model includes `RunSql` and `RunProcedure`. The default Admin
+rendered form host may leave those actions disabled by host policy, even for
+read-only procedure bodies. That is intentional: SQL/procedure actions can
+change data, so a host should enable them deliberately.
+
+To understand the design-time shape, add a disabled or non-running tutorial
+sequence named:
+
+```text
+tutorial_power_actions_reference
+```
+
+Add these steps with `StopOnFailure = false`:
+
+| Step | Action | Target | Value or settings |
+| ---: | --- | --- | --- |
+| 1 | `RunSql` | empty | `SELECT COUNT(*) AS open_orders FROM orders WHERE Status <> 'Closed'` |
+| 2 | `RunProcedure` | `tutorial_OpenOrderSnapshot` | arguments JSON shown below |
+| 3 | `ShowMessage` | empty | `Power action reference completed.` |
+
+For step 2, use JSON in the arguments field:
+
+```json
+{
+ "status": "released"
+}
+```
+
+If you run this sequence in a host where SQL/procedure actions are disabled,
+the expected result is a clear failure message such as `RunSql action is
+disabled by host policy` or `RunProcedure action is disabled by host policy`.
+That is the correct default posture. In a host that enables these actions, use
+a copied tutorial database and only run idempotent or disposable operations.
+
+If `tutorial_OpenOrderSnapshot` was not created earlier, use
+`RefreshOperationalStats` instead and leave the arguments field as `{}`.
+
+## Trusted Host Callbacks
+
+Callbacks are the bridge between saved database metadata and host-owned C#.
+
+1. Click **Callbacks** in Object Explorer.
+2. Open the callback catalog.
+3. Select `Slugify`.
+4. Review its descriptor:
+ - kind: scalar function
+ - runtime: `HostCallback`
+ - return type: text
+ - deterministic/null behavior
+ - capability requests and policy decision
+5. Select `EchoAutomationEvent`.
+6. Review its descriptor:
+ - kind: command
+ - runtime: `HostCallback`
+ - command capability
+ - policy decision
+
+The readiness badge summarizes whether saved metadata references callbacks that
+are missing from the current host.
+
+Important:
+
+- `Slugify` and `EchoAutomationEvent` are registered by the Admin host at
+ startup
+- the database can reference these names
+- the database does not contain their C# implementation
+- descriptor and policy metadata make callbacks visible, but do not sandbox
+ them
+
+## Demonstrate Missing Callback Readiness
+
+Use the copied tutorial database for this step.
+
+1. Open the `Order Workbench` designer.
+2. Select the **Tutorial Review** button.
+3. Add one more `RunCommand` step to `tutorial_prepare_review`.
+4. Set the command name to:
+
+ ```text
+ tutorial_SendOpsDigest
+ ```
+
+5. Set `StopOnFailure = false`.
+6. Add arguments:
+
+ ```json
+ {
+ "source": "tutorial_missing_callback_demo"
+ }
+ ```
+
+7. Save the form.
+8. Open **Callbacks**.
+9. Click **Refresh**.
+
+Expected result:
+
+- the readiness badge reports a missing callback
+- the grid includes `tutorial_SendOpsDigest`
+- the row is marked missing
+- the details panel shows the form/reference location
+- **Stubs** is enabled
+
+Click **Stubs** to copy registration stub source for all missing callbacks, or
+select the missing row and click **Copy Stub** for only that callback.
+
+The copied stub is a developer handoff. It is not stored in the database and it
+is not automatically trusted. A host developer must implement and register the
+command in the host application.
+
+Cleanup options:
+
+- remove the `tutorial_SendOpsDigest` step from the form, or
+- keep it as a deliberate missing-callback example in the copied tutorial
+ database
+
+## Implement The Missing Callback In A Host
+
+The previous section created the app-builder side of the story: database
+metadata now references a command named `tutorial_SendOpsDigest`. The database
+still does not contain C# code. A host developer must add that command to the
+host application and register it at startup.
+
+For the Admin demo host, the registration point is:
+
+```text
+src/CSharpDB.Admin/Services/AdminHostCallbacks.cs
+```
+
+For a smaller VS Code debugging walkthrough, open the sample host project:
+
+```powershell
+code .\samples\trusted-csharp-host
+```
+
+That sample shows the same pattern with `Slugify` and
+`AuditCustomerChange`: callbacks are ordinary C# methods/delegates, registered
+with `DbFunctionRegistry` or `DbCommandRegistry`, then invoked by SQL or form
+automation metadata that references them by name.
+
+A host developer would implement the tutorial command like this inside the
+host's command registry setup:
+
+```csharp
+commands.AddCommand(
+ "tutorial_SendOpsDigest",
+ new DbCommandOptions("Sends a tutorial fulfillment operations digest."),
+ static context =>
+ {
+ string source = context.Arguments.TryGetValue("source", out DbValue value)
+ ? value.AsText
+ : "unknown";
+
+ // Call host-owned services here: email, queues, logging, APIs, etc.
+ return DbCommandResult.Success($"Tutorial digest requested by {source}.");
+ });
+```
+
+Debug flow:
+
+1. Put a breakpoint inside the command delegate.
+2. Start the host from VS Code with `F5`.
+3. Open Admin against the copied tutorial database.
+4. Run the form action that references `tutorial_SendOpsDigest`.
+5. Confirm the breakpoint is hit in host-owned C# code.
+6. Refresh **Callbacks** and confirm readiness changes from missing to
+ registered/allowed when the name and kind match.
+
+The generated stub from Admin is only a starting point for the developer. The
+trusted implementation is the reviewed C# code compiled into the host app.
+
+## Reports And Review
+
+Reports let app builders validate that the workflow supports operational
+review, not only data entry.
+
+1. Click **Reports** in Object Explorer.
+2. Open `Low Stock Watch`.
+3. Preview the report.
+4. Open `Open Order Queue`.
+5. Preview the report.
+6. Open `Shipment Manifest`.
+7. Preview the report.
+
+Use these reports to answer:
+
+- do order changes show up where operators expect?
+- does low-stock review still surface the right products?
+- can a shipment manifest be reviewed after order/shipment work?
+- do report expressions and data sources still load after form automation
+ changes?
+
+If a report uses host-registered scalar functions in calculated expressions,
+those functions appear in the same callback catalog as form commands.
+
+## Security Model
+
+This tutorial uses several different kinds of behavior:
+
+| Behavior | Stored in database? | Executes C#? | Trust model |
+| --- | --- | --- | --- |
+| Tables and rows | yes | no | data only |
+| Collection documents | yes | no | JSON data only |
+| Stored procedures | yes | no | declarative SQL executed by CSharpDB |
+| Form/report metadata | yes | no | declarative metadata |
+| Macro/action sequences | yes | no | interpreted by Admin/runtime |
+| Trusted callbacks | name/reference only | yes | host-owned in-process C# |
+
+CSharpDB deliberately does not treat database files as executable plugin
+packages.
+
+Rejected for this feature track:
+
+- WASM plugin execution
+- source scanning as a security boundary
+- runtime-loaded database-owned assemblies in the CSharpDB process
+- C# source stored in the database and compiled on normal open paths
+
+Future out-of-process .NET/C# workers remain a gated exploration path for
+portable extension packages. They are not part of this tutorial workflow.
+
+## Troubleshooting
+
+### The Admin app opened the original demo database
+
+Stop Admin and restart with:
+
+```powershell
+$env:ConnectionStrings__CSharpDB = 'Data Source=fulfillment-hub-tutorial.db'
+dotnet run --project .\src\CSharpDB.Admin\CSharpDB.Admin.csproj --urls http://localhost:62818
+```
+
+### Object Explorer does not show the object I expect
+
+Click the refresh icon in Object Explorer. If the object is still missing,
+check that the tutorial database copy is the active database.
+
+### A collection document will not save
+
+Check:
+
+- the key is not blank
+- the JSON is valid
+- the collection name is `scanner_sessions` or another existing collection
+- you are not editing a read-only existing key
+
+### A form filter fails
+
+Check:
+
+- the filter references a field that exists on the form source
+- text values are quoted
+- brackets are balanced
+- parameters such as `@status` have matching action arguments
+
+Examples:
+
+```text
+Status <> 'Closed'
+[Status] = 'Ready'
+Quantity > 0
+ClosedAt = null
+```
+
+### `RunSql` or `RunProcedure` is disabled
+
+That is expected in the default safe host posture unless the host explicitly
+enables those rendered form actions. Use `RunCommand` for trusted host-owned C#
+callbacks, or enable SQL/procedure actions only in a host that has reviewed the
+risk.
+
+### A procedure will not save or run
+
+Check:
+
+- the procedure name is a simple identifier
+- every `@parameter` referenced by Body SQL is listed in Parameters
+- Args JSON contains every required parameter
+- argument names match parameter names without the `@` prefix
+- text values are JSON strings in Args JSON or single-quoted in `EXEC`
+- the procedure is enabled
+
+Examples:
+
+```json
+{
+ "status": "released"
+}
+```
+
+```sql
+EXEC tutorial_OpenOrderSnapshot @status = 'released';
+```
+
+### A callback is missing
+
+Open **Callbacks**, refresh the catalog, select the missing callback, and use
+**Copy Stub**. Give the generated stub to the host developer who owns the
+application. The fix is to register host-owned C# code with the same callback
+name and compatible arity.
+
+### A callback is denied
+
+Review the capability grid and policy reason in the callback details. A denied
+callback should not execute. The host policy must grant the requested capability
+before the callback is considered ready.
+
+### The tutorial controls should be removed
+
+Open the `Order Workbench` designer and delete:
+
+- the label with text `Tutorial review mode is active.`
+- the `Tutorial Review` button
+- the `Clear Tutorial Filter` button
+- action sequences whose names start with `tutorial_`
+
+Save the form and refresh Object Explorer.
+
+Also delete the disposable procedure:
+
+- `tutorial_OpenOrderSnapshot`
+
+## What You Built
+
+You used one copied fulfillment database to exercise the full current app-builder
+story:
+
+- browsed relational operational tables
+- inspected existing stored procedures and created a disposable read-only
+ procedure
+- inspected paged JSON collections
+- edited a disposable collection document
+- used forms as Access-style data-entry surfaces
+- added macro-style form action sequences
+- used trusted host callbacks from saved metadata
+- inspected callback readiness and generated missing registration stubs
+- reviewed reports as operational validation surfaces
+- kept executable code host-owned instead of database-owned
+
+That is the intended production posture for the current CSharpDB Admin
+automation model.
diff --git a/samples/README.md b/samples/README.md
index 8d38f2d6..38cba5fe 100644
--- a/samples/README.md
+++ b/samples/README.md
@@ -19,6 +19,7 @@ The SQL dataset samples use a conventional layout with `schema.sql` for setup, `
| `collection-indexing/` | Runnable `Collection` indexing walkthrough | `.csproj`, `Program.cs`, `README.md` |
| `generated-collections/` | Runnable source-generated collection fast-path walkthrough | `.csproj`, `Program.cs`, `README.md` |
| `efcore-provider/` | Runnable EF Core 10 embedded-provider sample | `.csproj`, `Program.cs`, `README.md` |
+| `trusted-csharp-host/` | Runnable trusted callback host sample | scalar functions, commands, validation rules, form automation metadata, `.csproj`, `Program.cs`, `README.md` |
## Tutorials
diff --git a/samples/trusted-csharp-host/.vscode/launch.json b/samples/trusted-csharp-host/.vscode/launch.json
new file mode 100644
index 00000000..c41047b9
--- /dev/null
+++ b/samples/trusted-csharp-host/.vscode/launch.json
@@ -0,0 +1,16 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Debug Trusted C# Host Sample",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build trusted C# host sample",
+ "program": "${workspaceFolder}/bin/Debug/net10.0/TrustedCSharpHostSample.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}",
+ "console": "integratedTerminal",
+ "stopAtEntry": false
+ }
+ ]
+}
diff --git a/samples/trusted-csharp-host/.vscode/tasks.json b/samples/trusted-csharp-host/.vscode/tasks.json
new file mode 100644
index 00000000..5d40ff03
--- /dev/null
+++ b/samples/trusted-csharp-host/.vscode/tasks.json
@@ -0,0 +1,28 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build trusted C# host sample",
+ "type": "process",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "${workspaceFolder}/TrustedCSharpHostSample.csproj"
+ ],
+ "problemMatcher": "$msCompile",
+ "group": "build"
+ },
+ {
+ "label": "run trusted C# host sample",
+ "type": "process",
+ "command": "dotnet",
+ "args": [
+ "run",
+ "--project",
+ "${workspaceFolder}/TrustedCSharpHostSample.csproj"
+ ],
+ "problemMatcher": "$msCompile",
+ "group": "test"
+ }
+ ]
+}
diff --git a/samples/trusted-csharp-host/Program.cs b/samples/trusted-csharp-host/Program.cs
new file mode 100644
index 00000000..a9876d6d
--- /dev/null
+++ b/samples/trusted-csharp-host/Program.cs
@@ -0,0 +1,341 @@
+using CSharpDB.Admin.Forms.Contracts;
+using CSharpDB.Admin.Forms.Models;
+using CSharpDB.Admin.Forms.Services;
+using CSharpDB.Engine;
+using CSharpDB.Primitives;
+
+Console.WriteLine("CSharpDB trusted C# host sample");
+Console.WriteLine();
+
+DbFunctionRegistry functions = DbFunctionRegistry.Create(builder =>
+{
+ builder.AddScalar(
+ "Slugify",
+ arity: 1,
+ options: new DbScalarFunctionOptions(
+ ReturnType: DbType.Text,
+ IsDeterministic: true,
+ NullPropagating: true),
+ invoke: static (_, args) =>
+ DbValue.FromText(Slugify(args[0].AsText)));
+});
+
+List auditLog = [];
+DbCommandRegistry commands = DbCommandRegistry.Create(builder =>
+{
+ builder.AddCommand(
+ "AuditCustomerChange",
+ new DbCommandOptions("Records a customer workflow event."),
+ context =>
+ {
+ long customerId = context.Arguments["Id"].AsInteger;
+ string status = context.Arguments["Status"].AsText;
+ string source = context.Arguments["source"].AsText;
+ string eventName = context.Metadata["event"];
+ string actionSequence = context.Metadata.TryGetValue("actionSequence", out string? value)
+ ? value
+ : "(none)";
+
+ auditLog.Add(
+ $"Customer {customerId} -> {status}; source={source}; event={eventName}; sequence={actionSequence}");
+ return DbCommandResult.Success();
+ });
+});
+
+DbValidationRuleRegistry validationRules = DbValidationRuleRegistry.Create(builder =>
+{
+ builder.AddRule(
+ "CustomerNamePolicy",
+ new DbValidationRuleOptions(
+ Description: "Rejects placeholder customer names.",
+ Timeout: TimeSpan.FromSeconds(2)),
+ static (context, _) =>
+ {
+ string text = context.Value.IsNull ? string.Empty : context.Value.AsText;
+ string blockedText = context.Parameters.TryGetValue("blockedText", out DbValue configured)
+ && !configured.IsNull
+ ? configured.AsText
+ : "test";
+
+ DbValidationRuleResult result = text.Contains(blockedText, StringComparison.OrdinalIgnoreCase)
+ ? DbValidationRuleResult.Failure(
+ context.FallbackMessage ?? "Customer name is not allowed.",
+ context.FieldName,
+ context.RuleName)
+ : DbValidationRuleResult.Success();
+ return ValueTask.FromResult(result);
+ });
+
+ builder.AddRule(
+ "CustomerReadyForInsert",
+ new DbValidationRuleOptions(
+ Description: "Requires the customer workflow status before save."),
+ static context =>
+ {
+ string requiredStatus = context.Parameters.TryGetValue("requiredStatus", out DbValue configured)
+ && !configured.IsNull
+ ? configured.AsText
+ : "Ready";
+ string status = context.Record.TryGetValue("Status", out DbValue value) && !value.IsNull
+ ? value.AsText
+ : string.Empty;
+
+ return string.Equals(status, requiredStatus, StringComparison.OrdinalIgnoreCase)
+ ? DbValidationRuleResult.Success()
+ : DbValidationRuleResult.Failure(
+ [
+ new DbValidationFailure(
+ "Status",
+ context.FallbackMessage ?? $"Status must be {requiredStatus}.",
+ context.RuleName),
+ ],
+ "Customer record is not ready.");
+ });
+});
+
+FormDefinition form = FormAutomationMetadata.NormalizeForExport(CreateCustomerEntryForm());
+DbAutomationMetadata automation = form.Automation
+ ?? throw new InvalidOperationException("The sample form should export automation metadata.");
+
+PrintAutomationMetadata(automation);
+ValidateAutomationMetadata(automation, functions, commands, validationRules);
+PrintGeneratedStub(automation);
+
+await RunSqlScalarFunctionDemoAsync(functions);
+await RunAdminFormsAutomationDemoAsync(form, commands, auditLog);
+await RunAdminFormsValidationDemoAsync(form, validationRules);
+
+Console.WriteLine();
+Console.WriteLine("Set breakpoints inside Slugify, AuditCustomerChange, or a validation rule, then run this sample from VS Code.");
+
+static async Task RunSqlScalarFunctionDemoAsync(DbFunctionRegistry functions)
+{
+ await using Database db = await Database.OpenInMemoryAsync(new DatabaseOptions
+ {
+ Functions = functions,
+ });
+
+ await db.ExecuteAsync("""
+ CREATE TABLE articles (
+ id INTEGER PRIMARY KEY,
+ title TEXT NOT NULL,
+ slug TEXT
+ );
+ """);
+
+ await db.ExecuteAsync("INSERT INTO articles VALUES (1, 'Hello From VS Code', Slugify('Hello From VS Code'));");
+ await db.ExecuteAsync("INSERT INTO articles VALUES (2, 'Trusted CSharpDB Callbacks', Slugify('Trusted CSharpDB Callbacks'));");
+
+ Console.WriteLine();
+ Console.WriteLine("SQL scalar function result:");
+ await using var rows = await db.ExecuteAsync("""
+ SELECT id, title, slug
+ FROM articles
+ ORDER BY id;
+ """);
+
+ while (await rows.MoveNextAsync())
+ {
+ IReadOnlyList row = rows.Current;
+ Console.WriteLine($" {row[0].AsInteger}: {row[1].AsText} -> {row[2].AsText}");
+ }
+}
+
+static async Task RunAdminFormsAutomationDemoAsync(
+ FormDefinition form,
+ DbCommandRegistry commands,
+ IReadOnlyList auditLog)
+{
+ var record = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["Id"] = 42L,
+ ["Name"] = "Ada Lovelace",
+ };
+
+ var dispatcher = new DefaultFormEventDispatcher(commands);
+ FormEventDispatchResult dispatchResult = await dispatcher.DispatchAsync(form, FormEventKind.BeforeInsert, record);
+
+ Console.WriteLine();
+ Console.WriteLine("Admin Forms action sequence result:");
+ Console.WriteLine($" Succeeded: {dispatchResult.Succeeded}");
+ Console.WriteLine($" Status field: {record["Status"]}");
+ foreach (string auditEntry in auditLog)
+ Console.WriteLine($" Audit: {auditEntry}");
+}
+
+static async Task RunAdminFormsValidationDemoAsync(
+ FormDefinition form,
+ DbValidationRuleRegistry validationRules)
+{
+ var record = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["Id"] = 43L,
+ ["Name"] = "Test Account",
+ ["Status"] = "Draft",
+ };
+
+ var validation = new DefaultValidationInferenceService(validationRules, DbExtensionPolicies.DefaultHostCallbackPolicy);
+ IReadOnlyList errors = await validation.EvaluateAsync(form, record);
+
+ Console.WriteLine();
+ Console.WriteLine("Admin Forms validation result:");
+ foreach (ValidationError error in errors)
+ Console.WriteLine($" {error.FieldName}: {error.RuleId} - {error.Message}");
+}
+
+static void PrintAutomationMetadata(DbAutomationMetadata automation)
+{
+ Console.WriteLine("Exported automation metadata:");
+ foreach (DbAutomationScalarFunctionReference function in automation.ScalarFunctions ?? [])
+ {
+ Console.WriteLine(
+ $" scalar {function.Name}/{function.Arity} from {function.Surface}:{function.Location}");
+ }
+
+ foreach (DbAutomationCommandReference command in automation.Commands ?? [])
+ Console.WriteLine($" command {command.Name} from {command.Surface}:{command.Location}");
+
+ foreach (DbAutomationValidationRuleReference rule in automation.ValidationRules ?? [])
+ Console.WriteLine($" validation {rule.Name} from {rule.Surface}:{rule.Location}");
+}
+
+static void ValidateAutomationMetadata(
+ DbAutomationMetadata automation,
+ DbFunctionRegistry functions,
+ DbCommandRegistry commands,
+ DbValidationRuleRegistry validationRules)
+{
+ AutomationValidationResult result = AutomationManifestValidator.Validate(
+ automation,
+ functions,
+ commands,
+ validationRules,
+ new AutomationManifestValidationOptions(RequireMetadata: true));
+
+ Console.WriteLine();
+ Console.WriteLine("Automation validation:");
+ if (result.Succeeded)
+ {
+ Console.WriteLine(" All referenced callbacks are registered.");
+ return;
+ }
+
+ foreach (AutomationValidationIssue issue in result.Issues)
+ Console.WriteLine($" {issue.Severity}: {issue.Message}");
+}
+
+static void PrintGeneratedStub(DbAutomationMetadata automation)
+{
+ string source = AutomationStubGenerator.GenerateCSharp(
+ automation,
+ new AutomationStubGenerationOptions(
+ Namespace: "MyApp.CSharpDbAutomation",
+ ClassName: "CSharpDbAutomationRegistration"));
+
+ Console.WriteLine();
+ Console.WriteLine("Starter C# registration stub:");
+ Console.WriteLine(source);
+}
+
+static FormDefinition CreateCustomerEntryForm()
+ => new(
+ "customers-entry",
+ "Customers Entry",
+ "Customers",
+ DefinitionVersion: 1,
+ SourceSchemaSignature: "sample:customers:v1",
+ Layout: new LayoutDefinition("absolute", 8, SnapToGrid: true, Breakpoints: []),
+ Controls:
+ [
+ new ControlDefinition(
+ "slug-preview",
+ "computed",
+ new Rect(24, 24, 240, 32),
+ Binding: null,
+ Props: new PropertyBag(new Dictionary
+ {
+ ["formula"] = "=Slugify(Name)",
+ }),
+ ValidationOverride: null),
+ new ControlDefinition(
+ "customer-name",
+ "text",
+ new Rect(24, 72, 240, 32),
+ Binding: new BindingDefinition("Name", "TwoWay"),
+ Props: new PropertyBag(new Dictionary
+ {
+ ["label"] = "Name",
+ }),
+ ValidationOverride: new ValidationOverride(
+ DisableInferredRules: false,
+ AddRules:
+ [
+ new ValidationRule(
+ "CustomerNamePolicy",
+ "Use the real customer name, not a placeholder.",
+ new Dictionary
+ {
+ ["blockedText"] = "test",
+ }),
+ ],
+ DisableRuleIds: [])),
+ new ControlDefinition(
+ "customer-status",
+ "text",
+ new Rect(24, 120, 160, 32),
+ Binding: new BindingDefinition("Status", "TwoWay"),
+ Props: new PropertyBag(new Dictionary
+ {
+ ["label"] = "Status",
+ }),
+ ValidationOverride: null),
+ ],
+ EventBindings:
+ [
+ new FormEventBinding(
+ FormEventKind.BeforeInsert,
+ CommandName: string.Empty,
+ ActionSequence: new DbActionSequence(
+ [
+ new DbActionStep(
+ DbActionKind.SetFieldValue,
+ Target: "Status",
+ Value: "Ready"),
+ new DbActionStep(
+ DbActionKind.RunActionSequence,
+ SequenceName: "ReusableCustomerAudit",
+ Arguments: new Dictionary
+ {
+ ["source"] = "trusted-csharp-host-sample",
+ }),
+ ],
+ Name: "PrepareCustomerInsert")),
+ ],
+ ActionSequences:
+ [
+ new DbActionSequence(
+ [
+ new DbActionStep(
+ DbActionKind.RunCommand,
+ CommandName: "AuditCustomerChange"),
+ ],
+ Name: "ReusableCustomerAudit"),
+ ],
+ ValidationRules:
+ [
+ new ValidationRule(
+ "CustomerReadyForInsert",
+ "Customer status must be Ready before save.",
+ new Dictionary
+ {
+ ["requiredStatus"] = "Ready",
+ }),
+ ]);
+
+static string Slugify(string text)
+{
+ return text
+ .Trim()
+ .ToLowerInvariant()
+ .Replace(' ', '-');
+}
diff --git a/samples/trusted-csharp-host/README.md b/samples/trusted-csharp-host/README.md
new file mode 100644
index 00000000..90d7a21b
--- /dev/null
+++ b/samples/trusted-csharp-host/README.md
@@ -0,0 +1,135 @@
+# Trusted C# Host Sample
+
+This sample is the VS Code workflow for CSharpDB's trusted C# integration. It
+is a normal C# project: open this folder in VS Code, set breakpoints inside the
+registered callbacks, and run or debug the host process.
+
+It demonstrates:
+
+- registering a trusted scalar function with `DbFunctionRegistry`
+- calling that function from SQL
+- registering a trusted command with `DbCommandRegistry`
+- registering trusted Admin Forms validation rules with
+ `DbValidationRuleRegistry`
+- exporting Admin Forms automation metadata
+- validating automation metadata against registered callbacks
+- generating starter C# registration stubs from automation metadata
+- running an Admin Forms action sequence that sets a field
+- invoking a reusable named Admin Forms action sequence that calls the command
+- running field-level and form-level validation callbacks
+- inspecting an Access-style macro form manifest with open form, filter,
+ run SQL, and conditional UI rule actions
+- inspecting callback arguments and metadata in console output
+
+The sample keeps the important runtime boundary visible: C# callback bodies live
+in the host project. The database/form metadata stores names and action data
+only.
+
+## Run From VS Code
+
+1. Open `samples/trusted-csharp-host` in VS Code.
+2. Install the C# Dev Kit or C# extension if VS Code prompts for it.
+3. Press `F5`, or run the task `run trusted C# host sample`.
+4. Watch the sample print exported automation metadata.
+5. Watch validation confirm that referenced callbacks are registered.
+6. Inspect the generated starter C# registration stub.
+7. Put breakpoints in `Slugify`, `AuditCustomerChange`, or one of the
+ validation rule callbacks.
+
+## Developer Handoff Story
+
+The intended production workflow has two roles:
+
+1. An app builder creates metadata in Admin, such as a calculated expression
+ that calls `Slugify(...)` or a form action sequence that runs
+ `AuditCustomerChange`.
+2. A host developer owns the C# implementation, registers that callback during
+ startup, and debugs it from VS Code.
+
+The database/form metadata stores callback names, argument values, and
+reference locations. It does not store C# source or compiled assemblies.
+
+`Program.cs` shows both registration paths:
+
+```csharp
+builder.AddScalar(
+ "Slugify",
+ arity: 1,
+ options: new DbScalarFunctionOptions(
+ ReturnType: DbType.Text,
+ IsDeterministic: true,
+ NullPropagating: true),
+ invoke: static (_, args) => DbValue.FromText(Slugify(args[0].AsText)));
+```
+
+```csharp
+builder.AddCommand(
+ "AuditCustomerChange",
+ new DbCommandOptions("Records a customer workflow event."),
+ context =>
+ {
+ long customerId = context.Arguments["Id"].AsInteger;
+ string status = context.Arguments["Status"].AsText;
+
+ return DbCommandResult.Success(
+ $"Customer {customerId} changed to {status}.");
+});
+```
+
+Validation rules use the same host-owned pattern:
+
+```csharp
+builder.AddRule(
+ "CustomerNamePolicy",
+ new DbValidationRuleOptions(
+ Description: "Rejects placeholder customer names.",
+ Timeout: TimeSpan.FromSeconds(2)),
+ static (context, ct) =>
+ {
+ string text = context.Value.IsNull ? string.Empty : context.Value.AsText;
+
+ DbValidationRuleResult result = text.Contains("test", StringComparison.OrdinalIgnoreCase)
+ ? DbValidationRuleResult.Failure(
+ context.FallbackMessage ?? "Customer name is not allowed.",
+ context.FieldName,
+ context.RuleName)
+ : DbValidationRuleResult.Success();
+ return ValueTask.FromResult(result);
+ });
+```
+
+When Admin reports a missing callback, use the generated stub as the handoff
+artifact. The host developer pastes the registration shape into the host app,
+replaces the stub body with reviewed C# code, sets a breakpoint, then runs the
+host with `F5` to verify the metadata reference reaches the callback.
+
+## Run From Terminal
+
+```powershell
+dotnet run --project samples\trusted-csharp-host\TrustedCSharpHostSample.csproj
+```
+
+Expected output includes:
+
+- exported callback metadata and locations
+- validation status
+- generated starter registration code
+- slug values from SQL
+- an audit entry from the reusable form action sequence
+- validation errors from a field rule and a form rule
+
+The audit entry prints callback metadata such as the form event and reusable
+action sequence name, along with callback arguments passed from the form record
+and action sequence.
+
+The validation result prints the failing field, rule id, and message. The same
+rules block save in Admin Forms when referenced by saved form metadata.
+
+## Files
+
+- `Program.cs` contains the host registration code, metadata validation, stub
+ generation, and runnable demo.
+- `access-style-macro-form.json` contains a Phase 8 form manifest with richer
+ macro actions and conditional UI rules.
+- `.vscode/launch.json` launches the sample under the debugger.
+- `.vscode/tasks.json` builds and runs the sample from VS Code tasks.
diff --git a/samples/trusted-csharp-host/TrustedCSharpHostSample.csproj b/samples/trusted-csharp-host/TrustedCSharpHostSample.csproj
new file mode 100644
index 00000000..a8952804
--- /dev/null
+++ b/samples/trusted-csharp-host/TrustedCSharpHostSample.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/samples/trusted-csharp-host/access-style-macro-form.json b/samples/trusted-csharp-host/access-style-macro-form.json
new file mode 100644
index 00000000..211f6fa0
--- /dev/null
+++ b/samples/trusted-csharp-host/access-style-macro-form.json
@@ -0,0 +1,178 @@
+{
+ "formId": "products-entry-access-macros",
+ "name": "Products Entry Access Macros",
+ "tableName": "Products",
+ "definitionVersion": 1,
+ "sourceSchemaSignature": "sample:products:v1",
+ "layout": {
+ "layoutMode": "absolute",
+ "gridSize": 8,
+ "snapToGrid": true,
+ "breakpoints": []
+ },
+ "controls": [
+ {
+ "controlId": "statusBox",
+ "controlType": "text",
+ "rect": { "x": 24, "y": 24, "width": 180, "height": 32 },
+ "binding": { "fieldName": "Status", "mode": "TwoWay" },
+ "props": { "placeholder": "Status" }
+ },
+ {
+ "controlId": "openOrdersButton",
+ "controlType": "commandButton",
+ "rect": { "x": 224, "y": 24, "width": 120, "height": 32 },
+ "props": { "text": "Open Orders" },
+ "eventBindings": [
+ {
+ "event": "onClick",
+ "commandName": "",
+ "stopOnFailure": true,
+ "actionSequence": {
+ "name": "OpenOrders",
+ "steps": [
+ {
+ "kind": "openForm",
+ "target": "Orders Entry",
+ "arguments": {
+ "recordId": "$record.Id",
+ "mode": "view"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "controlId": "showOpenOrdersButton",
+ "controlType": "commandButton",
+ "rect": { "x": 24, "y": 72, "width": 152, "height": 32 },
+ "props": { "text": "Show Open" },
+ "eventBindings": [
+ {
+ "event": "onClick",
+ "commandName": "",
+ "stopOnFailure": true,
+ "actionSequence": {
+ "name": "ShowOpenOrders",
+ "steps": [
+ {
+ "kind": "applyFilter",
+ "target": "ordersGrid",
+ "value": "[Status] = @status",
+ "arguments": {
+ "status": "Open"
+ }
+ },
+ {
+ "kind": "setControlVisibility",
+ "target": "clearOrdersFilterButton",
+ "value": true
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "controlId": "clearOrdersFilterButton",
+ "controlType": "commandButton",
+ "rect": { "x": 184, "y": 72, "width": 152, "height": 32 },
+ "props": {
+ "text": "Clear Filter",
+ "visible": false
+ },
+ "eventBindings": [
+ {
+ "event": "onClick",
+ "commandName": "",
+ "stopOnFailure": true,
+ "actionSequence": {
+ "name": "ClearOrdersFilter",
+ "steps": [
+ {
+ "kind": "clearFilter",
+ "target": "ordersGrid"
+ },
+ {
+ "kind": "setControlVisibility",
+ "target": "clearOrdersFilterButton",
+ "value": false
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "controlId": "archiveButton",
+ "controlType": "commandButton",
+ "rect": { "x": 344, "y": 72, "width": 120, "height": 32 },
+ "props": { "text": "Archive" },
+ "eventBindings": [
+ {
+ "event": "onClick",
+ "commandName": "",
+ "stopOnFailure": true,
+ "actionSequence": {
+ "name": "ArchiveProduct",
+ "steps": [
+ {
+ "kind": "runSql",
+ "value": "UPDATE Products SET Status = @status WHERE Id = @id",
+ "arguments": {
+ "status": "Archived",
+ "id": "$record.Id",
+ "refresh": true
+ }
+ },
+ {
+ "kind": "setControlProperty",
+ "target": "statusBox",
+ "value": "Archived",
+ "arguments": {
+ "property": "value"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "controlId": "ordersGrid",
+ "controlType": "datagrid",
+ "rect": { "x": 24, "y": 128, "width": 720, "height": 260 },
+ "props": {
+ "childTable": "Orders",
+ "dataGridMode": "related",
+ "foreignKeyField": "ProductId",
+ "parentKeyField": "Id",
+ "visibleColumns": [ "OrderId", "Status", "Total" ],
+ "allowAdd": true,
+ "allowEdit": true,
+ "allowDelete": false
+ }
+ }
+ ],
+ "rules": [
+ {
+ "ruleId": "archived-state",
+ "condition": "[Status] = 'Archived'",
+ "description": "Archived products cannot be edited from the entry form.",
+ "effects": [
+ {
+ "controlId": "statusBox",
+ "property": "readOnly",
+ "value": true
+ },
+ {
+ "controlId": "archiveButton",
+ "property": "enabled",
+ "value": false
+ }
+ ]
+ }
+ ]
+}
diff --git a/scripts/Start-CSharpDbAdminDirect.ps1 b/scripts/Start-CSharpDbAdminDirect.ps1
index 26ecc76a..7f5b63c9 100644
--- a/scripts/Start-CSharpDbAdminDirect.ps1
+++ b/scripts/Start-CSharpDbAdminDirect.ps1
@@ -6,11 +6,12 @@ Configures the admin site for direct mode, then starts only the admin host.
This script is intended for local development and manual operator workflows.
It updates `src/CSharpDB.Admin/appsettings.json` to use
`CSharpDbTransport.Direct`, removes any daemon endpoint from the admin config,
-ensures a connection string exists, and then starts `CSharpDB.Admin`.
+ensures a connection string exists, builds `CSharpDB.Admin`, and then starts it.
-The script does not install a Windows service or a background task. It launches
-one `dotnet run` process. If you close the shell that launched this script, the
-child process continues running until you stop it explicitly.
+The script does not install a Windows service or a background task. It builds
+the admin project first, then launches one `dotnet run --no-build` process. If
+you close the shell that launched this script, the child process continues
+running until you stop it explicitly.
.PARAMETER NoLaunch
Only updates the admin configuration. Does not start the admin host.
@@ -26,7 +27,8 @@ later with `Stop-Process`.
Overrides the admin database connection string before launch.
.PARAMETER AdminStartupTimeoutSeconds
-How long to wait for the admin endpoint to start accepting TCP connections.
+How long to wait for the admin endpoint to start accepting TCP connections
+after the admin project has built and the app process has launched.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File .\scripts\Start-CSharpDbAdminDirect.ps1
@@ -52,7 +54,7 @@ param(
[switch]$OpenAdmin,
[switch]$PassThru,
[string]$ConnectionString,
- [int]$AdminStartupTimeoutSeconds = 30
+ [int]$AdminStartupTimeoutSeconds = 60
)
$ErrorActionPreference = 'Stop'
@@ -182,6 +184,47 @@ function Select-PreferredUrl {
return $urls[0]
}
+function Get-LaunchUris {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$ApplicationUrl
+ )
+
+ return @(
+ $ApplicationUrl.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries) |
+ ForEach-Object { $_.Trim() } |
+ Where-Object { $_ } |
+ ForEach-Object { [Uri]$_ }
+ )
+}
+
+function Test-TcpEndpoint {
+ param(
+ [Parameter(Mandatory = $true)]
+ [Uri]$Uri
+ )
+
+ $client = $null
+
+ try {
+ $client = [System.Net.Sockets.TcpClient]::new()
+ $connectTask = $client.ConnectAsync($Uri.Host, $Uri.Port)
+
+ if ($connectTask.Wait([TimeSpan]::FromSeconds(1)) -and $client.Connected) {
+ return $true
+ }
+ }
+ catch {
+ }
+ finally {
+ if ($null -ne $client) {
+ $client.Dispose()
+ }
+ }
+
+ return $false
+}
+
function Wait-ForTcpEndpoint {
param(
[Parameter(Mandatory = $true)]
@@ -193,28 +236,45 @@ function Wait-ForTcpEndpoint {
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
- $client = $null
+ if (Test-TcpEndpoint -Uri $Uri) {
+ return $true
+ }
+
+ Start-Sleep -Milliseconds 500
+ }
+
+ return $false
+}
+
+function Wait-ForAnyTcpEndpoint {
+ param(
+ [Parameter(Mandatory = $true)]
+ [Uri[]]$Uris,
+ [Parameter(Mandatory = $true)]
+ [int]$TimeoutSeconds,
+ [System.Diagnostics.Process]$Process
+ )
- try {
- $client = [System.Net.Sockets.TcpClient]::new()
- $connectTask = $client.ConnectAsync($Uri.Host, $Uri.Port)
+ $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
- if ($connectTask.Wait([TimeSpan]::FromSeconds(1)) -and $client.Connected) {
- return $true
+ while ((Get-Date) -lt $deadline) {
+ if ($null -ne $Process) {
+ $Process.Refresh()
+ if ($Process.HasExited) {
+ return $null
}
}
- catch {
- }
- finally {
- if ($null -ne $client) {
- $client.Dispose()
+
+ foreach ($uri in $Uris) {
+ if (Test-TcpEndpoint -Uri $uri) {
+ return $uri
}
}
Start-Sleep -Milliseconds 500
}
- return $false
+ return $null
}
function Stop-ProcessIfRunning {
@@ -242,6 +302,7 @@ $originalAdminJson = $adminConfig | ConvertTo-Json -Depth 20
$adminLaunchProfile = Get-LaunchProfile -LaunchSettings $adminLaunchSettings -PreferredProfileName 'CSharpDB.Admin'
$adminUrl = Select-PreferredUrl -ApplicationUrl $adminLaunchProfile.Profile.applicationUrl -PreferredScheme 'https'
+$adminUris = @(Get-LaunchUris -ApplicationUrl $adminLaunchProfile.Profile.applicationUrl)
$csharpDbSection = Get-OrAddProperty -Object $adminConfig -Name 'CSharpDB' -DefaultValue ([pscustomobject]@{})
Set-JsonProperty -Object $csharpDbSection -Name 'Transport' -Value 'direct'
@@ -291,19 +352,30 @@ if ($NoLaunch) {
$adminArgs = @(
'run',
+ '--no-build',
'--project', $adminProjectPath,
'--launch-profile', $adminLaunchProfile.Name
)
+Write-Host "Building admin project..."
+dotnet build $adminProjectPath | Out-Host
+if ($LASTEXITCODE -ne 0) {
+ throw "The admin project build failed with exit code $LASTEXITCODE."
+}
+
Write-Host "Starting admin profile '$($adminLaunchProfile.Name)'..."
$adminProcess = Start-Process -FilePath 'dotnet' -ArgumentList $adminArgs -WorkingDirectory $repoRoot -PassThru
-if ($adminUrl) {
- if (-not (Wait-ForTcpEndpoint -Uri ([Uri]$adminUrl) -TimeoutSeconds $AdminStartupTimeoutSeconds)) {
+if ($adminUris.Count -gt 0) {
+ $listeningUri = Wait-ForAnyTcpEndpoint -Uris $adminUris -TimeoutSeconds $AdminStartupTimeoutSeconds -Process $adminProcess
+ if ($null -eq $listeningUri) {
Stop-ProcessIfRunning -Process $adminProcess
- throw "The admin site did not start listening on $adminUrl within $AdminStartupTimeoutSeconds seconds."
+
+ $configuredUrls = ($adminUris | ForEach-Object { $_.ToString().TrimEnd('/') }) -join ', '
+ throw "The admin site did not start listening on any configured URL ($configuredUrls) within $AdminStartupTimeoutSeconds seconds."
}
+ $adminUrl = $listeningUri.ToString().TrimEnd('/')
Write-Host "Admin is listening on $adminUrl (PID $($adminProcess.Id))."
if ($OpenAdmin) {
diff --git a/src/CSharpDB.Admin.Forms/CSharpDB.Admin.Forms.csproj b/src/CSharpDB.Admin.Forms/CSharpDB.Admin.Forms.csproj
index f7d414f9..6b923cd2 100644
--- a/src/CSharpDB.Admin.Forms/CSharpDB.Admin.Forms.csproj
+++ b/src/CSharpDB.Admin.Forms/CSharpDB.Admin.Forms.csproj
@@ -10,6 +10,7 @@
+
diff --git a/src/CSharpDB.Admin.Forms/Components/Designer/ActionSequenceEditor.razor b/src/CSharpDB.Admin.Forms/Components/Designer/ActionSequenceEditor.razor
new file mode 100644
index 00000000..e369d36e
--- /dev/null
+++ b/src/CSharpDB.Admin.Forms/Components/Designer/ActionSequenceEditor.razor
@@ -0,0 +1,581 @@
+@using System.Text.Json
+@using CSharpDB.Admin.Forms.Serialization
+@using CSharpDB.Primitives
+@inject DbCommandRegistry CommandRegistry
+
+