Inside CSharpDB v3.6.0: Admin Collections, Custom Form Controls & Trusted C#

CSharpDB v3.6.0 is an Admin-and-extensibility release. The engine work from v3.3.0 still carries the durable-write story; this release pushes Admin further toward an Access-style application builder, and opens the door for host developers to extend Admin Forms with their own C# without forking the project.

Five themes ship together:

Collections UI
Document collections are now first-class Admin objects with a browser, JSON editor, and command-palette entries.
Custom Form Controls
Register your own designer toolbox entries, runtime renderers, and property editors without touching saved form JSON.
Access-Style Macros
Phase 8 actions: openForm, applyFilter, runSql, runProcedure, setControlProperty, plus form-level conditional rules.
Trusted C# Sample
A runnable VS Code project that shows scalar functions, commands, and validation rules wired to a real Admin form.
Reports Parity Plan
A six-phase roadmap for closing the highest-impact gaps against Access reports, starting with bounded saved-query previews and full export.

The rest of this post walks each theme, with the code shapes you actually need to use it, and ends with a look at the database-owned C# code modules planned for the next major chunk of work.

Document Collections Get a First-Class UI

CSharpDB has supported document collections through ICSharpDbClient for several releases. v3.6.0 finally treats them as first-class Admin objects alongside tables, views, forms, and reports.

The Object Explorer now has a Collections filter chip and group. The collection group menu offers New Collection… and Refresh. Each collection item supports Open, New Document…, and Copy Name. The command palette adds New Collection plus one entry per existing collection.

Each collection opens in its own collection:{name} tab using the same toolbar, pager, grid, and detail-panel language as table data tabs:

  • 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.
  • Exact-key lookup uses GetDocumentAsync(collectionName, key).

Defaults are deliberately conservative: page sizes are 10, 25, 50, or 100 (default 25); collection names follow the same identifier shape as the direct client (^[A-Za-z_][A-Za-z0-9_]*$); and deleting all documents leaves the collection itself in place because a drop-collection API is still future work.

This is intentionally a v1: path-index management, document-content search, collection rename, and collection drop are queued for a follow-up. The point of v3.6.0 is to give document workloads the same browse/edit ergonomics SQL tables have had since day one.

Form Controls Are Now Extensible

Admin Forms have always persisted controls as ControlDefinition.ControlType plus a free-form PropertyBag. v3.6.0 turns that wire shape into a developer API. A host can add toolbox entries, placement defaults, property editing, designer previews, and runtime rendering — all without changing saved form JSON.

Registration sits next to the existing services call:

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<string, object?>
        {
            ["max"] = 5,
            ["displayMode"] = "buttons",
        },
        DesignerPreviewComponentType = typeof(RatingDesignerPreview),
        RuntimeComponentType = typeof(RatingRuntimeControl),
        PropertyEditorComponentType = typeof(RatingPropertyEditor),
    });
});

Custom components receive a single typed Context parameter. Designer previews use FormControlDesignContext; runtime controls use FormControlRuntimeContext, which exposes the form, control metadata, current record, bound field/value, resolved choices, enabled/read-only state, validation error, tab index, SetValueAsync, and DispatchEventAsync; property editors use FormControlPropertyContext and write back through Context.SetPropertyAsync(name, value).

For controls that just need a few simple properties, you can skip a custom property editor entirely and define PropertyDescriptors instead. The generic editor supports Text, TextArea, Number, Checkbox, and Select:

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"),
        ],
    },
];

The Admin host already includes a compiled sample sampleRating control under the Custom toolbox group, gated behind an env var so you can flip it on for local testing without touching production:

$env:AdminForms__EnableSampleControls = 'true'
dotnet run --project src\CSharpDB.Admin --urls http://127.0.0.1:61818

V1 limits worth knowing about: custom controls are leaf controls. They can be placed inside existing tabControl pages, but custom containers do not own or render child controls yet. And because form JSON never loads arbitrary component assemblies, custom components must be compiled into the host app or referenced assemblies — the registry is a developer surface, not a runtime plug-in surface.

Access-Style Macro Actions

Phase 8 of the Admin Forms work extends action sequences with the Access-style verbs people actually reach for when building a form-driven app. Action metadata stays declarative; host applications still own executable C#, the SQL/procedure opt-in policy, database connections, and navigation behavior.

The new actions are:

Action Runtime behavior
openForm Resolves a form by id or name and asks the host to open it. Arguments support mode, recordId, primaryKey, id, filter, and where.
closeForm Asks the host to close the active form entry tab or surface.
applyFilter Filters the current form record list (target: "form") or a rendered datagrid control.
clearFilter Clears the form or data-grid filter selected by target.
runSql Executes SQL only when the host enables SQL actions. @name parameters resolve from action arguments.
runProcedure Executes a named database procedure when procedure actions are enabled. Procedure body can run multiple statements.
setControlProperty Overrides rendered control props such as visible, enabled, readOnly, text, placeholder, and bound value.
setControlVisibility, setControlEnabled, setControlReadOnly Short forms for the corresponding setControlProperty calls.

Filters use the same bracketed field expression style as conditions, so the rest of the form metadata stays uniform:

{
  "kind": "applyFilter",
  "target": "ordersGrid",
  "value": "[Status] = @status AND [Total] > @minimum",
  "arguments": {
    "status": "Open",
    "minimum": 100
  }
}

runProcedure is the right verb when the workflow should invoke reusable database-owned SQL — allocating an order, receiving a purchase order, processing a return. runCommand is the right verb when the workflow needs host-owned C# behavior such as email, queues, external APIs, or filesystem access. A common pairing is runProcedure for the database update followed by runCommand for the host notification.

Conditional UI now lives at the form level too. Rules apply control property effects whenever their condition is true:

{
  "ruleId": "archived-state",
  "condition": "[Status] = 'Archived'",
  "effects": [
    { "controlId": "statusBox",    "property": "readOnly", "value": true },
    { "controlId": "archiveButton", "property": "enabled",  "value": false }
  ]
}

The designer's property inspector ships with a rules editor and a validation panel for action/rule readiness. For a worked example that combines open-form, data-grid filtering, SQL execution, control property changes, and conditional rules in one manifest, see samples/trusted-csharp-host/access-style-macro-form.json in the repo.

Rendered Admin form runtimes can leave runSql and runProcedure disabled by policy, which keeps database-mutating actions explicit at the host boundary. Action execution is observable through FormActionDiagnostics.Listener, which carries action kind, target, form id, event name, action sequence name, step index, elapsed time, success/cancellation state, result message, exception message, and metadata for every invocation.

The Trusted C# Host Sample

Trusted in-process C# extensions for SQL and Admin Forms have been growing across the last few releases. v3.6.0 ships a single, self-contained sample that pulls all of it together as the recommended developer workflow: samples/trusted-csharp-host.

Open the folder in VS Code, press F5, and the sample demonstrates — in one process —:

  • 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 that metadata against the registered callbacks
  • generating a starter C# registration stub from automation metadata
  • running an Admin Forms action sequence that sets a field
  • invoking a reusable named 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 rule actions

The runtime boundary the sample protects is simple: C# callback bodies live in the host project. The database and form metadata only store names and action data. That is what makes the workflow safe to hand off between roles.

The Scalar Function Path

Scalar functions are the smallest piece. Register a name and an arity, return a DbValue, and SQL can call it like any built-in:

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)));
});

From SQL: INSERT INTO articles VALUES (1, 'Hello From VS Code', Slugify('Hello From VS Code')); and the C# body runs in-process.

The Command Path

Commands are the workflow verb — the thing Admin Forms action sequences invoke when a field changes, a button is clicked, or a record is about to insert. The handler receives arguments and metadata and returns a result the form pipeline can act on:

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"];

            // Audit, queue work, call a service — this is your code.
            return DbCommandResult.Success(
                $"Customer {customerId} -> {status}; from {source}");
        });
});

The Validation Rule Path

Validation rules are new in v3.6.0. They use the same host-owned pattern as scalar functions and commands, and they participate in the existing field-level and form-level validation pipelines:

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;

            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);
        });
});

The same rules block save in Admin Forms when referenced by saved form metadata. From a host developer's perspective, a missing rule is exactly as visible as a missing command — metadata validation reports it, and the stub generator can produce the registration shape.

Two Roles, One Repo

The reason this sample exists is to make the two-role workflow obvious:

  1. An app builder creates metadata in Admin — a calculated expression that calls Slugify(…), an action sequence that runs AuditCustomerChange, a control rule that references CustomerNamePolicy.
  2. A host developer owns the C# implementation, registers the callback at startup, and debugs it from VS Code.

When Admin reports a missing callback, the generated registration stub is the handoff artifact. Paste it into the host app, replace the body with reviewed C#, set a breakpoint, run the host with F5, and verify the metadata reference reaches your code.

From a terminal, the same loop is one command:

dotnet run --project samples\trusted-csharp-host\TrustedCSharpHostSample.csproj

Expected output includes the exported callback metadata, validation status, the generated registration stub, slug values from SQL, an audit entry from the reusable form action sequence, and validation errors from a field rule and a form rule.

Reports Access Parity: The Plan

The reports surface today already covers the core Access-style design loop — banded designer, grouping/sorting, calculated text and aggregates, preview pagination, browser print, schema-drift warnings, and trusted command-backed lifecycle events for OnOpen, BeforeRender, and AfterRender.

The v3.6.0 review identified two issues that matter for production reporting:

  • Saved-query previews are unbounded before trimming. Saved-query reports run source.BaseSql directly and only trim rows after the full result has materialized. Table and view reports use the preview query builder with a row limit; saved-query reports bypass that path. Fix: apply the preview row cap before materializing saved-query results, while preserving group/sort ordering.
  • Preview and print are capped, with no full output/export pipeline. The preview service caps output at 10,000 rows and 250 pages — reasonable for an interactive preview, but Access-style reports also need a full render/export path. Fix: separate preview rendering from full rendering, add export targets (PDF first, then HTML, CSV, spreadsheet-friendly), and make full export explicit and cancellable.

Those two findings drive Phase 1 of the Access-parity roadmap. The phase ordering is deliberate — runtime safety and output foundations come before broadening the designer surface:

Phase Focus
1 Runtime safety and output foundations — bounded saved-query previews, full report render pipeline, export support, print warnings
2 Record sources, parameters, and filters — parameter prompts, runtime filters, saved filter definitions, query designer integration
3 Grouping, totals, and pagination semantics — intervals, keep-together, force-new-page, running totals, overflow rules
4 Design productivity — Layout View, Report Wizard, Label Wizard, style themes
5 Broader controls and formatting — image, rich text, barcode, chart, page break, subreport, conditional formatting
6 Distribution and operations — email/report delivery, scheduled reports, artifact history, large-report cancellation

The product position is “reliable report production and distribution” before “wider designer canvas.” A printable preview engine is useful; an exportable, parameterized, distributable report is what makes the surface stand on its own against Access for operational reporting.

What's Next: Database-Owned C# Code Modules

The trusted C# story in v3.6.0 is host-owned. The host project registers callbacks, the database stores names and references, and the runtime joins them. That is the right model for application services, external integrations, and anything privileged.

For the “code travels with the database” case — the Access tradition where a form's Form_BeforeUpdate and a button's btnShip_Click live alongside the form definition itself — we are working on a third lane: database code modules.

The plan, captured in docs/trusted-csharp-functions/database-code-modules-vscode-plan.md, has three product lanes that compose:

Lane Code Lives In Use For
Declarative macros/actions Form JSON / database metadata No-code app behavior, navigation, field updates, command orchestration
Host callbacks C# host project startup registration External services, compiled integrations, application-owned code
Database code modules Database module metadata, synced to files for editing Access-like form/business logic that travels with the database

Generated form modules will look familiar to Access developers, but with real C# tooling underneath:

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();
    }
}

The architecture deliberately splits responsibilities. Admin handles module discovery, trust prompts, event-stub creation, build status, and runtime execution. VS Code — with a small .NET sidecar exposing JSON-RPC — owns editing, file sync, Roslyn diagnostics, and a debug-harness workflow. There is no Monaco editor inside Admin and no in-browser IDE to maintain.

Trust will fail closed by default: a database must be explicitly trusted, the module source hash must match the trusted hash, the build must be clean, runtime policy must allow execution, and the host must opt in. None of that is automatic just because a database file happens to contain modules.

The phased delivery is straightforward: a CSharpDB.CodeModules core API first, then file-based import/export with conflict detection, Roslyn diagnostics, the Admin catalog and stub generator, the VS Code sidecar and extension, trusted runtime execution, and finally the debug-harness generator. There is no commitment to live Admin breakpoints in v1 — debugging is a normal C# project that happens to be generated from database modules.

Closing Thoughts

v3.6.0 is the release where Admin starts to feel like an application platform rather than just a database UI. Document collections are first-class, custom form controls are a real extension point, macro actions cover the verbs Access developers reach for, and the trusted C# story has a runnable sample that shows the whole loop end-to-end.

What's intentionally not here: an in-browser code editor, an automatic remote-execution path for arbitrary database C#, or any change to how host callbacks are trusted. Those are deliberate gates — the database code modules plan walks toward them carefully, behind explicit trust, file-hash verification, and host opt-in.

If you build on Admin Forms, the highest-value upgrade today is registering one custom control through AddCSharpDbAdminFormControls and one validation rule through DbValidationRuleRegistry. Both surfaces are stable, both ship with a working sample, and both are exactly the shape future code modules will hand control to.