refactor(sql): .Browser references .Driver — the same layering violation #512 removed from MQTT #526

Closed
opened 2026-07-31 06:17:41 -04:00 by dohertj2 · 1 comment
Owner

ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.csproj carries a ProjectReference to the runtime
Driver.Sql
project in addition to .Contracts — the same layering violation Gitea #512 just removed
from the MQTT browser, in a second driver.

Found while closing #512, not by a sweep. #512's .Browser csproj comment claimed the MQTT browser
was why ZB.MOM.WW.OtOpcUa.Core.dll reached the AdminUI's build/deploy graph. After the MQTT fix,
Driver.Mqtt.dll, Polly.Core and Serilog all disappeared from AdminUI/bin — but Core.dll did
not
, because Driver.Sql.Browser still pulls it in. So the old comment's attribution was wrong, and
this is the remaining cause.

Worse than #512 in one respect, better in another. Unlike the MQTT case it carries no comment at
all
justifying the exception — a reader has no way to know whether it is deliberate. But the shared
surface is smaller: MQTT's exception existed to avoid forking security-relevant TLS/CA-pin logic, and it
is not yet established that Sql's has an equivalent justification.

Do this first: find out what Sql.Browser actually uses from Driver.Sql. The fix follows from
that and only then:

  • a pure/transport-free helper (connection-string assembly, a type map) ⇒ move it to .Contracts,
    which is the cheap fix and needs no new project;
  • something that needs the data provider type (the MQTT shape — a builder returning a vendor type)
    ⇒ a leaf project mirroring ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Transport;
  • genuinely nothing ⇒ delete the reference.

Guard against the wrong fix, exactly as #512 warned: do not resolve this by copying the shared code
into the browser. If it touches credentials or TLS, a second copy is a silent security divergence, and
that is the hazard the exception was taken to avoid in the first place.

Acceptance. Driver.Sql.Browser references .Contracts (plus any new leaf) only; the Sql browse
suite still passes; ZB.MOM.WW.OtOpcUa.Core.dll is absent from the AdminUI's output. Extend
MqttBrowserLayeringTests' shape to Sql — or generalise it — so every bespoke browser is guarded rather
than just the one that had an issue filed.

`ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.csproj` carries a `ProjectReference` to the **runtime `Driver.Sql`** project in addition to `.Contracts` — the same layering violation Gitea #512 just removed from the MQTT browser, in a second driver. **Found while closing #512, not by a sweep.** #512's `.Browser` csproj comment claimed the MQTT browser was why `ZB.MOM.WW.OtOpcUa.Core.dll` reached the AdminUI's build/deploy graph. After the MQTT fix, `Driver.Mqtt.dll`, `Polly.Core` and `Serilog` all disappeared from `AdminUI/bin` — but **`Core.dll` did not**, because `Driver.Sql.Browser` still pulls it in. So the old comment's attribution was wrong, and this is the remaining cause. **Worse than #512 in one respect, better in another.** Unlike the MQTT case it carries **no comment at all** justifying the exception — a reader has no way to know whether it is deliberate. But the shared surface is smaller: MQTT's exception existed to avoid forking security-relevant TLS/CA-pin logic, and it is not yet established that Sql's has an equivalent justification. **Do this first:** find out *what* `Sql.Browser` actually uses from `Driver.Sql`. The fix follows from that and only then: - a **pure/transport-free** helper (connection-string assembly, a type map) ⇒ move it to `.Contracts`, which is the cheap fix and needs no new project; - something that **needs the data provider type** (the MQTT shape — a builder returning a vendor type) ⇒ a leaf project mirroring `ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Transport`; - genuinely **nothing** ⇒ delete the reference. **Guard against the wrong fix**, exactly as #512 warned: do not resolve this by copying the shared code into the browser. If it touches credentials or TLS, a second copy is a silent security divergence, and that is the hazard the exception was taken to avoid in the first place. **Acceptance.** `Driver.Sql.Browser` references `.Contracts` (plus any new leaf) only; the Sql browse suite still passes; `ZB.MOM.WW.OtOpcUa.Core.dll` is absent from the AdminUI's output. Extend `MqttBrowserLayeringTests`' shape to Sql — or generalise it — so every bespoke browser is guarded rather than just the one that had an issue filed.
Author
Owner

Done — master 803f4e6e (merged 9fa701b1).

ISqlDialect + SqlServerDialect moved to a new leaf project ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect, referenced by the runtime driver and the browser alike. Assembly name is .Dialect; the namespace stays ZB.MOM.WW.OtOpcUa.Driver.Sql, matching the Driver.Mqtt.Transport precedent — the extraction cost zero using changes anywhere.

Acceptance, measured in both directions on a clean AdminUI build (this issue exists because #512 asserted the same thing without measuring):

before: ZB.MOM.WW.OtOpcUa.Core.dll + ZB.MOM.WW.OtOpcUa.Driver.Sql.dll present
after:  both absent

Two corrections to the issue as written.

It says the reference "carries no comment at all justifying the exception". It carried a detailed one from the original scaffold commit (3b75e1ac), naming the design doc and asking that nobody "fix" it without reading that context. That doesn't make the reference right, but the "a reader has no way to know whether it is deliberate" argument doesn't hold, and the design doc's project table recorded it as deliberate too — both are now corrected.

Second, it was the .Transport shape, not the cheap one. ISqlDialect exposes DbProviderFactory and SqlServerDialect returns SqlClientFactory.Instance, so .Contracts — deliberately provider-free — cannot hold it. The old comment's premise was sound and survives: the dialect's catalog SQL is the browse engine, so it must be shared rather than duplicated. What was wrong was only the conclusion that sharing required a reference to the runtime driver.

Two dependencies the cut edge surfaced as the implicit ones they were. The AdminUI itself read SqlDriver.DriverTypeName in three places (tag-editor map, tag-config validator, Sql address picker); all now read DriverTypeNames.Sql, which is exactly what that constant resolves to and exactly the repoint SqlDriverBrowser's own comment had deferred until a factory existed for the shared constant. And SqlDriverFormContractTests genuinely needs the runtime driver — it asserts the AdminUI form's emitted JSON against the factory that reads it — so AdminUI.Tests now references it explicitly. A test project referencing a runtime driver is fine; tests are not in the AdminUI's deploy graph, which is the whole point.

Guard: generalised, as asked. DriverBrowserLayeringTests (Commons.Tests) walks the csproj graph of every *.Browser project in the tree, transitively. It reads csproj files rather than loaded assemblies because the cost being prevented is a build-graph cost — the compiler emits no assembly reference for an unused ProjectReference, so MqttBrowserLayeringTests' assembly-reference approach cannot see that case. The two are complementary and now point at each other.

Falsified three ways, each red then green again: the direct reference re-added to the browser; the same reference one hop out via .Dialect (proving the walk is really transitive); and both controls — browsers-discovered and references-parsed — which exist so an empty discovery cannot pass the absence assertions vacuously.

Done — master `803f4e6e` (merged `9fa701b1`). `ISqlDialect` + `SqlServerDialect` moved to a new leaf project `ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect`, referenced by the runtime driver and the browser alike. Assembly name is `.Dialect`; the **namespace stays `ZB.MOM.WW.OtOpcUa.Driver.Sql`**, matching the `Driver.Mqtt.Transport` precedent — the extraction cost zero `using` changes anywhere. **Acceptance, measured in both directions on a clean AdminUI build** (this issue exists because #512 asserted the same thing without measuring): ``` before: ZB.MOM.WW.OtOpcUa.Core.dll + ZB.MOM.WW.OtOpcUa.Driver.Sql.dll present after: both absent ``` **Two corrections to the issue as written.** It says the reference "carries **no comment at all** justifying the exception". It carried a detailed one from the original scaffold commit (`3b75e1ac`), naming the design doc and asking that nobody "fix" it without reading that context. That doesn't make the reference right, but the "a reader has no way to know whether it is deliberate" argument doesn't hold, and the design doc's project table recorded it as deliberate too — both are now corrected. Second, it was the **`.Transport` shape, not the cheap one**. `ISqlDialect` exposes `DbProviderFactory` and `SqlServerDialect` returns `SqlClientFactory.Instance`, so `.Contracts` — deliberately provider-free — cannot hold it. The old comment's *premise* was sound and survives: the dialect's catalog SQL **is** the browse engine, so it must be shared rather than duplicated. What was wrong was only the conclusion that sharing required a reference to the runtime driver. **Two dependencies the cut edge surfaced as the implicit ones they were.** The AdminUI itself read `SqlDriver.DriverTypeName` in three places (tag-editor map, tag-config validator, Sql address picker); all now read `DriverTypeNames.Sql`, which is exactly what that constant resolves to and exactly the repoint `SqlDriverBrowser`'s own comment had deferred until a factory existed for the shared constant. And `SqlDriverFormContractTests` genuinely needs the runtime driver — it asserts the AdminUI form's emitted JSON against the factory that reads it — so `AdminUI.Tests` now references it explicitly. A test project referencing a runtime driver is fine; tests are not in the AdminUI's deploy graph, which is the whole point. **Guard: generalised, as asked.** `DriverBrowserLayeringTests` (Commons.Tests) walks the csproj graph of every `*.Browser` project in the tree, **transitively**. It reads csproj files rather than loaded assemblies because the cost being prevented is a build-graph cost — the compiler emits no assembly reference for an unused `ProjectReference`, so `MqttBrowserLayeringTests`' assembly-reference approach cannot see that case. The two are complementary and now point at each other. Falsified three ways, each red then green again: the direct reference re-added to the browser; the same reference one hop out via `.Dialect` (proving the walk is really transitive); and both controls — browsers-discovered and references-parsed — which exist so an empty discovery cannot pass the absence assertions vacuously.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#526