diff --git a/clients/dotnet/README.md b/clients/dotnet/README.md index 1625e0c..d6eca9b 100644 --- a/clients/dotnet/README.md +++ b/clients/dotnet/README.md @@ -168,6 +168,30 @@ the unchanged elements included. For example, to change 2 elements of a the 2 new ones). Sending only the 2 changed values overwrites the attribute with a 2-element array. +When only a few indices need changing and the rest should be reset to the +element type's default, use `WriteArrayElementsAsync` instead of building the +full array manually: + +```csharp +await session.WriteArrayElementsAsync( + serverHandle, itemHandle, + elementDataType: MxDataType.Integer, + totalLength: 20, + elements: new Dictionary + { + [2] = 42.ToMxValue(), + [7] = 99.ToMxValue(), + }); +``` + +The gateway expands the sparse descriptor into a full `totalLength`-element +array before forwarding to the worker. Indices not listed in `elements` are +written as the element type's default — this is a **reset**, not a preserve; +current values at those positions are discarded. `totalLength` is required and +must match the declared length of the array attribute. Bare-name array items +(`Area001.Pump001.Speed`) are auto-normalized to the `[]` form at `AddItem` +so the array attribute accepts the write. + ## CLI Usage The test CLI supports deterministic JSON output for automation: diff --git a/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/MxGatewayClientSessionTests.cs b/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/MxGatewayClientSessionTests.cs index 1052a8d..d827be1 100644 --- a/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/MxGatewayClientSessionTests.cs +++ b/clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/MxGatewayClientSessionTests.cs @@ -303,6 +303,69 @@ public sealed class MxGatewayClientSessionTests Assert.Equal(cancellation.Token, Assert.Single(transport.InvokeCalls).CallOptions.CancellationToken); } + /// Verifies that BuildSparseArray produces a SparseArrayValue MxValue with the correct total length and elements. + [Fact] + public void BuildSparseArray_ProducesSparseArrayValueWithCorrectTotalLengthAndElements() + { + MxValue element0 = 42.ToMxValue(); + MxValue element3 = 99.ToMxValue(); + Dictionary elements = new() + { + [0u] = element0, + [3u] = element3, + }; + + MxValue result = MxGatewaySession.BuildSparseArray(MxDataType.Integer, totalLength: 10, elements); + + Assert.Equal(MxValue.KindOneofCase.SparseArrayValue, result.KindCase); + Assert.Equal(10u, result.SparseArrayValue.TotalLength); + Assert.Equal(MxDataType.Integer, result.SparseArrayValue.ElementDataType); + Assert.Equal(2, result.SparseArrayValue.Elements.Count); + + MxSparseElement el0 = Assert.Single(result.SparseArrayValue.Elements, e => e.Index == 0u); + Assert.Same(element0, el0.Value); + + MxSparseElement el3 = Assert.Single(result.SparseArrayValue.Elements, e => e.Index == 3u); + Assert.Same(element3, el3.Value); + } + + /// Verifies that WriteArrayElementsAsync builds a write command whose value is a SparseArrayValue. + [Fact] + public async Task WriteArrayElementsAsync_BuildsWriteCommandWithSparseArrayValue() + { + FakeGatewayTransport transport = CreateTransport(); + transport.AddInvokeReply(new MxCommandReply + { + SessionId = "session-fixture", + Kind = MxCommandKind.Write, + ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, + }); + await using MxGatewayClient client = CreateClient(transport); + MxGatewaySession session = await client.OpenSessionAsync(); + Dictionary elements = new() { [1u] = 7.ToMxValue() }; + + await session.WriteArrayElementsAsync( + serverHandle: 12, + itemHandle: 34, + elementDataType: MxDataType.Integer, + totalLength: 5, + elements: elements, + userId: 56); + + MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request; + Assert.Equal(MxCommandKind.Write, request.Command.Kind); + Assert.Equal(12, request.Command.Write.ServerHandle); + Assert.Equal(34, request.Command.Write.ItemHandle); + Assert.Equal(56, request.Command.Write.UserId); + MxValue written = request.Command.Write.Value; + Assert.Equal(MxValue.KindOneofCase.SparseArrayValue, written.KindCase); + Assert.Equal(5u, written.SparseArrayValue.TotalLength); + Assert.Equal(MxDataType.Integer, written.SparseArrayValue.ElementDataType); + MxSparseElement el = Assert.Single(written.SparseArrayValue.Elements); + Assert.Equal(1u, el.Index); + Assert.Equal(7, el.Value.Int32Value); + } + private static MxGatewayClient CreateClient(FakeGatewayTransport transport) { return new MxGatewayClient(transport.Options, transport); diff --git a/clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewaySession.cs b/clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewaySession.cs index 158589c..e0c6f24 100644 --- a/clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewaySession.cs +++ b/clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewaySession.cs @@ -687,6 +687,63 @@ public sealed class MxGatewaySession : IAsyncDisposable reply.EnsureProtocolSuccess().EnsureMxAccessSuccess(); } + /// + /// Writes specific array indices to an item using default-fill semantics. + /// + /// + /// The gateway expands the sparse descriptor into a full totalLength-element array + /// before forwarding to the worker. Indices not listed in are + /// written as the element type's default value — this is a RESET, not a preserve. The + /// current values at those positions are discarded. is + /// required and must match the declared length of the array attribute. + /// + /// The ServerHandle from register. + /// The ItemHandle from add-item. + /// The MXAccess data type of each element. + /// The total declared length of the target array attribute. + /// Map of zero-based array index to scalar . + /// User ID context for the write. + /// Cancellation token. + public Task WriteArrayElementsAsync( + int serverHandle, + int itemHandle, + MxDataType elementDataType, + uint totalLength, + IReadOnlyDictionary elements, + int userId = 0, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(elements); + MxValue value = BuildSparseArray(elementDataType, totalLength, elements); + return WriteAsync(serverHandle, itemHandle, value, userId, cancellationToken); + } + + /// + /// Builds an whose describes a + /// default-fill partial array write. + /// + /// The MXAccess data type of each element. + /// The total declared length of the target array attribute. + /// Map of zero-based array index to scalar . + /// An with set. + internal static MxValue BuildSparseArray( + MxDataType elementDataType, + uint totalLength, + IReadOnlyDictionary elements) + { + MxSparseArray sparse = new() + { + ElementDataType = elementDataType, + TotalLength = totalLength, + }; + foreach (KeyValuePair kv in elements) + { + sparse.Elements.Add(new MxSparseElement { Index = kv.Key, Value = kv.Value }); + } + + return new MxValue { SparseArrayValue = sparse }; + } + /// /// Writes a value to an item on the MXAccess server without error checking. ///