Files
scadalink-design/docs/plans/2026-05-18-contained-template-names-design.md

131 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Contained Names for Composition-Derived Templates — Design
**Date:** 2026-05-18
**Status:** Approved (brainstorming) — implementation to follow
## Context
When a template composes another template, the `MigrateCompositionsToDerived`
migration materializes the composition slot as its own *derived* `Template`
row. Each derived template was named `OwnerTemplate.Name + "." + Composition.
InstanceName` — e.g. the `Pump` slot inside `Motor Controller` became a
template literally named **`Motor Controller.Pump`**.
The dotted name exists only to satisfy the global `UNIQUE` index on
`Template.Name`: there is already a standalone `Pump` template, so the derived
one could not also be called `Pump`.
This is the opposite of how AVEVA System Platform models containment — there,
an object has a **contained name** (`Pump`, unique only within its container)
and a **hierarchical name** (`MotorController.Pump`) that is *derived*, not
stored. This design moves ScadaLink to that model.
### Decisions taken during brainstorming
- **Adopt the contained-name model.** A derived template's stored `Name` is its
contained name (the composition's `InstanceName`); the qualified path is
computed, not stored.
- **Keep derived templates materialized.** The `MigrateCompositionsToDerived`
design is not revisited — the contained-name fix works with derived
templates as-is.
- **`QualifiedName` is computed**, not a denormalized column — a stored column
would need cascade maintenance on every rename up the chain. Composition
chains are 13 deep, so walking them on read is cheap.
### Audit findings (read-only, completed before this design)
- No code resolves a template *by name*: there is no `GetTemplateByNameAsync`;
the Management API and CLI are entirely ID-based.
- The template tree already hides derived templates (`Where(t => !t.IsDerived)`).
- Flattening, member canonical names, and collision detection use
`composition.InstanceName` — never `Template.Name`.
- The flattened/deployed artifact carries no template name; deployment keys on
ID + revision hash. The dotted name never reaches sites.
- `TemplateComposition` already has a DB `UNIQUE (TemplateId, InstanceName)`
index — the contained-name uniqueness backstop already exists.
- Every other `Template.Name` use is either dotted-name construction
(`TemplateService` + the old migration), a dotted-name uniqueness pre-check,
or reading `.Name` for display after an ID lookup.
## Design
### 1. Schema
- The `UNIQUE` index on `Template.Name` becomes a **filtered** unique index:
`UNIQUE ... WHERE [IsDerived] = 0`. User-authored (base) templates stay
globally unique; derived templates do not need to be.
- Derived templates' uniqueness is enforced entirely by the existing
`UNIQUE (TemplateId, InstanceName)` index on `TemplateComposition` — a slot
name is unique within its owner.
- A new EF Core migration:
- Rewrites every derived row: `Name = OwnerComposition.InstanceName`.
- Replaces the `Template.Name` index with the filtered version.
- `TemplateConfiguration`: `HasIndex(t => t.Name).IsUnique()` gains
`.HasFilter("[IsDerived] = 0")`.
### 2. Qualified name
A new pure helper resolves the hierarchical name on demand:
```
QualifiedName(template, byId, compById):
if not template.IsDerived: return template.Name
comp = compById[template.OwnerCompositionId]
owner = byId[comp.TemplateId]
return QualifiedName(owner, …) + "." + comp.InstanceName
```
It lives in a small static class in `ScadaLink.TemplateEngine` (e.g.
`TemplateNaming`), takes lookups the callers already have, and is null-safe
(a missing link falls back to the stored `Name`).
### 3. TemplateService
The composition create / rename / cascade paths currently build
`$"{owner.Name}.{instanceName}"` for the derived `Template.Name`. They change
to store the **contained name** (`instanceName`) directly:
- `CreateCascadedCompositionAsync` — derived `Name = instanceName`.
- Nested cascade (`EnumerateCascadeNames`, cascade-rename) — each level stores
its own `InstanceName`, not the accumulated path.
- The dotted-name global collision pre-check (`allTemplates.Any(t => t.Name ==
newName)`) is removed — uniqueness is the in-owner `InstanceName` check that
already runs (and the DB index behind it).
### 4. UI
- `TemplateEdit`: the page title shows the contained `Name`; the qualified
path is shown as a breadcrumb / subtitle (the existing "— composed inside X
as Y" line already carries this information and becomes the canonical
presentation).
- "inherits X", cycle-detection messages, and deletion-blocked messages use
`QualifiedName` when the referenced template is derived, so a bare `TempSensor`
is never ambiguous in a message.
### 5. Out of scope
- Un-materializing derived templates (treating a composition purely as a slot).
- Any change to member/attribute canonical names — they already use
`InstanceName`.
## Testing & verification
- **`TemplateNaming` unit tests** — qualified name for a base template, a
one-level derived template, and a nested derived template; null-safe
fallback.
- **`TemplateService` tests** — composition create and rename store the
contained name; two derived templates with the same contained name under
different owners are allowed; a duplicate slot name on one owner is rejected.
- **Migration** — applied to the dev cluster; existing derived rows
(`Motor Controller.Pump`, `Tank Monitor.DrivePump.TempSensor`, …) collapse to
contained names; verified in the browser.
## Affected files
- `src/ScadaLink.ConfigurationDatabase/Configurations/TemplateConfiguration.cs`
- `src/ScadaLink.ConfigurationDatabase/Migrations/*` (new migration)
- `src/ScadaLink.TemplateEngine/TemplateService.cs`
- `src/ScadaLink.TemplateEngine/TemplateNaming.cs` (new)
- `src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor`
- Docs: `docs/requirements/Component-TemplateEngine.md`