fix(transport): transport LockedInDerived on template attributes/alarms/scripts (additive DTO fields)
Adds a trailing optional `bool LockedInDerived = false` to TemplateAttributeDto, TemplateAlarmDto and TemplateScriptDto (mirrors the ExecutionTimeoutSeconds additive precedent — no schemaVersion bump). Populated at export (ToBundleContent), consumed on import (FromBundleContent, BuildTemplate, and all three SyncTemplate*Async changed-predicate + copy + new-entity paths, incl. audit payloads). Old-form bundles that lack the property still deserialize to false. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -1549,6 +1549,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
Description = a.Description,
|
Description = a.Description,
|
||||||
DataSourceReference = a.DataSourceReference,
|
DataSourceReference = a.DataSourceReference,
|
||||||
ElementDataType = a.ElementDataType,
|
ElementDataType = a.ElementDataType,
|
||||||
|
LockedInDerived = a.LockedInDerived,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
foreach (var al in dto.Alarms)
|
foreach (var al in dto.Alarms)
|
||||||
@@ -1560,6 +1561,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
TriggerType = al.TriggerType,
|
TriggerType = al.TriggerType,
|
||||||
TriggerConfiguration = al.TriggerConfiguration,
|
TriggerConfiguration = al.TriggerConfiguration,
|
||||||
IsLocked = al.IsLocked,
|
IsLocked = al.IsLocked,
|
||||||
|
LockedInDerived = al.LockedInDerived,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
foreach (var s in dto.Scripts)
|
foreach (var s in dto.Scripts)
|
||||||
@@ -1573,6 +1575,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
IsLocked = s.IsLocked,
|
IsLocked = s.IsLocked,
|
||||||
MinTimeBetweenRuns = s.MinTimeBetweenRuns,
|
MinTimeBetweenRuns = s.MinTimeBetweenRuns,
|
||||||
ExecutionTimeoutSeconds = s.ExecutionTimeoutSeconds,
|
ExecutionTimeoutSeconds = s.ExecutionTimeoutSeconds,
|
||||||
|
LockedInDerived = s.LockedInDerived,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
@@ -1635,7 +1638,8 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
current.IsLocked != attrDto.IsLocked ||
|
current.IsLocked != attrDto.IsLocked ||
|
||||||
!string.Equals(current.Description, attrDto.Description, StringComparison.Ordinal) ||
|
!string.Equals(current.Description, attrDto.Description, StringComparison.Ordinal) ||
|
||||||
!string.Equals(current.DataSourceReference, attrDto.DataSourceReference, StringComparison.Ordinal) ||
|
!string.Equals(current.DataSourceReference, attrDto.DataSourceReference, StringComparison.Ordinal) ||
|
||||||
current.ElementDataType != attrDto.ElementDataType;
|
current.ElementDataType != attrDto.ElementDataType ||
|
||||||
|
current.LockedInDerived != attrDto.LockedInDerived;
|
||||||
if (!changed) continue;
|
if (!changed) continue;
|
||||||
|
|
||||||
current.Value = normalizedValue;
|
current.Value = normalizedValue;
|
||||||
@@ -1644,6 +1648,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
current.Description = attrDto.Description;
|
current.Description = attrDto.Description;
|
||||||
current.DataSourceReference = attrDto.DataSourceReference;
|
current.DataSourceReference = attrDto.DataSourceReference;
|
||||||
current.ElementDataType = attrDto.ElementDataType;
|
current.ElementDataType = attrDto.ElementDataType;
|
||||||
|
current.LockedInDerived = attrDto.LockedInDerived;
|
||||||
await _templateRepo.UpdateTemplateAttributeAsync(current, ct).ConfigureAwait(false);
|
await _templateRepo.UpdateTemplateAttributeAsync(current, ct).ConfigureAwait(false);
|
||||||
await _auditService.LogAsync(
|
await _auditService.LogAsync(
|
||||||
user,
|
user,
|
||||||
@@ -1659,6 +1664,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
current.DataType,
|
current.DataType,
|
||||||
current.ElementDataType,
|
current.ElementDataType,
|
||||||
current.IsLocked,
|
current.IsLocked,
|
||||||
|
current.LockedInDerived,
|
||||||
current.Description,
|
current.Description,
|
||||||
current.DataSourceReference,
|
current.DataSourceReference,
|
||||||
},
|
},
|
||||||
@@ -1674,6 +1680,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
Description = attrDto.Description,
|
Description = attrDto.Description,
|
||||||
DataSourceReference = attrDto.DataSourceReference,
|
DataSourceReference = attrDto.DataSourceReference,
|
||||||
ElementDataType = attrDto.ElementDataType,
|
ElementDataType = attrDto.ElementDataType,
|
||||||
|
LockedInDerived = attrDto.LockedInDerived,
|
||||||
};
|
};
|
||||||
ex.Attributes.Add(newAttr);
|
ex.Attributes.Add(newAttr);
|
||||||
await _auditService.LogAsync(
|
await _auditService.LogAsync(
|
||||||
@@ -1690,6 +1697,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
newAttr.DataType,
|
newAttr.DataType,
|
||||||
newAttr.ElementDataType,
|
newAttr.ElementDataType,
|
||||||
newAttr.IsLocked,
|
newAttr.IsLocked,
|
||||||
|
newAttr.LockedInDerived,
|
||||||
newAttr.Description,
|
newAttr.Description,
|
||||||
newAttr.DataSourceReference,
|
newAttr.DataSourceReference,
|
||||||
},
|
},
|
||||||
@@ -1740,7 +1748,8 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
current.PriorityLevel != alarmDto.PriorityLevel ||
|
current.PriorityLevel != alarmDto.PriorityLevel ||
|
||||||
current.TriggerType != alarmDto.TriggerType ||
|
current.TriggerType != alarmDto.TriggerType ||
|
||||||
!string.Equals(current.TriggerConfiguration, alarmDto.TriggerConfiguration, StringComparison.Ordinal) ||
|
!string.Equals(current.TriggerConfiguration, alarmDto.TriggerConfiguration, StringComparison.Ordinal) ||
|
||||||
current.IsLocked != alarmDto.IsLocked;
|
current.IsLocked != alarmDto.IsLocked ||
|
||||||
|
current.LockedInDerived != alarmDto.LockedInDerived;
|
||||||
if (!changed)
|
if (!changed)
|
||||||
{
|
{
|
||||||
// Always reset the script FK on Overwrite so the post-flush
|
// Always reset the script FK on Overwrite so the post-flush
|
||||||
@@ -1762,6 +1771,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
current.TriggerType = alarmDto.TriggerType;
|
current.TriggerType = alarmDto.TriggerType;
|
||||||
current.TriggerConfiguration = alarmDto.TriggerConfiguration;
|
current.TriggerConfiguration = alarmDto.TriggerConfiguration;
|
||||||
current.IsLocked = alarmDto.IsLocked;
|
current.IsLocked = alarmDto.IsLocked;
|
||||||
|
current.LockedInDerived = alarmDto.LockedInDerived;
|
||||||
current.OnTriggerScriptId = null; // re-resolved post-flush.
|
current.OnTriggerScriptId = null; // re-resolved post-flush.
|
||||||
await _templateRepo.UpdateTemplateAlarmAsync(current, ct).ConfigureAwait(false);
|
await _templateRepo.UpdateTemplateAlarmAsync(current, ct).ConfigureAwait(false);
|
||||||
await _auditService.LogAsync(
|
await _auditService.LogAsync(
|
||||||
@@ -1779,6 +1789,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
current.TriggerType,
|
current.TriggerType,
|
||||||
current.TriggerConfiguration,
|
current.TriggerConfiguration,
|
||||||
current.IsLocked,
|
current.IsLocked,
|
||||||
|
current.LockedInDerived,
|
||||||
OnTriggerScriptName = alarmDto.OnTriggerScriptName,
|
OnTriggerScriptName = alarmDto.OnTriggerScriptName,
|
||||||
},
|
},
|
||||||
ct).ConfigureAwait(false);
|
ct).ConfigureAwait(false);
|
||||||
@@ -1792,6 +1803,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
TriggerType = alarmDto.TriggerType,
|
TriggerType = alarmDto.TriggerType,
|
||||||
TriggerConfiguration = alarmDto.TriggerConfiguration,
|
TriggerConfiguration = alarmDto.TriggerConfiguration,
|
||||||
IsLocked = alarmDto.IsLocked,
|
IsLocked = alarmDto.IsLocked,
|
||||||
|
LockedInDerived = alarmDto.LockedInDerived,
|
||||||
};
|
};
|
||||||
ex.Alarms.Add(newAlarm);
|
ex.Alarms.Add(newAlarm);
|
||||||
await _auditService.LogAsync(
|
await _auditService.LogAsync(
|
||||||
@@ -1809,6 +1821,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
newAlarm.TriggerType,
|
newAlarm.TriggerType,
|
||||||
newAlarm.TriggerConfiguration,
|
newAlarm.TriggerConfiguration,
|
||||||
newAlarm.IsLocked,
|
newAlarm.IsLocked,
|
||||||
|
newAlarm.LockedInDerived,
|
||||||
OnTriggerScriptName = alarmDto.OnTriggerScriptName,
|
OnTriggerScriptName = alarmDto.OnTriggerScriptName,
|
||||||
},
|
},
|
||||||
ct).ConfigureAwait(false);
|
ct).ConfigureAwait(false);
|
||||||
@@ -1858,7 +1871,8 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
!string.Equals(current.ReturnDefinition, scriptDto.ReturnDefinition, StringComparison.Ordinal) ||
|
!string.Equals(current.ReturnDefinition, scriptDto.ReturnDefinition, StringComparison.Ordinal) ||
|
||||||
current.IsLocked != scriptDto.IsLocked ||
|
current.IsLocked != scriptDto.IsLocked ||
|
||||||
current.MinTimeBetweenRuns != scriptDto.MinTimeBetweenRuns ||
|
current.MinTimeBetweenRuns != scriptDto.MinTimeBetweenRuns ||
|
||||||
current.ExecutionTimeoutSeconds != scriptDto.ExecutionTimeoutSeconds;
|
current.ExecutionTimeoutSeconds != scriptDto.ExecutionTimeoutSeconds ||
|
||||||
|
current.LockedInDerived != scriptDto.LockedInDerived;
|
||||||
if (!changed) continue;
|
if (!changed) continue;
|
||||||
|
|
||||||
current.Code = scriptDto.Code;
|
current.Code = scriptDto.Code;
|
||||||
@@ -1869,6 +1883,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
current.IsLocked = scriptDto.IsLocked;
|
current.IsLocked = scriptDto.IsLocked;
|
||||||
current.MinTimeBetweenRuns = scriptDto.MinTimeBetweenRuns;
|
current.MinTimeBetweenRuns = scriptDto.MinTimeBetweenRuns;
|
||||||
current.ExecutionTimeoutSeconds = scriptDto.ExecutionTimeoutSeconds;
|
current.ExecutionTimeoutSeconds = scriptDto.ExecutionTimeoutSeconds;
|
||||||
|
current.LockedInDerived = scriptDto.LockedInDerived;
|
||||||
await _templateRepo.UpdateTemplateScriptAsync(current, ct).ConfigureAwait(false);
|
await _templateRepo.UpdateTemplateScriptAsync(current, ct).ConfigureAwait(false);
|
||||||
await _auditService.LogAsync(
|
await _auditService.LogAsync(
|
||||||
user,
|
user,
|
||||||
@@ -1883,6 +1898,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
current.TriggerType,
|
current.TriggerType,
|
||||||
current.TriggerConfiguration,
|
current.TriggerConfiguration,
|
||||||
current.IsLocked,
|
current.IsLocked,
|
||||||
|
current.LockedInDerived,
|
||||||
},
|
},
|
||||||
ct).ConfigureAwait(false);
|
ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -1897,6 +1913,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
IsLocked = scriptDto.IsLocked,
|
IsLocked = scriptDto.IsLocked,
|
||||||
MinTimeBetweenRuns = scriptDto.MinTimeBetweenRuns,
|
MinTimeBetweenRuns = scriptDto.MinTimeBetweenRuns,
|
||||||
ExecutionTimeoutSeconds = scriptDto.ExecutionTimeoutSeconds,
|
ExecutionTimeoutSeconds = scriptDto.ExecutionTimeoutSeconds,
|
||||||
|
LockedInDerived = scriptDto.LockedInDerived,
|
||||||
};
|
};
|
||||||
ex.Scripts.Add(newScript);
|
ex.Scripts.Add(newScript);
|
||||||
await _auditService.LogAsync(
|
await _auditService.LogAsync(
|
||||||
@@ -1912,6 +1929,7 @@ public sealed class BundleImporter : IBundleImporter
|
|||||||
newScript.TriggerType,
|
newScript.TriggerType,
|
||||||
newScript.TriggerConfiguration,
|
newScript.TriggerConfiguration,
|
||||||
newScript.IsLocked,
|
newScript.IsLocked,
|
||||||
|
newScript.LockedInDerived,
|
||||||
},
|
},
|
||||||
ct).ConfigureAwait(false);
|
ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,13 @@ public sealed record TemplateAttributeDto(
|
|||||||
bool IsLocked,
|
bool IsLocked,
|
||||||
string? Description,
|
string? Description,
|
||||||
string? DataSourceReference,
|
string? DataSourceReference,
|
||||||
DataType? ElementDataType = null);
|
DataType? ElementDataType = null,
|
||||||
|
// Whether this member is locked against override in derived templates.
|
||||||
|
// Additive trailing field; false on bundles written before it existed
|
||||||
|
// (dropped from JSON by WhenWritingNull? — no, bool is a value type, so it
|
||||||
|
// is always emitted; older bundles simply lack the property and deserialize
|
||||||
|
// to the default false).
|
||||||
|
bool LockedInDerived = false);
|
||||||
|
|
||||||
public sealed record TemplateAlarmDto(
|
public sealed record TemplateAlarmDto(
|
||||||
string Name,
|
string Name,
|
||||||
@@ -144,7 +150,9 @@ public sealed record TemplateAlarmDto(
|
|||||||
AlarmTriggerType TriggerType,
|
AlarmTriggerType TriggerType,
|
||||||
string? TriggerConfiguration,
|
string? TriggerConfiguration,
|
||||||
bool IsLocked,
|
bool IsLocked,
|
||||||
string? OnTriggerScriptName);
|
string? OnTriggerScriptName,
|
||||||
|
// Derived-lock flag; additive trailing field (see TemplateAttributeDto).
|
||||||
|
bool LockedInDerived = false);
|
||||||
|
|
||||||
public sealed record TemplateScriptDto(
|
public sealed record TemplateScriptDto(
|
||||||
string Name,
|
string Name,
|
||||||
@@ -157,7 +165,9 @@ public sealed record TemplateScriptDto(
|
|||||||
TimeSpan? MinTimeBetweenRuns,
|
TimeSpan? MinTimeBetweenRuns,
|
||||||
// Per-script execution timeout (seconds). Additive trailing field;
|
// Per-script execution timeout (seconds). Additive trailing field;
|
||||||
// null on bundles written before this field existed.
|
// null on bundles written before this field existed.
|
||||||
int? ExecutionTimeoutSeconds = null);
|
int? ExecutionTimeoutSeconds = null,
|
||||||
|
// Derived-lock flag; additive trailing field (see TemplateAttributeDto).
|
||||||
|
bool LockedInDerived = false);
|
||||||
|
|
||||||
public sealed record TemplateCompositionDto(
|
public sealed record TemplateCompositionDto(
|
||||||
string InstanceName,
|
string InstanceName,
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ public sealed class EntitySerializer
|
|||||||
IsLocked: a.IsLocked,
|
IsLocked: a.IsLocked,
|
||||||
Description: a.Description,
|
Description: a.Description,
|
||||||
DataSourceReference: a.DataSourceReference,
|
DataSourceReference: a.DataSourceReference,
|
||||||
ElementDataType: a.ElementDataType)).ToList(),
|
ElementDataType: a.ElementDataType,
|
||||||
|
LockedInDerived: a.LockedInDerived)).ToList(),
|
||||||
Alarms: t.Alarms.Select(a => new TemplateAlarmDto(
|
Alarms: t.Alarms.Select(a => new TemplateAlarmDto(
|
||||||
Name: a.Name,
|
Name: a.Name,
|
||||||
Description: a.Description,
|
Description: a.Description,
|
||||||
@@ -75,7 +76,8 @@ public sealed class EntitySerializer
|
|||||||
// resolve in this aggregate (e.g. corrupt/orphan row), the
|
// resolve in this aggregate (e.g. corrupt/orphan row), the
|
||||||
// name comes through as null and the importer leaves the
|
// name comes through as null and the importer leaves the
|
||||||
// FK null on the imported alarm.
|
// FK null on the imported alarm.
|
||||||
OnTriggerScriptName: a.OnTriggerScriptId is { } sid && scriptNameById.TryGetValue(sid, out var sn) ? sn : null)).ToList(),
|
OnTriggerScriptName: a.OnTriggerScriptId is { } sid && scriptNameById.TryGetValue(sid, out var sn) ? sn : null,
|
||||||
|
LockedInDerived: a.LockedInDerived)).ToList(),
|
||||||
Scripts: t.Scripts.Select(s => new TemplateScriptDto(
|
Scripts: t.Scripts.Select(s => new TemplateScriptDto(
|
||||||
Name: s.Name,
|
Name: s.Name,
|
||||||
Code: s.Code,
|
Code: s.Code,
|
||||||
@@ -85,7 +87,8 @@ public sealed class EntitySerializer
|
|||||||
ReturnDefinition: s.ReturnDefinition,
|
ReturnDefinition: s.ReturnDefinition,
|
||||||
IsLocked: s.IsLocked,
|
IsLocked: s.IsLocked,
|
||||||
MinTimeBetweenRuns: s.MinTimeBetweenRuns,
|
MinTimeBetweenRuns: s.MinTimeBetweenRuns,
|
||||||
ExecutionTimeoutSeconds: s.ExecutionTimeoutSeconds)).ToList(),
|
ExecutionTimeoutSeconds: s.ExecutionTimeoutSeconds,
|
||||||
|
LockedInDerived: s.LockedInDerived)).ToList(),
|
||||||
Compositions: t.Compositions.Select(c => new TemplateCompositionDto(
|
Compositions: t.Compositions.Select(c => new TemplateCompositionDto(
|
||||||
InstanceName: c.InstanceName,
|
InstanceName: c.InstanceName,
|
||||||
ComposedTemplateName: templateNameById.TryGetValue(c.ComposedTemplateId, out var cn) ? cn : string.Empty)).ToList());
|
ComposedTemplateName: templateNameById.TryGetValue(c.ComposedTemplateId, out var cn) ? cn : string.Empty)).ToList());
|
||||||
@@ -318,6 +321,7 @@ public sealed class EntitySerializer
|
|||||||
Description = a.Description,
|
Description = a.Description,
|
||||||
DataSourceReference = a.DataSourceReference,
|
DataSourceReference = a.DataSourceReference,
|
||||||
ElementDataType = a.ElementDataType,
|
ElementDataType = a.ElementDataType,
|
||||||
|
LockedInDerived = a.LockedInDerived,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
foreach (var al in dto.Alarms)
|
foreach (var al in dto.Alarms)
|
||||||
@@ -330,6 +334,7 @@ public sealed class EntitySerializer
|
|||||||
TriggerType = al.TriggerType,
|
TriggerType = al.TriggerType,
|
||||||
TriggerConfiguration = al.TriggerConfiguration,
|
TriggerConfiguration = al.TriggerConfiguration,
|
||||||
IsLocked = al.IsLocked,
|
IsLocked = al.IsLocked,
|
||||||
|
LockedInDerived = al.LockedInDerived,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
foreach (var s in dto.Scripts)
|
foreach (var s in dto.Scripts)
|
||||||
@@ -344,6 +349,7 @@ public sealed class EntitySerializer
|
|||||||
IsLocked = s.IsLocked,
|
IsLocked = s.IsLocked,
|
||||||
MinTimeBetweenRuns = s.MinTimeBetweenRuns,
|
MinTimeBetweenRuns = s.MinTimeBetweenRuns,
|
||||||
ExecutionTimeoutSeconds = s.ExecutionTimeoutSeconds,
|
ExecutionTimeoutSeconds = s.ExecutionTimeoutSeconds,
|
||||||
|
LockedInDerived = s.LockedInDerived,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
|
|||||||
+57
@@ -197,4 +197,61 @@ public sealed class TemplateScriptFidelityTests : IDisposable
|
|||||||
a.Action == "TemplateScriptUpdated" && a.EntityName == "Pump.cadence"));
|
a.Action == "TemplateScriptUpdated" && a.EntityName == "Pump.cadence"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Add_preserves_LockedInDerived_on_attribute_alarm_script()
|
||||||
|
{
|
||||||
|
// #05-T4: LockedInDerived must survive export→import on all three child kinds.
|
||||||
|
await using (var scope = _provider.CreateAsyncScope())
|
||||||
|
{
|
||||||
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||||
|
var t = new Template("Pump") { Description = "fresh" };
|
||||||
|
t.Attributes.Add(new TemplateAttribute("Pressure")
|
||||||
|
{
|
||||||
|
DataType = DataType.Double,
|
||||||
|
LockedInDerived = true,
|
||||||
|
});
|
||||||
|
t.Alarms.Add(new TemplateAlarm("High")
|
||||||
|
{
|
||||||
|
TriggerType = AlarmTriggerType.RangeViolation,
|
||||||
|
LockedInDerived = true,
|
||||||
|
});
|
||||||
|
t.Scripts.Add(new TemplateScript("cadence", "return 1;")
|
||||||
|
{
|
||||||
|
LockedInDerived = true,
|
||||||
|
});
|
||||||
|
ctx.Templates.Add(t);
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
var sessionId = await ExportAndLoadAsync();
|
||||||
|
|
||||||
|
await using (var scope = _provider.CreateAsyncScope())
|
||||||
|
{
|
||||||
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||||
|
ctx.Templates.RemoveRange(ctx.Templates);
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await using (var scope = _provider.CreateAsyncScope())
|
||||||
|
{
|
||||||
|
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||||
|
await importer.ApplyAsync(sessionId,
|
||||||
|
new List<ImportResolution> { new("Template", "Pump", ResolutionAction.Add, null) },
|
||||||
|
user: "bob");
|
||||||
|
}
|
||||||
|
|
||||||
|
await using (var scope = _provider.CreateAsyncScope())
|
||||||
|
{
|
||||||
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||||
|
var t = await ctx.Templates
|
||||||
|
.Where(t => t.Name == "Pump")
|
||||||
|
.Include(t => t.Attributes)
|
||||||
|
.Include(t => t.Alarms)
|
||||||
|
.Include(t => t.Scripts)
|
||||||
|
.SingleAsync();
|
||||||
|
Assert.True(t.Attributes.Single(a => a.Name == "Pressure").LockedInDerived);
|
||||||
|
Assert.True(t.Alarms.Single(a => a.Name == "High").LockedInDerived);
|
||||||
|
Assert.True(t.Scripts.Single(s => s.Name == "cadence").LockedInDerived);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
|||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||||
|
using System.Text.Json;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||||
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
||||||
|
|
||||||
@@ -158,6 +159,99 @@ public sealed class EntitySerializerTests
|
|||||||
Assert.Equal("MotorA", rtComp.InstanceName);
|
Assert.Equal("MotorA", rtComp.InstanceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Roundtrip_template_preserves_LockedInDerived_on_attribute_alarm_script()
|
||||||
|
{
|
||||||
|
var basic = new Template("Basic") { Id = 1 };
|
||||||
|
basic.Attributes.Add(new TemplateAttribute("Pressure")
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
TemplateId = 1,
|
||||||
|
DataType = DataType.Double,
|
||||||
|
LockedInDerived = true,
|
||||||
|
});
|
||||||
|
basic.Alarms.Add(new TemplateAlarm("High")
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
TemplateId = 1,
|
||||||
|
TriggerType = AlarmTriggerType.RangeViolation,
|
||||||
|
LockedInDerived = true,
|
||||||
|
});
|
||||||
|
basic.Scripts.Add(new TemplateScript("OnUpdate", "return 1;")
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
TemplateId = 1,
|
||||||
|
LockedInDerived = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
var aggregate = MakeEmptyAggregate() with { Templates = new[] { basic } };
|
||||||
|
|
||||||
|
var sut = new EntitySerializer();
|
||||||
|
var dto = sut.ToBundleContent(aggregate);
|
||||||
|
|
||||||
|
// Export carries the flag on all three child DTOs.
|
||||||
|
var dtoBasic = Assert.Single(dto.Templates);
|
||||||
|
Assert.True(Assert.Single(dtoBasic.Attributes).LockedInDerived);
|
||||||
|
Assert.True(Assert.Single(dtoBasic.Alarms).LockedInDerived);
|
||||||
|
Assert.True(Assert.Single(dtoBasic.Scripts).LockedInDerived);
|
||||||
|
|
||||||
|
// FromBundleContent restores it onto the entities.
|
||||||
|
var rtBasic = Assert.Single(sut.FromBundleContent(dto).Templates);
|
||||||
|
Assert.True(Assert.Single(rtBasic.Attributes).LockedInDerived);
|
||||||
|
Assert.True(Assert.Single(rtBasic.Alarms).LockedInDerived);
|
||||||
|
Assert.True(Assert.Single(rtBasic.Scripts).LockedInDerived);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OldForm_bundle_json_without_LockedInDerived_deserializes_to_false()
|
||||||
|
{
|
||||||
|
// A bundle written before LockedInDerived existed: the property is absent
|
||||||
|
// from every template-child object. It must deserialize to false, not throw.
|
||||||
|
// PascalCase keys + string enum names — the on-wire form BundleJsonOptions
|
||||||
|
// produces (no naming policy, JsonStringEnumConverter, case-sensitive). All
|
||||||
|
// required collections present; LockedInDerived absent on every child.
|
||||||
|
const string json = """
|
||||||
|
{
|
||||||
|
"TemplateFolders": [],
|
||||||
|
"Templates": [
|
||||||
|
{
|
||||||
|
"Name": "Legacy",
|
||||||
|
"FolderName": null,
|
||||||
|
"BaseTemplateName": null,
|
||||||
|
"Description": null,
|
||||||
|
"Attributes": [
|
||||||
|
{ "Name": "A", "Value": "1", "DataType": "Double", "IsLocked": false,
|
||||||
|
"Description": null, "DataSourceReference": null }
|
||||||
|
],
|
||||||
|
"Alarms": [
|
||||||
|
{ "Name": "Alm", "Description": null, "PriorityLevel": 1,
|
||||||
|
"TriggerType": "RangeViolation", "TriggerConfiguration": null, "IsLocked": false,
|
||||||
|
"OnTriggerScriptName": null }
|
||||||
|
],
|
||||||
|
"Scripts": [
|
||||||
|
{ "Name": "S", "Code": "return 1;", "TriggerType": null,
|
||||||
|
"TriggerConfiguration": null, "ParameterDefinitions": null,
|
||||||
|
"ReturnDefinition": null, "IsLocked": false }
|
||||||
|
],
|
||||||
|
"Compositions": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SharedScripts": [],
|
||||||
|
"ExternalSystems": [],
|
||||||
|
"DatabaseConnections": [],
|
||||||
|
"NotificationLists": [],
|
||||||
|
"SmtpConfigs": [],
|
||||||
|
"ApiMethods": []
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var content = JsonSerializer.Deserialize<BundleContentDto>(json, BundleJsonOptions.Default)!;
|
||||||
|
var template = Assert.Single(content.Templates);
|
||||||
|
Assert.False(Assert.Single(template.Attributes).LockedInDerived);
|
||||||
|
Assert.False(Assert.Single(template.Alarms).LockedInDerived);
|
||||||
|
Assert.False(Assert.Single(template.Scripts).LockedInDerived);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Roundtrip_template_folder_preserves_hierarchy()
|
public void Roundtrip_template_folder_preserves_hierarchy()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user