Skip to content
bitzorcas
中EN

Reference

DocumentStructure template preview, apply, and Scriban

A source-verified walkthrough of naming context, Scriban compilation and rendering, preview trees, parent-first category creation, partial-success windows, target authorization, and idempotency.

Last updated

Preview and Apply share dynamic-name rendering but have very different consequences. Preview only returns a tree; Apply directly creates Documents categories. Both currently hide expression failures by falling back to the static Name, while Apply also lacks target-instance authorization, proven atomicity, and idempotency.

1. NamingContext

The request dictionary is converted into a case-insensitive Dictionary<string, object> and rendered with NamingConvention.PascalCase. Documentation should describe only syntax demonstrated by the actual compiler instead of promising every Scriban feature.

Preview dynamic folder names
POST /api/v1/document-structures/templates/template-1/preview
Content-Type: application/json
{
"namingContext": {
"clientName": "Northstar Labs",
"year": "2026"
}
}

NamingContext currently has no key-count, key-name, value-length, or sensitive-data limit. Expressions and context both enter the compiler, so a production contract needs input limits, an allowed-variable model, and execution-resource controls.

2. Preview tree construction

Preview loads a tenant-scoped template and nodes, finds roots, sorts by SortOrder, and recursively scans all nodes to build Children. Create and Update reject cycles, but old or manually corrupted data can still make runtime recursion unsafe. Preview has no independent depth or visited-set guard.

The result exposes OriginalName and RenderedName, but not node ID, parent ID, expression error, or warning. A client cannot identify the failing expression; it sees only the static fallback.

Diagnostic preview shape required for safe editing
{
"nodeId": "node-12",
"originalName": "Client folder",
"renderedName": "Northstar Labs",
"diagnostics": [],
"children": []
}

This diagnostic envelope is a target design, not the current response DTO.

3. Sync-over-async compiler calls

Both RenderName implementations call CompileAsync(..., CancellationToken.None).GetAwaiter().GetResult() and synchronously wait for RenderAsync the same way. This violates the repository rule against blocking asynchronous work and discards request cancellation.

GA target: asynchronous rendering with visible diagnostics
// Target contract: current source still blocks and silently falls back.
var compiled = await templateCompiler.CompileAsync(
node.DynamicNameExpression,
TemplateEngine.Scriban,
cancellationToken);
if (compiled.IsFailure)
return RenderedNode.Failed(node.Id, compiled.Error);
// A required expression failure blocks Apply instead of becoming a static name.
var rendered = await templateCompiler.RenderAsync(
compiled.Value!, context, NamingConvention.PascalCase, cancellationToken);

4. Silent fallback semantics

A failed compile Result, failed render Result, or any exception returns node.Name. Misspelled variables, compiler outages, timeouts, and unsafe expressions therefore appear successful.

Preview should return per-node diagnostics. Apply should fail closed by default. Only an explicitly optional dynamic name should permit configurable fallback, with a warning and metric.

5. Apply preconditions actually enforced

The handler checks only template existence and IsActive. It neither compares request.TargetType with template.TargetType nor validates TargetId as a knowledge base. It does not load the knowledge-base object or enforce the caller’s instance-level management permission. A generic Update action on IAuthorizedRequest is not proof that this specific TargetId can be managed.

Every TargetType reaches DocumentCategory.Create(tenantId, request.TargetId, ...). The result still echoes request.TargetType, which can falsely suggest multi-owner dispatch.

6. Parent-first creation algorithm

Root nodes are created in SortOrder. Children enter a queue and are created only after the parent has a CategoryId; otherwise, they are requeued. The iteration bound is nodes.Count + 10.

Current Apply call
// TargetType is echoed today; it does not dispatch to another owner implementation.
var result = await mediator.Send(new ApplyTemplate.Command(
TemplateId: "template-1",
TargetType: "KnowledgeBase",
TargetId: "kb-100",
NamingContext: new Dictionary<string, string>
{
["ClientName"] = "Northstar Labs"
}), cancellationToken);
// Current success returns only IDs that were actually created.
return result.Value!.CreatedCategoryIds;

A healthy validated graph can complete. If persisted nodes are corrupt or concurrently replaced, however, parents may never resolve. When the bound is reached, the queue may still be nonempty and the handler does not fail. That is a concrete partial-success gap.

7. One-save-per-node failure window

Every node calls DocumentCategory.Create and repository SaveAsync. If node N fails, the previous N-1 saves have already occurred. A command transaction pipeline might roll them all back if every adapter shares the same unit of work, but no dedicated failure-injection test proves that. The manual must not claim unconditional atomicity.

A stronger boundary is a Documents-owned CreateCategoryTree contract. It validates target, conflicts, parent graph, and limits once, saves in one transaction, and returns the complete node mapping.

8. Repeat and concurrent application

The command has no ApplicationId or IdempotencyKey, and categories have no unique template-application provenance. A timeout retry, double click, or concurrent request can create duplicate folder trees. Name conflict behavior cannot substitute for idempotency because it depends on DocumentCategory rules and existing data.

An idempotency record should carry TenantId, TemplateId, TemplateVersion, TargetType, TargetId, ApplicationId, status, and node mapping. Running returns status, Completed replays the result, and Failed exposes a deliberate retry or compensation path.

9. Rendered-name conflicts

Different template nodes may render to the same name, and the target may already contain a same-name category. Apply does not render and validate a complete plan before writing. The first conflict can occur halfway through the tree and enlarge the partial-write window.

Preview with a target context should expose conflicts. Apply should freeze a complete plan containing rendered name, parent, conflict policy, expected count, template version, and authorization evidence before the first write.

10. Expression security boundary

Tenant administrators supply Scriban expressions, but configuration remains untrusted input. The engine needs limits for accessible objects/functions, recursion, loops, output length, compile/runtime, and memory. It must not expose files, network, reflection, or host objects. Secrets and PII in NamingContext must not enter errors, logs, or unrelated expressions.

11. Observability contract

Record template/application ID, version, target, node count, render duration, created count, failed node, and compensation state. Do not record full NamingContext or sensitive rendered names. Useful metrics include preview error, fallback, partial apply, idempotency replay, category conflict, and transaction rollback.

12. Failure matrix

Injection pointEvidence required
first/Nth compilePreview diagnostics; Apply writes nothing
render timeout/cancelcancellation propagates; no background writes
Nth category savecomplete rollback or proven compensation
missing parentexplicit failure, never partial success
concurrent same ApplicationIdone tree and one stable result
revoked target permissionrefusal before creation
template deleted during Applypinned version or conflict

13. Review commands

Terminal window
# Current sync-over-async, ignored cancellation, and per-node save path.
rg -n "GetAwaiter\(\)\.GetResult|CancellationToken.None|SaveAsync\(category|remaining.Count" \
src/Platform/DocumentStructure -g '*.cs'
# Target authorization, idempotency, and compensation should currently have no matches.
rg -n "EnsureCanManageKnowledgeBase|ApplicationId|Compensat|CreateCategoryTree" \
src/Platform/DocumentStructure -g '*.cs'

DocumentStructure overview · Template versioning and persistence · Testing and GA

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%