fix(transport): export instance AreaName — Area-by-name reconciliation was half-shipped (exporter hardcoded null)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
|
||||
@@ -72,6 +73,24 @@ public sealed class BundleExporter : IBundleExporter
|
||||
// folder chain, transitive shared-scripts/external-systems if requested).
|
||||
var resolved = await _resolver.ResolveAsync(selection, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 1b. Areas travel by NAME (the importer resolves-or-creates them under the
|
||||
// target site), so we only need an id→name lookup for the AreaIds the
|
||||
// exported instances actually reference — a single query over Areas keyed
|
||||
// to those FKs. Without this the serializer would emit AreaName: null and
|
||||
// the imported instance would lose its area assignment.
|
||||
var referencedAreaIds = resolved.Instances
|
||||
.Select(i => i.AreaId)
|
||||
.Where(id => id is not null)
|
||||
.Select(id => id!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var areaNameById = referencedAreaIds.Count == 0
|
||||
? new Dictionary<int, string>()
|
||||
: await _dbContext.Areas
|
||||
.Where(a => referencedAreaIds.Contains(a.Id))
|
||||
.ToDictionaryAsync(a => a.Id, a => a.Name, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// 2. Convert to the wire-shaped DTO (strips EF identity, refs-by-name).
|
||||
// The resolver's site/dataConnection/instance closure is wired
|
||||
// through via object-initializer — without this the aggregate would
|
||||
@@ -97,6 +116,9 @@ public sealed class BundleExporter : IBundleExporter
|
||||
// bundle ships an empty smsConfigs array even when SMS configs were
|
||||
// selected — mirrors the site/instance fix (review item I3).
|
||||
SmsConfigurations = resolved.SmsConfigs,
|
||||
// Area id→name lookup for the exported instances — feeds the serializer's
|
||||
// AreaName projection so each instance's area travels by name.
|
||||
AreaNameById = areaNameById,
|
||||
};
|
||||
var contentDto = _entitySerializer.ToBundleContent(aggregate);
|
||||
|
||||
|
||||
@@ -41,6 +41,15 @@ public sealed record EntityAggregate(
|
||||
// and never sees null; producers that resolve SMS config opt in via
|
||||
// object-initializer.
|
||||
public IReadOnlyList<SmsConfiguration> SmsConfigurations { get; init; } = Array.Empty<SmsConfiguration>();
|
||||
|
||||
// Area id → name lookup for the exported instances. Areas are not carried as a
|
||||
// first-class bundle collection (they reconcile by NAME under the target site),
|
||||
// so the aggregate only needs enough to resolve each instance's AreaId FK back to
|
||||
// a portable name at serialization time. Init-only with an empty default: producers
|
||||
// that export instances populate it; every other caller keeps compiling and the
|
||||
// serializer emits AreaName: null when an instance's AreaId isn't in the map.
|
||||
public IReadOnlyDictionary<int, string> AreaNameById { get; init; } =
|
||||
new Dictionary<int, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -248,10 +248,13 @@ public sealed class EntitySerializer
|
||||
UniqueName: inst.UniqueName,
|
||||
TemplateName: templateNameById.TryGetValue(inst.TemplateId, out var tn) ? tn : string.Empty,
|
||||
SiteIdentifier: siteIdentifierBySiteId.TryGetValue(inst.SiteId, out var isid) ? isid : string.Empty,
|
||||
// TODO: Areas don't travel in the bundle yet — EntityAggregate
|
||||
// carries no Areas collection, so there's no id→name lookup. Emit
|
||||
// null until area transport is added; the importer leaves AreaId null.
|
||||
AreaName: null,
|
||||
// Area travels by NAME — the importer resolves-or-creates the
|
||||
// equivalent area under the target site (ResolveOrCreateAreaIdAsync).
|
||||
// Resolve the instance's AreaId FK against the aggregate's id→name
|
||||
// map; if the FK is null (no area) or unresolved (orphan/absent from
|
||||
// the map), the name comes through as null and the importer leaves
|
||||
// AreaId null on the imported instance.
|
||||
AreaName: inst.AreaId is { } aid && aggregate.AreaNameById.TryGetValue(aid, out var an) ? an : null,
|
||||
State: inst.State,
|
||||
AttributeOverrides: inst.AttributeOverrides.Select(o => new InstanceAttributeOverrideDto(
|
||||
AttributeName: o.AttributeName,
|
||||
|
||||
Reference in New Issue
Block a user