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.
|
||||
- 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)
|
||||
- `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
|
||||
|
||||
@@ -20,9 +20,15 @@
|
||||
|
||||
**Files:**
|
||||
- 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`)
|
||||
- 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.
|
||||
|
||||
**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>
|
||||
public Guid EquipmentUuid { get; set; }
|
||||
|
||||
/// <summary>Logical FK to the driver providing data for this equipment.</summary>
|
||||
public required string DriverInstanceId { get; set; }
|
||||
/// <summary>
|
||||
/// 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>
|
||||
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)");
|
||||
|
||||
b.Property<string>("DriverInstanceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ else
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="driver">Driver instance</label>
|
||||
<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)
|
||||
{
|
||||
<option value="@d.DriverInstanceId">@d.DriverInstanceId — @d.Name (@d.DriverType)</option>
|
||||
@@ -206,7 +206,6 @@ else
|
||||
try
|
||||
{
|
||||
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();
|
||||
if (IsNew)
|
||||
@@ -219,7 +218,7 @@ else
|
||||
{
|
||||
EquipmentId = equipmentId,
|
||||
EquipmentUuid = uuid,
|
||||
DriverInstanceId = _form.DriverInstanceId,
|
||||
DriverInstanceId = string.IsNullOrWhiteSpace(_form.DriverInstanceId) ? null : _form.DriverInstanceId,
|
||||
UnsLineId = _form.UnsLineId,
|
||||
Name = _form.Name,
|
||||
MachineCode = _form.MachineCode,
|
||||
@@ -242,7 +241,7 @@ else
|
||||
var entity = await db.Equipment.FirstOrDefaultAsync(e => e.EquipmentId == EquipmentId);
|
||||
if (entity is null) { _error = "Row no longer exists."; return; }
|
||||
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.Name = _form.Name;
|
||||
entity.MachineCode = _form.MachineCode;
|
||||
@@ -292,7 +291,7 @@ else
|
||||
public string Name { get; set; } = "";
|
||||
[Required] public string MachineCode { 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? SAPID { get; set; }
|
||||
public string? Manufacturer { get; set; }
|
||||
|
||||
@@ -188,7 +188,7 @@ else
|
||||
d => nsById.TryGetValue(d.NamespaceId, out var ns) ? ns : namespaces.First());
|
||||
var driverIds = _drivers.Select(d => d.DriverInstanceId).ToHashSet();
|
||||
_equipment = await db.Equipment.AsNoTracking()
|
||||
.Where(e => driverIds.Contains(e.DriverInstanceId))
|
||||
.Where(e => e.DriverInstanceId != null && driverIds.Contains(e.DriverInstanceId))
|
||||
.OrderBy(e => e.MachineCode)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user