feat(config): make Equipment.DriverInstanceId nullable + driver-less AdminUI support + migration
This commit is contained in:
@@ -106,6 +106,15 @@ In `cmd_populate_equipment`:
|
|||||||
become no-ops; remove for clarity. Keep the `Namespace` INSERT/teardown.
|
become no-ops; remove for clarity. Keep the `Namespace` INSERT/teardown.
|
||||||
- Fix the stale `# … "an Equipment namespace has a driver" expectations` comment.
|
- Fix the stale `# … "an Equipment namespace has a driver" expectations` comment.
|
||||||
|
|
||||||
|
### AdminUI — two production derefs (found at build time, not by the grep sweep)
|
||||||
|
Making the column nullable surfaced two `.razor` sites the impact grep missed (caught by `TreatWarningsAsErrors`):
|
||||||
|
- `Components/Pages/Clusters/TagEdit.razor:191` — `db.Equipment.Where(e => driverIds.Contains(e.DriverInstanceId))` (CS8604).
|
||||||
|
Behavior-preserving fix: guard `e.DriverInstanceId != null && …` (SQL already excludes NULL from an `IN` set, so this only satisfies the compiler).
|
||||||
|
- `Components/Pages/Clusters/EquipmentEdit.razor` — the equipment editor loads `DriverInstanceId` into a non-null
|
||||||
|
`FormModel` (line 183, CS8601) and **mandates** a driver on save (`"Pick a driver instance."`). Decision: give it
|
||||||
|
**full driver-less support** — `FormModel.DriverInstanceId` → `string?`, add a "(none / driver-less)" option to the
|
||||||
|
driver dropdown, relax the mandatory-driver validation, and persist NULL when none is selected (normalize empty → null).
|
||||||
|
|
||||||
### Noted, not changing (YAGNI)
|
### Noted, not changing (YAGNI)
|
||||||
- `sp_ComputeGenerationDiff` includes `DriverInstanceId` in a `CHECKSUM(...)`. It is NULL-tolerant for
|
- `sp_ComputeGenerationDiff` includes `DriverInstanceId` in a `CHECKSUM(...)`. It is NULL-tolerant for
|
||||||
this one-time transition and sits on the **dormant** generation-diff path (the active deploy gate is
|
this one-time transition and sits on the **dormant** generation-diff path (the active deploy gate is
|
||||||
|
|||||||
@@ -20,9 +20,15 @@
|
|||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Equipment.cs` (line ~23)
|
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Equipment.cs` (line ~23)
|
||||||
|
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/TagEdit.razor` (~line 191 — null-guard the EF predicate)
|
||||||
|
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/EquipmentEdit.razor` (FormModel + dropdown + save — full driver-less support)
|
||||||
- Create: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/<timestamp>_NullableEquipmentDriverInstanceId.cs` (generated by `dotnet ef`)
|
- Create: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/<timestamp>_NullableEquipmentDriverInstanceId.cs` (generated by `dotnet ef`)
|
||||||
- Auto-modify: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs` (regenerated by `dotnet ef`)
|
- Auto-modify: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs` (regenerated by `dotnet ef`)
|
||||||
|
|
||||||
|
**AdminUI surface (found at build time — the nullable change breaks two `.razor` sites under `TreatWarningsAsErrors`):**
|
||||||
|
- `TagEdit.razor:191`: `db.Equipment.Where(e => driverIds.Contains(e.DriverInstanceId))` → guard `e.DriverInstanceId != null && driverIds.Contains(e.DriverInstanceId)` (behavior-preserving; SQL already excludes NULL).
|
||||||
|
- `EquipmentEdit.razor`: full driver-less support — `FormModel.DriverInstanceId` → `string?`; add a "(none / driver-less)" option to the driver `<select>`; remove the mandatory `if (string.IsNullOrEmpty(_form.DriverInstanceId)) { _error = "Pick a driver instance."; return; }` check (~line 209); on save (both the IsNew `Add` ~line 222 and the update ~line 245) write `string.IsNullOrWhiteSpace(_form.DriverInstanceId) ? null : _form.DriverInstanceId`.
|
||||||
|
|
||||||
**Context:** `DriverInstanceId` is currently `public required string DriverInstanceId { get; set; }`. The EF config `OtOpcUaConfigDbContext.ConfigureEquipment` has **no `.IsRequired()`** call and **no FK relationship** for it (it's a logical FK only) — so EF infers NOT NULL purely from the C# `required string` type. Changing the type to `string?` flips the column to nullable; no fluent-config change is needed. The index `IX_Equipment_Driver` is a plain non-unique index and stays valid on a nullable column.
|
**Context:** `DriverInstanceId` is currently `public required string DriverInstanceId { get; set; }`. The EF config `OtOpcUaConfigDbContext.ConfigureEquipment` has **no `.IsRequired()`** call and **no FK relationship** for it (it's a logical FK only) — so EF infers NOT NULL purely from the C# `required string` type. Changing the type to `string?` flips the column to nullable; no fluent-config change is needed. The index `IX_Equipment_Driver` is a plain non-unique index and stays valid on a nullable column.
|
||||||
|
|
||||||
**Step 1 — Change the entity property.** In `Equipment.cs`, change:
|
**Step 1 — Change the entity property.** In `Equipment.cs`, change:
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ public sealed class Equipment
|
|||||||
/// <summary>UUIDv4, IMMUTABLE across all generations of the same EquipmentId. Downstream-consumer join key.</summary>
|
/// <summary>UUIDv4, IMMUTABLE across all generations of the same EquipmentId. Downstream-consumer join key.</summary>
|
||||||
public Guid EquipmentUuid { get; set; }
|
public Guid EquipmentUuid { get; set; }
|
||||||
|
|
||||||
/// <summary>Logical FK to the driver providing data for this equipment.</summary>
|
/// <summary>
|
||||||
public required string DriverInstanceId { get; set; }
|
/// Optional logical FK to the driver providing data for this equipment.
|
||||||
|
/// <c>null</c> = VirtualTag-only / driver-less equipment (no field driver).
|
||||||
|
/// </summary>
|
||||||
|
public string? DriverInstanceId { get; set; }
|
||||||
|
|
||||||
/// <summary>Optional logical FK to a multi-device driver's device.</summary>
|
/// <summary>Optional logical FK to a multi-device driver's device.</summary>
|
||||||
public string? DeviceId { get; set; }
|
public string? DeviceId { get; set; }
|
||||||
|
|||||||
+1758
File diff suppressed because it is too large
Load Diff
+40
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class NullableEquipmentDriverInstanceId : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "DriverInstanceId",
|
||||||
|
table: "Equipment",
|
||||||
|
type: "nvarchar(64)",
|
||||||
|
maxLength: 64,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(64)",
|
||||||
|
oldMaxLength: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "DriverInstanceId",
|
||||||
|
table: "Equipment",
|
||||||
|
type: "nvarchar(64)",
|
||||||
|
maxLength: 64,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(64)",
|
||||||
|
oldMaxLength: 64,
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-1
@@ -546,7 +546,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
|||||||
.HasColumnType("nvarchar(512)");
|
.HasColumnType("nvarchar(512)");
|
||||||
|
|
||||||
b.Property<string>("DriverInstanceId")
|
b.Property<string>("DriverInstanceId")
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("nvarchar(64)");
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ else
|
|||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label" for="driver">Driver instance</label>
|
<label class="form-label" for="driver">Driver instance</label>
|
||||||
<InputSelect id="driver" @bind-Value="_form.DriverInstanceId" class="form-select form-select-sm">
|
<InputSelect id="driver" @bind-Value="_form.DriverInstanceId" class="form-select form-select-sm">
|
||||||
<option value="">— pick a driver —</option>
|
<option value="">(none / driver-less)</option>
|
||||||
@foreach (var d in _drivers)
|
@foreach (var d in _drivers)
|
||||||
{
|
{
|
||||||
<option value="@d.DriverInstanceId">@d.DriverInstanceId — @d.Name (@d.DriverType)</option>
|
<option value="@d.DriverInstanceId">@d.DriverInstanceId — @d.Name (@d.DriverType)</option>
|
||||||
@@ -206,7 +206,6 @@ else
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_form.UnsLineId)) { _error = "Pick a UNS line."; return; }
|
if (string.IsNullOrEmpty(_form.UnsLineId)) { _error = "Pick a UNS line."; return; }
|
||||||
if (string.IsNullOrEmpty(_form.DriverInstanceId)) { _error = "Pick a driver instance."; return; }
|
|
||||||
|
|
||||||
await using var db = await DbFactory.CreateDbContextAsync();
|
await using var db = await DbFactory.CreateDbContextAsync();
|
||||||
if (IsNew)
|
if (IsNew)
|
||||||
@@ -219,7 +218,7 @@ else
|
|||||||
{
|
{
|
||||||
EquipmentId = equipmentId,
|
EquipmentId = equipmentId,
|
||||||
EquipmentUuid = uuid,
|
EquipmentUuid = uuid,
|
||||||
DriverInstanceId = _form.DriverInstanceId,
|
DriverInstanceId = string.IsNullOrWhiteSpace(_form.DriverInstanceId) ? null : _form.DriverInstanceId,
|
||||||
UnsLineId = _form.UnsLineId,
|
UnsLineId = _form.UnsLineId,
|
||||||
Name = _form.Name,
|
Name = _form.Name,
|
||||||
MachineCode = _form.MachineCode,
|
MachineCode = _form.MachineCode,
|
||||||
@@ -242,7 +241,7 @@ else
|
|||||||
var entity = await db.Equipment.FirstOrDefaultAsync(e => e.EquipmentId == EquipmentId);
|
var entity = await db.Equipment.FirstOrDefaultAsync(e => e.EquipmentId == EquipmentId);
|
||||||
if (entity is null) { _error = "Row no longer exists."; return; }
|
if (entity is null) { _error = "Row no longer exists."; return; }
|
||||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion;
|
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion;
|
||||||
entity.DriverInstanceId = _form.DriverInstanceId;
|
entity.DriverInstanceId = string.IsNullOrWhiteSpace(_form.DriverInstanceId) ? null : _form.DriverInstanceId;
|
||||||
entity.UnsLineId = _form.UnsLineId;
|
entity.UnsLineId = _form.UnsLineId;
|
||||||
entity.Name = _form.Name;
|
entity.Name = _form.Name;
|
||||||
entity.MachineCode = _form.MachineCode;
|
entity.MachineCode = _form.MachineCode;
|
||||||
@@ -292,7 +291,7 @@ else
|
|||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
[Required] public string MachineCode { get; set; } = "";
|
[Required] public string MachineCode { get; set; } = "";
|
||||||
[Required] public string UnsLineId { get; set; } = "";
|
[Required] public string UnsLineId { get; set; } = "";
|
||||||
[Required] public string DriverInstanceId { get; set; } = "";
|
public string? DriverInstanceId { get; set; }
|
||||||
public string? ZTag { get; set; }
|
public string? ZTag { get; set; }
|
||||||
public string? SAPID { get; set; }
|
public string? SAPID { get; set; }
|
||||||
public string? Manufacturer { get; set; }
|
public string? Manufacturer { get; set; }
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ else
|
|||||||
d => nsById.TryGetValue(d.NamespaceId, out var ns) ? ns : namespaces.First());
|
d => nsById.TryGetValue(d.NamespaceId, out var ns) ? ns : namespaces.First());
|
||||||
var driverIds = _drivers.Select(d => d.DriverInstanceId).ToHashSet();
|
var driverIds = _drivers.Select(d => d.DriverInstanceId).ToHashSet();
|
||||||
_equipment = await db.Equipment.AsNoTracking()
|
_equipment = await db.Equipment.AsNoTracking()
|
||||||
.Where(e => driverIds.Contains(e.DriverInstanceId))
|
.Where(e => e.DriverInstanceId != null && driverIds.Contains(e.DriverInstanceId))
|
||||||
.OrderBy(e => e.MachineCode)
|
.OrderBy(e => e.MachineCode)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user