Implement MXAccess write commands in the worker

The .proto contract and MxCommandKind already defined Write, Write2,
WriteSecured, and WriteSecured2, but the worker's MxAccessCommandExecutor
had no case for any of them — every write kind fell through to
CreateInvalidRequestReply ("Unsupported MXAccess command kind Write").

Implement all four:

- VariantConverter.ConvertToComValue projects an MxValue into a
  COM-marshalable object (scalars, arrays, null) — the inverse of the
  existing COM-to-MxValue projection.
- IMxAccessServer / MxAccessComServer gain Write/Write2/WriteSecured/
  WriteSecured2, routed to ILMXProxyServer / ILMXProxyServer4.
- MxAccessSession and MxAccessCommandExecutor add the four write paths,
  following the existing ExecuteAdvise pattern; the reply is a plain OK
  reply and the outcome surfaces later as an OnWriteComplete event.

Verified live: a Write now returns PROTOCOL_STATUS_CODE_OK and produces
an OnWriteComplete event where it previously returned InvalidRequest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-19 14:45:35 -04:00
parent e355a7674b
commit 06030dd1ef
9 changed files with 823 additions and 4 deletions
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Linq;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using MxGateway.Contracts.Proto;
@@ -118,6 +119,63 @@ public sealed class VariantConverter
}
}
/// <summary>
/// Converts an <see cref="MxValue"/> into a CLR object suitable for an
/// MXAccess COM write. The COM marshaler boxes the returned value into the
/// matching VARIANT, so this is the inverse of <see cref="Convert(object?)"/>.
/// </summary>
/// <param name="value">Protobuf value to convert.</param>
/// <returns>A COM-marshalable value, or <see langword="null"/> for an MXAccess null.</returns>
public object? ConvertToComValue(MxValue value)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
if (value.IsNull)
{
return null;
}
return value.KindCase switch
{
MxValue.KindOneofCase.BoolValue => value.BoolValue,
MxValue.KindOneofCase.Int32Value => value.Int32Value,
MxValue.KindOneofCase.Int64Value => value.Int64Value,
MxValue.KindOneofCase.FloatValue => value.FloatValue,
MxValue.KindOneofCase.DoubleValue => value.DoubleValue,
MxValue.KindOneofCase.StringValue => value.StringValue,
// The COM marshaler renders a DateTime as VT_DATE; MXAccess accepts
// it as the timestamped-write time argument.
MxValue.KindOneofCase.TimestampValue => value.TimestampValue.ToDateTime(),
MxValue.KindOneofCase.ArrayValue => ConvertToComArray(value.ArrayValue),
MxValue.KindOneofCase.RawValue => throw new ArgumentException(
"MxValue raw payloads cannot be written to MXAccess.", nameof(value)),
_ => throw new ArgumentException(
"MxValue has no value kind set; nothing to write.", nameof(value)),
};
}
private static Array ConvertToComArray(MxArray array)
{
return array.ValuesCase switch
{
MxArray.ValuesOneofCase.BoolValues => array.BoolValues.Values.ToArray(),
MxArray.ValuesOneofCase.Int32Values => array.Int32Values.Values.ToArray(),
MxArray.ValuesOneofCase.Int64Values => array.Int64Values.Values.ToArray(),
MxArray.ValuesOneofCase.FloatValues => array.FloatValues.Values.ToArray(),
MxArray.ValuesOneofCase.DoubleValues => array.DoubleValues.Values.ToArray(),
MxArray.ValuesOneofCase.StringValues => array.StringValues.Values.ToArray(),
MxArray.ValuesOneofCase.TimestampValues =>
array.TimestampValues.Values.Select(timestamp => timestamp.ToDateTime()).ToArray(),
MxArray.ValuesOneofCase.RawValues => throw new ArgumentException(
"MxArray raw payloads cannot be written to MXAccess.", nameof(array)),
_ => throw new ArgumentException(
"MxArray has no element values set; nothing to write.", nameof(array)),
};
}
private static MxValue ConvertScalar(
object value,
MxDataType expectedDataType)