diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SparseArrayExpander.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SparseArrayExpander.cs
new file mode 100644
index 0000000..890ff0b
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SparseArrayExpander.cs
@@ -0,0 +1,279 @@
+using Google.Protobuf.WellKnownTypes;
+using Grpc.Core;
+using ZB.MOM.WW.MxGateway.Contracts.Proto;
+
+namespace ZB.MOM.WW.MxGateway.Server.Sessions;
+
+///
+/// Expands a client-supplied sparse array write () into a
+/// full, default-filled in place.
+///
+///
+/// MXAccess has no partial-array write primitive: a write replaces the whole array.
+/// Clients that only care about a few indices send an with
+/// the total length plus the indices they want to set; this expander materializes the
+/// full array so the worker can do an ordinary whole-array COM write. Indices the client
+/// did not mention are reset to the element type's default (they are NOT preserved from
+/// the live value); this is intentional, because the gateway cannot read-modify-write
+/// without racing the provider.
+///
+/// For the MXAccess Integer element type the worker's COM-array converter (see
+/// VariantConverter.ConvertArray) chooses between a 32-bit and 64-bit sub-array
+/// based on the CLR element type. The sparse value carries no CLR array, so this expander
+/// mirrors that choice by inspecting the supplied element value kinds: if any element is an
+/// the whole array is emitted as
+/// ; otherwise it is emitted as
+/// (matching a default Integer array).
+///
+internal static class SparseArrayExpander
+{
+ ///
+ /// Replaces 's with an
+ /// equivalent full . If is not
+ /// a sparse array this is a no-op, so callers may invoke it unconditionally.
+ ///
+ /// The value to expand in place.
+ ///
+ /// when the sparse payload is invalid: zero
+ /// total length, an index at or beyond the total length, a duplicate index, an
+ /// unsupported element type, or an element value whose kind does not match the declared
+ /// element type.
+ ///
+ public static void Expand(MxValue value)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ if (value.KindCase != MxValue.KindOneofCase.SparseArrayValue)
+ {
+ return;
+ }
+
+ MxSparseArray sparse = value.SparseArrayValue;
+ MxDataType elementType = sparse.ElementDataType;
+ uint totalLength = sparse.TotalLength;
+
+ if (totalLength == 0)
+ {
+ throw Invalid("Sparse array total_length must be greater than zero.");
+ }
+
+ if (!IsSupportedElementType(elementType))
+ {
+ throw Invalid($"Sparse array element_data_type '{elementType}' is not a supported scalar element type.");
+ }
+
+ int length = checked((int)totalLength);
+ HashSet seenIndices = new();
+
+ foreach (MxSparseElement element in sparse.Elements)
+ {
+ if (element.Index >= totalLength)
+ {
+ throw Invalid(
+ $"Sparse array index {element.Index} is out of range for total_length {totalLength}.");
+ }
+
+ if (!seenIndices.Add(element.Index))
+ {
+ throw Invalid($"Sparse array has a duplicate index {element.Index}.");
+ }
+
+ ValidateElementKind(elementType, element);
+ }
+
+ MxArray array = BuildArray(elementType, length, sparse.Elements);
+ array.ElementDataType = elementType;
+ array.Dimensions.Add(totalLength);
+
+ // Assigning ArrayValue switches the oneof and clears SparseArrayValue.
+ value.ArrayValue = array;
+ }
+
+ private static MxArray BuildArray(
+ MxDataType elementType,
+ int length,
+ IReadOnlyList elements)
+ {
+ MxArray array = new();
+
+ switch (elementType)
+ {
+ case MxDataType.Boolean:
+ {
+ BoolArray values = new();
+ for (int i = 0; i < length; i++)
+ {
+ values.Values.Add(false);
+ }
+
+ foreach (MxSparseElement element in elements)
+ {
+ values.Values[(int)element.Index] = element.Value.BoolValue;
+ }
+
+ array.BoolValues = values;
+ break;
+ }
+
+ case MxDataType.Integer when UsesInt64(elements):
+ {
+ Int64Array values = new();
+ for (int i = 0; i < length; i++)
+ {
+ values.Values.Add(0L);
+ }
+
+ foreach (MxSparseElement element in elements)
+ {
+ values.Values[(int)element.Index] = ReadInt64(element.Value);
+ }
+
+ array.Int64Values = values;
+ break;
+ }
+
+ case MxDataType.Integer:
+ {
+ Int32Array values = new();
+ for (int i = 0; i < length; i++)
+ {
+ values.Values.Add(0);
+ }
+
+ foreach (MxSparseElement element in elements)
+ {
+ values.Values[(int)element.Index] = element.Value.Int32Value;
+ }
+
+ array.Int32Values = values;
+ break;
+ }
+
+ case MxDataType.Float:
+ {
+ FloatArray values = new();
+ for (int i = 0; i < length; i++)
+ {
+ values.Values.Add(0f);
+ }
+
+ foreach (MxSparseElement element in elements)
+ {
+ values.Values[(int)element.Index] = element.Value.FloatValue;
+ }
+
+ array.FloatValues = values;
+ break;
+ }
+
+ case MxDataType.Double:
+ {
+ DoubleArray values = new();
+ for (int i = 0; i < length; i++)
+ {
+ values.Values.Add(0d);
+ }
+
+ foreach (MxSparseElement element in elements)
+ {
+ values.Values[(int)element.Index] = element.Value.DoubleValue;
+ }
+
+ array.DoubleValues = values;
+ break;
+ }
+
+ case MxDataType.String:
+ {
+ StringArray values = new();
+ for (int i = 0; i < length; i++)
+ {
+ values.Values.Add(string.Empty);
+ }
+
+ foreach (MxSparseElement element in elements)
+ {
+ values.Values[(int)element.Index] = element.Value.StringValue;
+ }
+
+ array.StringValues = values;
+ break;
+ }
+
+ case MxDataType.Time:
+ {
+ TimestampArray values = new();
+ for (int i = 0; i < length; i++)
+ {
+ values.Values.Add(new Timestamp { Seconds = 0, Nanos = 0 });
+ }
+
+ foreach (MxSparseElement element in elements)
+ {
+ values.Values[(int)element.Index] = element.Value.TimestampValue;
+ }
+
+ array.TimestampValues = values;
+ break;
+ }
+
+ default:
+ // Unreachable: IsSupportedElementType gates the element type before BuildArray.
+ throw Invalid($"Sparse array element_data_type '{elementType}' is not supported.");
+ }
+
+ return array;
+ }
+
+ private static bool IsSupportedElementType(MxDataType elementType) => elementType switch
+ {
+ MxDataType.Boolean => true,
+ MxDataType.Integer => true,
+ MxDataType.Float => true,
+ MxDataType.Double => true,
+ MxDataType.String => true,
+ MxDataType.Time => true,
+ _ => false,
+ };
+
+ private static void ValidateElementKind(MxDataType elementType, MxSparseElement element)
+ {
+ MxValue.KindOneofCase kind = element.Value?.KindCase ?? MxValue.KindOneofCase.None;
+
+ bool matches = elementType switch
+ {
+ MxDataType.Boolean => kind == MxValue.KindOneofCase.BoolValue,
+ MxDataType.Integer => kind is MxValue.KindOneofCase.Int32Value or MxValue.KindOneofCase.Int64Value,
+ MxDataType.Float => kind == MxValue.KindOneofCase.FloatValue,
+ MxDataType.Double => kind == MxValue.KindOneofCase.DoubleValue,
+ MxDataType.String => kind == MxValue.KindOneofCase.StringValue,
+ MxDataType.Time => kind == MxValue.KindOneofCase.TimestampValue,
+ _ => false,
+ };
+
+ if (!matches)
+ {
+ throw Invalid(
+ $"Sparse array element at index {element.Index} has value kind '{kind}' which does not match element_data_type '{elementType}'.");
+ }
+ }
+
+ private static bool UsesInt64(IReadOnlyList elements)
+ {
+ foreach (MxSparseElement element in elements)
+ {
+ if (element.Value.KindCase == MxValue.KindOneofCase.Int64Value)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static long ReadInt64(MxValue value) =>
+ value.KindCase == MxValue.KindOneofCase.Int64Value ? value.Int64Value : value.Int32Value;
+
+ private static RpcException Invalid(string message) =>
+ new(new Status(StatusCode.InvalidArgument, message));
+}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SparseArrayExpanderTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SparseArrayExpanderTests.cs
new file mode 100644
index 0000000..a329ad4
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SparseArrayExpanderTests.cs
@@ -0,0 +1,195 @@
+using Google.Protobuf.WellKnownTypes;
+using Grpc.Core;
+using ZB.MOM.WW.MxGateway.Contracts.Proto;
+using ZB.MOM.WW.MxGateway.Server.Sessions;
+
+namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions;
+
+public sealed class SparseArrayExpanderTests
+{
+ private static MxValue SparseValue(
+ MxDataType elementType,
+ uint totalLength,
+ params (uint Index, MxValue Value)[] elements)
+ {
+ MxSparseArray sparse = new()
+ {
+ ElementDataType = elementType,
+ TotalLength = totalLength,
+ };
+
+ foreach ((uint index, MxValue value) in elements)
+ {
+ sparse.Elements.Add(new MxSparseElement { Index = index, Value = value });
+ }
+
+ return new MxValue { SparseArrayValue = sparse };
+ }
+
+ [Fact]
+ public void Expand_Int32_FillsDefaultsAndSetsElement()
+ {
+ MxValue value = SparseValue(
+ MxDataType.Integer,
+ 4,
+ (1, new MxValue { Int32Value = 7 }));
+
+ SparseArrayExpander.Expand(value);
+
+ Assert.Equal(MxValue.KindOneofCase.ArrayValue, value.KindCase);
+ Assert.Equal(MxDataType.Integer, value.ArrayValue.ElementDataType);
+ Assert.Equal(new uint[] { 4 }, value.ArrayValue.Dimensions);
+ Assert.Equal(MxArray.ValuesOneofCase.Int32Values, value.ArrayValue.ValuesCase);
+ Assert.Equal(new[] { 0, 7, 0, 0 }, value.ArrayValue.Int32Values.Values);
+ }
+
+ [Fact]
+ public void Expand_Boolean_EmptyElements_AllDefaultFalse()
+ {
+ MxValue value = SparseValue(MxDataType.Boolean, 3);
+
+ SparseArrayExpander.Expand(value);
+
+ Assert.Equal(MxArray.ValuesOneofCase.BoolValues, value.ArrayValue.ValuesCase);
+ Assert.Equal(new[] { false, false, false }, value.ArrayValue.BoolValues.Values);
+ }
+
+ [Fact]
+ public void Expand_ZeroTotalLength_Throws()
+ {
+ MxValue value = SparseValue(MxDataType.Integer, 0);
+
+ RpcException ex = Assert.Throws(() => SparseArrayExpander.Expand(value));
+ Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
+ }
+
+ [Fact]
+ public void Expand_IndexOutOfRange_Throws()
+ {
+ MxValue value = SparseValue(
+ MxDataType.Integer,
+ 2,
+ (5, new MxValue { Int32Value = 1 }));
+
+ RpcException ex = Assert.Throws(() => SparseArrayExpander.Expand(value));
+ Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
+ }
+
+ [Fact]
+ public void Expand_DuplicateIndex_Throws()
+ {
+ MxValue value = SparseValue(
+ MxDataType.Integer,
+ 4,
+ (1, new MxValue { Int32Value = 1 }),
+ (1, new MxValue { Int32Value = 2 }));
+
+ RpcException ex = Assert.Throws(() => SparseArrayExpander.Expand(value));
+ Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
+ }
+
+ [Fact]
+ public void Expand_UnsupportedElementType_Throws()
+ {
+ MxValue value = SparseValue(MxDataType.Unspecified, 2);
+
+ RpcException ex = Assert.Throws(() => SparseArrayExpander.Expand(value));
+ Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
+ }
+
+ [Fact]
+ public void Expand_ElementValueKindMismatch_Throws()
+ {
+ MxValue value = SparseValue(
+ MxDataType.Integer,
+ 2,
+ (0, new MxValue { StringValue = "nope" }));
+
+ RpcException ex = Assert.Throws(() => SparseArrayExpander.Expand(value));
+ Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
+ }
+
+ [Fact]
+ public void Expand_String_FillsEmptyStringDefault()
+ {
+ MxValue value = SparseValue(
+ MxDataType.String,
+ 2,
+ (0, new MxValue { StringValue = "a" }));
+
+ SparseArrayExpander.Expand(value);
+
+ Assert.Equal(MxArray.ValuesOneofCase.StringValues, value.ArrayValue.ValuesCase);
+ Assert.Equal(new[] { "a", string.Empty }, value.ArrayValue.StringValues.Values);
+ }
+
+ [Fact]
+ public void Expand_Time_FillsEpochDefault()
+ {
+ MxValue value = SparseValue(
+ MxDataType.Time,
+ 2,
+ (1, new MxValue { TimestampValue = new Timestamp { Seconds = 5 } }));
+
+ SparseArrayExpander.Expand(value);
+
+ Assert.Equal(MxArray.ValuesOneofCase.TimestampValues, value.ArrayValue.ValuesCase);
+ Assert.Equal(2, value.ArrayValue.TimestampValues.Values.Count);
+ Assert.Equal(0, value.ArrayValue.TimestampValues.Values[0].Seconds);
+ Assert.Equal(0, value.ArrayValue.TimestampValues.Values[0].Nanos);
+ Assert.Equal(5, value.ArrayValue.TimestampValues.Values[1].Seconds);
+ }
+
+ [Fact]
+ public void Expand_Double_HappyPath()
+ {
+ MxValue value = SparseValue(
+ MxDataType.Double,
+ 3,
+ (2, new MxValue { DoubleValue = 1.5 }));
+
+ SparseArrayExpander.Expand(value);
+
+ Assert.Equal(MxArray.ValuesOneofCase.DoubleValues, value.ArrayValue.ValuesCase);
+ Assert.Equal(new[] { 0d, 0d, 1.5 }, value.ArrayValue.DoubleValues.Values);
+ }
+
+ [Fact]
+ public void Expand_Float_HappyPath()
+ {
+ MxValue value = SparseValue(
+ MxDataType.Float,
+ 2,
+ (0, new MxValue { FloatValue = 2.5f }));
+
+ SparseArrayExpander.Expand(value);
+
+ Assert.Equal(MxArray.ValuesOneofCase.FloatValues, value.ArrayValue.ValuesCase);
+ Assert.Equal(new[] { 2.5f, 0f }, value.ArrayValue.FloatValues.Values);
+ }
+
+ [Fact]
+ public void Expand_Int64_WhenElementIsInt64()
+ {
+ MxValue value = SparseValue(
+ MxDataType.Integer,
+ 3,
+ (2, new MxValue { Int64Value = 9_000_000_000L }));
+
+ SparseArrayExpander.Expand(value);
+
+ Assert.Equal(MxArray.ValuesOneofCase.Int64Values, value.ArrayValue.ValuesCase);
+ Assert.Equal(new[] { 0L, 0L, 9_000_000_000L }, value.ArrayValue.Int64Values.Values);
+ }
+
+ [Fact]
+ public void Expand_NonSparseValue_NoOps()
+ {
+ MxValue value = new() { Int32Value = 42 };
+
+ SparseArrayExpander.Expand(value);
+
+ Assert.Equal(MxValue.KindOneofCase.Int32Value, value.KindCase);
+ Assert.Equal(42, value.Int32Value);
+ }
+}