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 { /// /// Converts an object value to an MxValue without a specified data type. /// /// Value to convert. /// Converted MxValue. public MxValue Convert(object? value) { return Convert(value, MxDataType.Unspecified); } /// /// Converts an object value to an MxValue with an expected data type. /// /// Value to convert. /// Expected MXAccess data type. /// Converted MxValue. 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); } /// /// Converts a .NET array to an MxArray. /// /// Array to convert. /// Expected data type for array elements. /// Converted MxArray. 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; } } /// /// Converts an 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 . /// /// Protobuf value to convert. /// A COM-marshalable value, or for an MXAccess null. 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}'."; } }