feat(client-dotnet): add WriteArrayElementsAsync default-fill helper and document semantics
Adds a public WriteArrayElementsAsync helper on MxGatewaySession that builds
an MxValue{SparseArrayValue} and delegates to the existing WriteAsync. Extracts
the MxValue construction into an internal static BuildSparseArray builder for
unit-testability. Two new tests cover builder output shape and the full write
command path. README documents the reset (not preserve) semantics alongside
the existing whole-array guidance.
This commit is contained in:
@@ -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
|
the 2 new ones). Sending only the 2 changed values overwrites the attribute
|
||||||
with a 2-element array.
|
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<uint, MxValue>
|
||||||
|
{
|
||||||
|
[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
|
## CLI Usage
|
||||||
|
|
||||||
The test CLI supports deterministic JSON output for automation:
|
The test CLI supports deterministic JSON output for automation:
|
||||||
|
|||||||
@@ -303,6 +303,69 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(cancellation.Token, Assert.Single(transport.InvokeCalls).CallOptions.CancellationToken);
|
Assert.Equal(cancellation.Token, Assert.Single(transport.InvokeCalls).CallOptions.CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that BuildSparseArray produces a SparseArrayValue MxValue with the correct total length and elements.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void BuildSparseArray_ProducesSparseArrayValueWithCorrectTotalLengthAndElements()
|
||||||
|
{
|
||||||
|
MxValue element0 = 42.ToMxValue();
|
||||||
|
MxValue element3 = 99.ToMxValue();
|
||||||
|
Dictionary<uint, MxValue> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that WriteArrayElementsAsync builds a write command whose value is a SparseArrayValue.</summary>
|
||||||
|
[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<uint, MxValue> 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)
|
private static MxGatewayClient CreateClient(FakeGatewayTransport transport)
|
||||||
{
|
{
|
||||||
return new MxGatewayClient(transport.Options, transport);
|
return new MxGatewayClient(transport.Options, transport);
|
||||||
|
|||||||
@@ -687,6 +687,63 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes specific array indices to an item using default-fill semantics.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The gateway expands the sparse descriptor into a full <c>totalLength</c>-element array
|
||||||
|
/// before forwarding to the worker. Indices not listed in <paramref name="elements"/> are
|
||||||
|
/// written as the element type's default value — this is a RESET, not a preserve. The
|
||||||
|
/// current values at those positions are discarded. <paramref name="totalLength"/> is
|
||||||
|
/// required and must match the declared length of the array attribute.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||||
|
/// <param name="elementDataType">The MXAccess data type of each element.</param>
|
||||||
|
/// <param name="totalLength">The total declared length of the target array attribute.</param>
|
||||||
|
/// <param name="elements">Map of zero-based array index to scalar <see cref="MxValue"/>.</param>
|
||||||
|
/// <param name="userId">User ID context for the write.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
public Task WriteArrayElementsAsync(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
MxDataType elementDataType,
|
||||||
|
uint totalLength,
|
||||||
|
IReadOnlyDictionary<uint, MxValue> elements,
|
||||||
|
int userId = 0,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(elements);
|
||||||
|
MxValue value = BuildSparseArray(elementDataType, totalLength, elements);
|
||||||
|
return WriteAsync(serverHandle, itemHandle, value, userId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds an <see cref="MxValue"/> whose <see cref="MxValue.SparseArrayValue"/> describes a
|
||||||
|
/// default-fill partial array write.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="elementDataType">The MXAccess data type of each element.</param>
|
||||||
|
/// <param name="totalLength">The total declared length of the target array attribute.</param>
|
||||||
|
/// <param name="elements">Map of zero-based array index to scalar <see cref="MxValue"/>.</param>
|
||||||
|
/// <returns>An <see cref="MxValue"/> with <see cref="MxValue.KindOneofCase.SparseArrayValue"/> set.</returns>
|
||||||
|
internal static MxValue BuildSparseArray(
|
||||||
|
MxDataType elementDataType,
|
||||||
|
uint totalLength,
|
||||||
|
IReadOnlyDictionary<uint, MxValue> elements)
|
||||||
|
{
|
||||||
|
MxSparseArray sparse = new()
|
||||||
|
{
|
||||||
|
ElementDataType = elementDataType,
|
||||||
|
TotalLength = totalLength,
|
||||||
|
};
|
||||||
|
foreach (KeyValuePair<uint, MxValue> kv in elements)
|
||||||
|
{
|
||||||
|
sparse.Elements.Add(new MxSparseElement { Index = kv.Key, Value = kv.Value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MxValue { SparseArrayValue = sparse };
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes a value to an item on the MXAccess server without error checking.
|
/// Writes a value to an item on the MXAccess server without error checking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user