fix(dcl): address MXAccess array attributes with trailing "[]" on write

MXAccess silently no-ops a whole-array write unless the item reference
ends in "[]" (e.g. "<object>.MoveInWorkOrderNumbers[]") — the COM Write
returns success but the value never commits. Reads work either way, so
the bug surfaced only on writes. Mirror the AVEVA MES Camstar API, which
registers array tags as "<object>.<attr>[]" (scalars have no brackets).

WriteAsync now resolves/advises/writes array values against tag + "[]"
(scalars unchanged), keeping the original tag for result mapping. Adds
IsArrayValue matching the ToMxValue/PadArrayToDeclaredSizeAsync array set.

Verified live via mxwrtest against the deployed gateway: bare ref write
ok but read-back unchanged; "[]" ref write commits (read-back changes,
fresh source timestamp). No RealMxGatewayClient unit harness exists (the
gRPC session is concrete) — consistent with how the sibling supervisory/
pad/encode fixes are verified.
This commit is contained in:
Joseph Doherty
2026-06-17 08:32:03 -04:00
parent 8cbecdec0e
commit eeb6210151
@@ -143,7 +143,13 @@ public sealed class RealMxGatewayClient : IMxGatewayClient
var orderedTags = new List<string>(writes.Count);
foreach (var (tag, value) in writes)
{
var handle = await GetOrAddItemHandleAsync(tag, ct).ConfigureAwait(false);
// MXAccess addresses a WHOLE array attribute with a trailing "[]" on the
// item reference. A write to the bare reference is silently dropped — the
// COM Write returns success but the value never commits (reads work
// either way). This mirrors the AVEVA MES Camstar API, which registers
// array tags as "<object>.<attr>[]". Scalars keep the bare reference.
var writeRef = IsArrayValue(value) ? tag + "[]" : tag;
var handle = await GetOrAddItemHandleAsync(writeRef, ct).ConfigureAwait(false);
// MXAccess requires a supervisory advise on the item before it will
// accept a write; without it the worker's synchronous COM Write blocks.
// With no write-user context we advise supervisory by default (a
@@ -155,13 +161,14 @@ public sealed class RealMxGatewayClient : IMxGatewayClient
// rejected and the COM write blocks. Pad list values out to the live
// array's slot count before encoding (callers signal the valid count
// via a separate scalar, e.g. MoveInNumberWorkOrders).
var toWrite = await PadArrayToDeclaredSizeAsync(tag, value, ct).ConfigureAwait(false);
var toWrite = await PadArrayToDeclaredSizeAsync(writeRef, value, ct).ConfigureAwait(false);
entries.Add(new WriteBulkEntry
{
ItemHandle = handle,
Value = ToMxValue(toWrite),
UserId = _writeUserId,
});
// Map the per-handle result back to the ORIGINAL caller tag (not writeRef).
orderedTags.Add(tag);
}
@@ -387,6 +394,26 @@ public sealed class RealMxGatewayClient : IMxGatewayClient
}
}
/// <summary>
/// Whether a write value is a multi-element (array) attribute value — i.e. one
/// <see cref="ToMxValue"/> encodes as an MXAccess array. Such attributes must be
/// addressed with a trailing "[]" on the item reference (see <c>WriteAsync</c>).
/// The set matches the array cases in <see cref="ToMxValue"/> /
/// <see cref="PadArrayToDeclaredSizeAsync"/>; scalars (string, int, bool, …) are false.
/// </summary>
private static bool IsArrayValue(object? value) => value switch
{
IReadOnlyList<bool> => true,
IReadOnlyList<int> => true,
IReadOnlyList<long> => true,
IReadOnlyList<float> => true,
IReadOnlyList<double> => true,
IReadOnlyList<string> => true,
IReadOnlyList<DateTimeOffset> => true,
IReadOnlyList<DateTime> => true,
_ => false,
};
private async Task<IReadOnlyList<T>> PadAsync<T>(string tag, IReadOnlyList<T> values, T pad, CancellationToken ct)
{
var size = await GetArraySlotCountAsync(tag, ct).ConfigureAwait(false);