06030dd1ef
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>
605 lines
19 KiB
C#
605 lines
19 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using Google.Protobuf;
|
|
using Google.Protobuf.WellKnownTypes;
|
|
using MxGateway.Contracts.Proto;
|
|
|
|
namespace MxGateway.Worker.Conversion;
|
|
|
|
public sealed class VariantConverter
|
|
{
|
|
/// <summary>
|
|
/// Converts an object value to an MxValue without a specified data type.
|
|
/// </summary>
|
|
/// <param name="value">Value to convert.</param>
|
|
/// <returns>Converted MxValue.</returns>
|
|
public MxValue Convert(object? value)
|
|
{
|
|
return Convert(value, MxDataType.Unspecified);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts an object value to an MxValue with an expected data type.
|
|
/// </summary>
|
|
/// <param name="value">Value to convert.</param>
|
|
/// <param name="expectedDataType">Expected MXAccess data type.</param>
|
|
/// <returns>Converted MxValue.</returns>
|
|
public MxValue Convert(
|
|
object? value,
|
|
MxDataType expectedDataType)
|
|
{
|
|
if (value is null || value is DBNull)
|
|
{
|
|
return CreateNullValue(value, expectedDataType);
|
|
}
|
|
|
|
if (value is Array array)
|
|
{
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Unspecified,
|
|
VariantType = CreateArrayVariantType(array),
|
|
ArrayValue = ConvertArray(array, expectedDataType),
|
|
};
|
|
}
|
|
|
|
return ConvertScalar(value, expectedDataType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a .NET array to an MxArray.
|
|
/// </summary>
|
|
/// <param name="array">Array to convert.</param>
|
|
/// <param name="expectedElementDataType">Expected data type for array elements.</param>
|
|
/// <returns>Converted MxArray.</returns>
|
|
public MxArray ConvertArray(
|
|
Array array,
|
|
MxDataType expectedElementDataType = MxDataType.Unspecified)
|
|
{
|
|
if (array is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(array));
|
|
}
|
|
|
|
MxArray mxArray = new()
|
|
{
|
|
VariantType = CreateArrayVariantType(array),
|
|
};
|
|
|
|
for (int dimension = 0; dimension < array.Rank; dimension++)
|
|
{
|
|
mxArray.Dimensions.Add((uint)array.GetLength(dimension));
|
|
}
|
|
|
|
System.Type? elementType = array.GetType().GetElementType();
|
|
MxDataType elementDataType = ResolveArrayElementDataType(elementType, expectedElementDataType);
|
|
mxArray.ElementDataType = elementDataType;
|
|
|
|
switch (elementDataType)
|
|
{
|
|
case MxDataType.Boolean:
|
|
mxArray.BoolValues = ConvertBoolArray(array);
|
|
return mxArray;
|
|
|
|
case MxDataType.Integer:
|
|
if (elementType == typeof(long) || elementType == typeof(ulong))
|
|
{
|
|
mxArray.Int64Values = ConvertInt64Array(array);
|
|
}
|
|
else
|
|
{
|
|
mxArray.Int32Values = ConvertInt32Array(array);
|
|
}
|
|
|
|
return mxArray;
|
|
|
|
case MxDataType.Float:
|
|
mxArray.FloatValues = ConvertFloatArray(array);
|
|
return mxArray;
|
|
|
|
case MxDataType.Double:
|
|
mxArray.DoubleValues = ConvertDoubleArray(array);
|
|
return mxArray;
|
|
|
|
case MxDataType.String:
|
|
mxArray.StringValues = ConvertStringArray(array);
|
|
return mxArray;
|
|
|
|
case MxDataType.Time:
|
|
mxArray.TimestampValues = ConvertTimestampArray(array);
|
|
return mxArray;
|
|
|
|
default:
|
|
mxArray.ElementDataType = MxDataType.Unknown;
|
|
mxArray.RawElementDataType = (int)expectedElementDataType;
|
|
mxArray.RawDiagnostic = CreateRawDiagnostic(array);
|
|
mxArray.RawValues = ConvertRawArray(array);
|
|
return mxArray;
|
|
}
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
System.Type valueType = value.GetType();
|
|
string variantType = GetVariantTypeName(valueType);
|
|
|
|
switch (System.Type.GetTypeCode(valueType))
|
|
{
|
|
case TypeCode.Boolean:
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Boolean,
|
|
VariantType = variantType,
|
|
BoolValue = (bool)value,
|
|
};
|
|
|
|
case TypeCode.Byte:
|
|
case TypeCode.SByte:
|
|
case TypeCode.Int16:
|
|
case TypeCode.UInt16:
|
|
case TypeCode.Int32:
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Integer,
|
|
VariantType = variantType,
|
|
Int32Value = System.Convert.ToInt32(value, CultureInfo.InvariantCulture),
|
|
};
|
|
|
|
case TypeCode.UInt32:
|
|
case TypeCode.Int64:
|
|
return ConvertInt64Scalar(value, variantType, expectedDataType);
|
|
|
|
case TypeCode.UInt64:
|
|
return ConvertUInt64Scalar((ulong)value, variantType, expectedDataType);
|
|
|
|
case TypeCode.Single:
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Float,
|
|
VariantType = variantType,
|
|
FloatValue = (float)value,
|
|
};
|
|
|
|
case TypeCode.Double:
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Double,
|
|
VariantType = variantType,
|
|
DoubleValue = (double)value,
|
|
};
|
|
|
|
case TypeCode.Decimal:
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Double,
|
|
VariantType = variantType,
|
|
DoubleValue = System.Convert.ToDouble(value, CultureInfo.InvariantCulture),
|
|
RawDiagnostic = "Decimal value projected to double.",
|
|
};
|
|
|
|
case TypeCode.String:
|
|
case TypeCode.Char:
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.String,
|
|
VariantType = variantType,
|
|
StringValue = System.Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty,
|
|
};
|
|
|
|
case TypeCode.DateTime:
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Time,
|
|
VariantType = variantType,
|
|
TimestampValue = ToTimestamp((DateTime)value),
|
|
};
|
|
|
|
default:
|
|
return CreateRawValue(value, expectedDataType);
|
|
}
|
|
}
|
|
|
|
private static MxValue ConvertInt64Scalar(
|
|
object value,
|
|
string variantType,
|
|
MxDataType expectedDataType)
|
|
{
|
|
long longValue = System.Convert.ToInt64(value, CultureInfo.InvariantCulture);
|
|
|
|
// The MxDataType.Time projection treats the source as a Windows FILETIME
|
|
// (a 64-bit 100-ns tick count since 1601). Only a genuine 64-bit source
|
|
// (long) can carry a valid full FILETIME; a uint can only hold the low
|
|
// 32 bits, which DateTime.FromFileTimeUtc would silently render as a
|
|
// near-1601 timestamp. For uint sources fall through to the integer
|
|
// projection rather than producing a bogus timestamp.
|
|
if (expectedDataType == MxDataType.Time && value is long)
|
|
{
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Time,
|
|
VariantType = variantType,
|
|
TimestampValue = Timestamp.FromDateTime(DateTime.FromFileTimeUtc(longValue)),
|
|
};
|
|
}
|
|
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Integer,
|
|
VariantType = variantType,
|
|
Int64Value = longValue,
|
|
};
|
|
}
|
|
|
|
private static MxValue ConvertUInt64Scalar(
|
|
ulong value,
|
|
string variantType,
|
|
MxDataType expectedDataType)
|
|
{
|
|
if (expectedDataType == MxDataType.Time && value <= long.MaxValue)
|
|
{
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Time,
|
|
VariantType = variantType,
|
|
TimestampValue = Timestamp.FromDateTime(DateTime.FromFileTimeUtc((long)value)),
|
|
};
|
|
}
|
|
|
|
if (value <= long.MaxValue)
|
|
{
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Integer,
|
|
VariantType = variantType,
|
|
Int64Value = (long)value,
|
|
};
|
|
}
|
|
|
|
return CreateRawValue(value, expectedDataType, "UInt64 value exceeds Int64 range.");
|
|
}
|
|
|
|
private static MxValue CreateNullValue(
|
|
object? value,
|
|
MxDataType expectedDataType)
|
|
{
|
|
return new MxValue
|
|
{
|
|
DataType = expectedDataType == MxDataType.Unspecified ? MxDataType.NoData : expectedDataType,
|
|
VariantType = value is DBNull ? "VT_NULL" : "VT_EMPTY",
|
|
IsNull = true,
|
|
};
|
|
}
|
|
|
|
private static MxValue CreateRawValue(
|
|
object value,
|
|
MxDataType expectedDataType,
|
|
string? diagnosticPrefix = null)
|
|
{
|
|
string diagnostic = CreateRawDiagnostic(value);
|
|
if (!string.IsNullOrWhiteSpace(diagnosticPrefix))
|
|
{
|
|
diagnostic = $"{diagnosticPrefix} {diagnostic}";
|
|
}
|
|
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Unknown,
|
|
VariantType = GetVariantTypeName(value.GetType()),
|
|
RawDataType = (int)expectedDataType,
|
|
RawDiagnostic = diagnostic,
|
|
RawValue = ByteString.CopyFromUtf8(System.Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty),
|
|
};
|
|
}
|
|
|
|
private static BoolArray ConvertBoolArray(Array array)
|
|
{
|
|
BoolArray values = new();
|
|
foreach (object? item in array)
|
|
{
|
|
values.Values.Add(item is not null && System.Convert.ToBoolean(item, CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static Int32Array ConvertInt32Array(Array array)
|
|
{
|
|
Int32Array values = new();
|
|
foreach (object? item in array)
|
|
{
|
|
values.Values.Add(item is null ? 0 : System.Convert.ToInt32(item, CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static Int64Array ConvertInt64Array(Array array)
|
|
{
|
|
Int64Array values = new();
|
|
foreach (object? item in array)
|
|
{
|
|
values.Values.Add(item is null ? 0 : System.Convert.ToInt64(item, CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static FloatArray ConvertFloatArray(Array array)
|
|
{
|
|
FloatArray values = new();
|
|
foreach (object? item in array)
|
|
{
|
|
values.Values.Add(item is null ? 0 : System.Convert.ToSingle(item, CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static DoubleArray ConvertDoubleArray(Array array)
|
|
{
|
|
DoubleArray values = new();
|
|
foreach (object? item in array)
|
|
{
|
|
values.Values.Add(item is null ? 0 : System.Convert.ToDouble(item, CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static StringArray ConvertStringArray(Array array)
|
|
{
|
|
StringArray values = new();
|
|
foreach (object? item in array)
|
|
{
|
|
values.Values.Add(item is null ? string.Empty : System.Convert.ToString(item, CultureInfo.InvariantCulture) ?? string.Empty);
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static TimestampArray ConvertTimestampArray(Array array)
|
|
{
|
|
TimestampArray values = new();
|
|
foreach (object? item in array)
|
|
{
|
|
if (item is null)
|
|
{
|
|
values.Values.Add(Timestamp.FromDateTime(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)));
|
|
}
|
|
else if (item is DateTime dateTime)
|
|
{
|
|
values.Values.Add(ToTimestamp(dateTime));
|
|
}
|
|
else
|
|
{
|
|
long fileTime = System.Convert.ToInt64(item, CultureInfo.InvariantCulture);
|
|
values.Values.Add(Timestamp.FromDateTime(DateTime.FromFileTimeUtc(fileTime)));
|
|
}
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static RawArray ConvertRawArray(Array array)
|
|
{
|
|
RawArray values = new();
|
|
foreach (object? item in array)
|
|
{
|
|
string rawValue = item is null
|
|
? string.Empty
|
|
: System.Convert.ToString(item, CultureInfo.InvariantCulture) ?? string.Empty;
|
|
values.Values.Add(ByteString.CopyFromUtf8(rawValue));
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static MxDataType ResolveArrayElementDataType(
|
|
System.Type? elementType,
|
|
MxDataType expectedElementDataType)
|
|
{
|
|
if (expectedElementDataType != MxDataType.Unspecified)
|
|
{
|
|
return expectedElementDataType;
|
|
}
|
|
|
|
if (elementType == typeof(bool))
|
|
{
|
|
return MxDataType.Boolean;
|
|
}
|
|
|
|
if (elementType == typeof(byte)
|
|
|| elementType == typeof(sbyte)
|
|
|| elementType == typeof(short)
|
|
|| elementType == typeof(ushort)
|
|
|| elementType == typeof(int)
|
|
|| elementType == typeof(uint)
|
|
|| elementType == typeof(long)
|
|
|| elementType == typeof(ulong))
|
|
{
|
|
return MxDataType.Integer;
|
|
}
|
|
|
|
if (elementType == typeof(float))
|
|
{
|
|
return MxDataType.Float;
|
|
}
|
|
|
|
if (elementType == typeof(double) || elementType == typeof(decimal))
|
|
{
|
|
return MxDataType.Double;
|
|
}
|
|
|
|
if (elementType == typeof(string) || elementType == typeof(char))
|
|
{
|
|
return MxDataType.String;
|
|
}
|
|
|
|
if (elementType == typeof(DateTime))
|
|
{
|
|
return MxDataType.Time;
|
|
}
|
|
|
|
return MxDataType.Unknown;
|
|
}
|
|
|
|
private static Timestamp ToTimestamp(DateTime dateTime)
|
|
{
|
|
DateTime utcDateTime = dateTime.Kind switch
|
|
{
|
|
DateTimeKind.Utc => dateTime,
|
|
DateTimeKind.Local => dateTime.ToUniversalTime(),
|
|
_ => DateTime.SpecifyKind(dateTime, DateTimeKind.Utc),
|
|
};
|
|
|
|
return Timestamp.FromDateTime(utcDateTime);
|
|
}
|
|
|
|
private static string CreateArrayVariantType(Array array)
|
|
{
|
|
System.Type? elementType = array.GetType().GetElementType();
|
|
return $"SAFEARRAY({GetVariantTypeName(elementType)})";
|
|
}
|
|
|
|
private static string GetVariantTypeName(System.Type? type)
|
|
{
|
|
if (type is null)
|
|
{
|
|
return "VT_EMPTY";
|
|
}
|
|
|
|
System.Type nonNullableType = Nullable.GetUnderlyingType(type) ?? type;
|
|
if (nonNullableType == typeof(bool))
|
|
{
|
|
return "VT_BOOL";
|
|
}
|
|
|
|
if (nonNullableType == typeof(byte))
|
|
{
|
|
return "VT_UI1";
|
|
}
|
|
|
|
if (nonNullableType == typeof(sbyte))
|
|
{
|
|
return "VT_I1";
|
|
}
|
|
|
|
if (nonNullableType == typeof(short))
|
|
{
|
|
return "VT_I2";
|
|
}
|
|
|
|
if (nonNullableType == typeof(ushort))
|
|
{
|
|
return "VT_UI2";
|
|
}
|
|
|
|
if (nonNullableType == typeof(int))
|
|
{
|
|
return "VT_I4";
|
|
}
|
|
|
|
if (nonNullableType == typeof(uint))
|
|
{
|
|
return "VT_UI4";
|
|
}
|
|
|
|
if (nonNullableType == typeof(long))
|
|
{
|
|
return "VT_I8";
|
|
}
|
|
|
|
if (nonNullableType == typeof(ulong))
|
|
{
|
|
return "VT_UI8";
|
|
}
|
|
|
|
if (nonNullableType == typeof(float))
|
|
{
|
|
return "VT_R4";
|
|
}
|
|
|
|
if (nonNullableType == typeof(double) || nonNullableType == typeof(decimal))
|
|
{
|
|
return "VT_R8";
|
|
}
|
|
|
|
if (nonNullableType == typeof(string) || nonNullableType == typeof(char))
|
|
{
|
|
return "VT_BSTR";
|
|
}
|
|
|
|
if (nonNullableType == typeof(DateTime))
|
|
{
|
|
return "VT_DATE";
|
|
}
|
|
|
|
return $"CLR:{nonNullableType.FullName}";
|
|
}
|
|
|
|
private static string CreateRawDiagnostic(object value)
|
|
{
|
|
return $"Unsupported variant projection for CLR type '{value.GetType().FullName}'.";
|
|
}
|
|
}
|