From fc943c8a3698a8cd9a65d603ab3f3874efbdecbb Mon Sep 17 00:00:00 2001 From: ahmed Date: Tue, 4 Nov 2025 11:47:40 +0300 Subject: [PATCH] Async serialization --- Esiur/Core/AsyncBag.cs | 4 + Esiur/Data/DataConverter.cs | 4 +- Esiur/Data/DataDeserializer.cs | 396 ++++++++++++++---- Esiur/Data/DataSerializer.cs | 2 +- Esiur/Data/RuntimeCaster.cs | 370 ++++++++++++++++ Esiur/Data/TRU.cs | 4 + Esiur/Net/HTTP/HTTPServer.cs | 2 +- Esiur/Net/IIP/DistributedConnection.cs | 9 +- .../Net/IIP/DistributedConnectionProtocol.cs | 41 +- Esiur/Resource/Instance.cs | 4 +- 10 files changed, 717 insertions(+), 119 deletions(-) create mode 100644 Esiur/Data/RuntimeCaster.cs diff --git a/Esiur/Core/AsyncBag.cs b/Esiur/Core/AsyncBag.cs index 565789b..a6bf245 100644 --- a/Esiur/Core/AsyncBag.cs +++ b/Esiur/Core/AsyncBag.cs @@ -22,6 +22,7 @@ SOFTWARE. */ +using Esiur.Data; using System; using System.Collections.Generic; using System.Linq; @@ -102,6 +103,9 @@ public class AsyncBag : AsyncReply, IAsyncBag } else { + if (ArrayType != null) + replies[i] = RuntimeCaster.Cast(replies[i], ArrayType); + results.SetValue(replies[i], index); count++; if (count == replies.Count) diff --git a/Esiur/Data/DataConverter.cs b/Esiur/Data/DataConverter.cs index dfa3b23..c9c78c1 100644 --- a/Esiur/Data/DataConverter.cs +++ b/Esiur/Data/DataConverter.cs @@ -45,7 +45,7 @@ public static class DC // Data Converter { - public static object CastConvert(object value, Type destinationType) + public static object CastConvertOld(object value, Type destinationType) { if (value == null) return null; @@ -68,7 +68,7 @@ public static class DC // Data Converter for (var i = 0; i < rt.Length; i++) { - rt.SetValue(CastConvert(v.GetValue(i), destinationType), i); + rt.SetValue(CastConvertOld(v.GetValue(i), destinationType), i); } return rt; diff --git a/Esiur/Data/DataDeserializer.cs b/Esiur/Data/DataDeserializer.cs index afa7d3a..75999c9 100644 --- a/Esiur/Data/DataDeserializer.cs +++ b/Esiur/Data/DataDeserializer.cs @@ -376,34 +376,74 @@ public static class DataDeserializer public static unsafe object RecordParserAsync(ParsedTDU tdu, DistributedConnection connection, uint[] requestSequence) { - - var reply = new AsyncReply(); - var classId = tdu.Metadata.GetUUID(0); + var template = connection.Instance.Warehouse.GetTemplateByClassId(classId, + TemplateType.Record); + var rt = new AsyncReply(); - var template = connection.Instance.Warehouse.GetTemplateByClassId(classId, TemplateType.Record); + + var list = new AsyncBag(); + + ParsedTDU current; + ParsedTDU? previous = null; + + var offset = tdu.Offset; + var length = tdu.ContentLength; + var ends = offset + (uint)length; var initRecord = (TypeTemplate template) => { - ListParserAsync(tdu, connection, requestSequence).Then(r => + for (var i = 0; i < template.Properties.Length; i++) { - var ar = (object[])r; + current = ParsedTDU.Parse(tdu.Data, offset, ends); - if (template == null) + if (current.Class == TDUClass.Invalid) + throw new Exception("Unknown type."); + + + if (current.Identifier == TDUIdentifier.TypeContinuation) { - // @TODO: add parse if no template settings - reply.TriggerError(new AsyncException(ErrorType.Management, (ushort)ExceptionCode.TemplateNotFound, - "Template not found for record.")); + current.Class = previous.Value.Class; + current.Identifier = previous.Value.Identifier; + current.Metadata = previous.Value.Metadata; } - else if (template.DefinedType != null) + else if (current.Identifier == TDUIdentifier.TypeOfTarget) { + var (idf, mt) = template.Properties[i].ValueType.GetMetadata(); + current.Class = TDUClass.Typed; + current.Identifier = idf; + current.Metadata = mt; + current.Index = (int)idf & 0x7; + } + + var (cs, reply) = Codec.ParseAsync(current, connection, requestSequence); + + list.Add(reply); + + if (cs > 0) + { + offset += (uint)current.TotalLength; + length -= (uint)current.TotalLength; + previous = current; + } + else + throw new Exception("Error while parsing structured data"); + + } + + list.Seal(); + + list.Then(results => + { + if (template.DefinedType != null) + { + var record = Activator.CreateInstance(template.DefinedType) as IRecord; for (var i = 0; i < template.Properties.Length; i++) { try { - //var v = Convert.ChangeType(ar[i], template.Properties[i].PropertyInfo.PropertyType); - var v = DC.CastConvert(ar[i], template.Properties[i].PropertyInfo.PropertyType); + var v = RuntimeCaster.Cast(results[i], template.Properties[i].PropertyInfo.PropertyType); template.Properties[i].PropertyInfo.SetValue(record, v); } catch (Exception ex) @@ -412,21 +452,23 @@ public static class DataDeserializer } } - reply.Trigger(record); + rt.Trigger(record); } else { var record = new Record(); for (var i = 0; i < template.Properties.Length; i++) - record.Add(template.Properties[i].Name, ar[i]); + record.Add(template.Properties[i].Name, results[i]); - reply.Trigger(record); + rt.Trigger(record); } }); + }; + if (template != null) { initRecord(template); @@ -437,14 +479,83 @@ public static class DataDeserializer connection.GetTemplate(classId).Then(tmp => { initRecord(tmp); - }).Error(x => reply.TriggerError(x)); + }).Error(x => rt.TriggerError(x)); } else { initRecord(null); } - return reply; + return rt; + + + + //var classId = tdu.Metadata.GetUUID(0); + + //var template = connection.Instance.Warehouse.GetTemplateByClassId(classId, TemplateType.Record); + + //var initRecord = (TypeTemplate template) => + //{ + // ListParserAsync(tdu, connection, requestSequence).Then(r => + // { + // var ar = (object[])r; + + // if (template == null) + // { + // // @TODO: add parse if no template settings + // reply.TriggerError(new AsyncException(ErrorType.Management, (ushort)ExceptionCode.TemplateNotFound, + // "Template not found for record.")); + // } + // else if (template.DefinedType != null) + // { + // var record = Activator.CreateInstance(template.DefinedType) as IRecord; + // for (var i = 0; i < template.Properties.Length; i++) + // { + // try + // { + // //var v = Convert.ChangeType(ar[i], template.Properties[i].PropertyInfo.PropertyType); + // var v = RuntimeCaster.Cast(ar[i], template.Properties[i].PropertyInfo.PropertyType); + // template.Properties[i].PropertyInfo.SetValue(record, v); + // } + // catch (Exception ex) + // { + // Global.Log(ex); + // } + // } + + // reply.Trigger(record); + // } + // else + // { + // var record = new Record(); + + // for (var i = 0; i < template.Properties.Length; i++) + // record.Add(template.Properties[i].Name, ar[i]); + + // reply.Trigger(record); + // } + + // }); + //}; + + //if (template != null) + //{ + // initRecord(template); + //} + //else if (connection != null) + //{ + // // try to get the template from the other end + // connection.GetTemplate(classId).Then(tmp => + // { + // initRecord(tmp); + // }).Error(x => reply.TriggerError(x)); + //} + //else + //{ + // initRecord(null); + //} + + //return reply; } @@ -453,13 +564,6 @@ public static class DataDeserializer var classId = tdu.Metadata.GetUUID(0); var template = warehouse.GetTemplateByClassId(classId, TemplateType.Record); - - - - //var r = ListParser(tdu, warehouse); - - //var ar = (object[])r; - if (template == null) { // @TODO: add parse if no template settings @@ -515,9 +619,6 @@ public static class DataDeserializer } - - - if (template.DefinedType != null) { @@ -526,8 +627,7 @@ public static class DataDeserializer { try { - //var v = Convert.ChangeType(ar[i], template.Properties[i].PropertyInfo.PropertyType); - var v = DC.CastConvert(list[i], template.Properties[i].PropertyInfo.PropertyType); + var v = RuntimeCaster.Cast(list[i], template.Properties[i].PropertyInfo.PropertyType); template.Properties[i].PropertyInfo.SetValue(record, v); } catch (Exception ex) @@ -567,7 +667,8 @@ public static class DataDeserializer var index = tdu.Data[tdu.Offset]; - var template = connection.Instance.Warehouse.GetTemplateByClassId(classId, TemplateType.Enum); + var template = connection.Instance.Warehouse.GetTemplateByClassId(classId, + TemplateType.Enum); if (template != null) { @@ -839,52 +940,90 @@ public static class DataDeserializer public static AsyncReply TypedMapParserAsync(ParsedTDU tdu, DistributedConnection connection, uint[] requestSequence) { - // get key type - - var (keyCs, keyRepType) = TRU.Parse(tdu.Metadata, 0); - var (valueCs, valueRepType) = TRU.Parse(tdu.Metadata, keyCs); - - var wh = connection.Instance.Warehouse; - - var map = (IMap)Activator.CreateInstance(typeof(Map<,>).MakeGenericType(keyRepType.GetRuntimeType(wh), valueRepType.GetRuntimeType(wh))); var rt = new AsyncReply(); - var results = new AsyncBag(); + // get key type - var offset = tdu.Offset; - var length = tdu.ContentLength; + var (keyCs, keysTru) = TRU.Parse(tdu.Metadata, 0); + var (valueCs, valuesTru) = TRU.Parse(tdu.Metadata, keyCs); - while (length > 0) + var map = (IMap)Activator.CreateInstance(typeof(Map<,>).MakeGenericType( + keysTru.GetRuntimeType(connection.Instance.Warehouse), + valuesTru.GetRuntimeType(connection.Instance.Warehouse))); + + + + var keysTdu = ParsedTDU.Parse(tdu.Data, tdu.Offset, + (uint)(tdu.Offset + tdu.ContentLength)); + + var valuesTdu = ParsedTDU.Parse(tdu.Data, + (uint)(keysTdu.Offset + keysTdu.ContentLength), + tdu.Ends); + + var keysReply = TypedArrayParserAsync(keysTdu, keysTru, connection, requestSequence); + var valuesReply = TypedArrayParserAsync(valuesTdu, valuesTru, connection, requestSequence); + + + keysReply.Then(keys => { - var (cs, reply) = Codec.ParseAsync(tdu.Data, offset, connection, requestSequence); - - - results.Add(reply); - - if (cs > 0) + valuesReply.Then(values => { - offset += (uint)cs; - length -= (uint)cs; - } - else - throw new Exception("Error while parsing structured data"); - - } - - results.Seal(); - - results.Then(ar => - { - for (var i = 0; i < ar.Length; i += 2) - map.Add(ar[i], ar[i + 1]); - - rt.Trigger(map); + for (var i = 0; i < ((Array)keys).Length; i++) + map.Add(((Array)keys).GetValue(i), ((Array)values).GetValue(i)); + }); }); return rt; + + //// get key type + + //var (keyCs, keyRepType) = TRU.Parse(tdu.Metadata, 0); + //var (valueCs, valueRepType) = TRU.Parse(tdu.Metadata, keyCs); + + //var wh = connection.Instance.Warehouse; + + //var map = (IMap)Activator.CreateInstance(typeof(Map<,>).MakeGenericType(keyRepType.GetRuntimeType(wh), valueRepType.GetRuntimeType(wh))); + + //var rt = new AsyncReply(); + + //var results = new AsyncBag(); + + //var offset = tdu.Offset; + //var length = tdu.ContentLength; + + //while (length > 0) + //{ + // var (cs, reply) = Codec.ParseAsync(tdu.Data, offset, connection, requestSequence); + + + // results.Add(reply); + + // if (cs > 0) + // { + // offset += (uint)cs; + // length -= (uint)cs; + // } + // else + // throw new Exception("Error while parsing structured data"); + + //} + + //results.Seal(); + + //results.Then(ar => + //{ + // for (var i = 0; i < ar.Length; i += 2) + // map.Add(ar[i], ar[i + 1]); + + // rt.Trigger(map); + //}); + + + //return rt; + } public static Array TypedArrayParser(ParsedTDU tdu, TRU tru, Warehouse warehouse) @@ -927,7 +1066,7 @@ public static class DataDeserializer if (current.Class == TDUClass.Invalid) throw new Exception("Unknown type."); - + if (current.Identifier == TDUIdentifier.TypeContinuation) { @@ -979,11 +1118,11 @@ public static class DataDeserializer - var keysTdu = ParsedTDU.Parse(tdu.Data, tdu.Offset, + var keysTdu = ParsedTDU.Parse(tdu.Data, tdu.Offset, (uint)(tdu.Offset + tdu.ContentLength)); - var valuesTdu = ParsedTDU.Parse(tdu.Data, - (uint)(keysTdu.Offset+keysTdu.ContentLength), + var valuesTdu = ParsedTDU.Parse(tdu.Data, + (uint)(keysTdu.Offset + keysTdu.ContentLength), tdu.Ends); var keys = TypedArrayParser(keysTdu, keysTru, warehouse); @@ -1168,17 +1307,9 @@ public static class DataDeserializer } - public static AsyncReply TypedArrayParserAsync(byte[] data, uint offset, TRU tru, DistributedConnection connection, uint[] requestSequence) + public static AsyncReply TypedArrayParserAsync(ParsedTDU tdu, TRU tru, DistributedConnection connection, uint[] requestSequence) { - throw new NotImplementedException(); - } - - public static AsyncReply TypedListParserAsync(ParsedTDU tdu, DistributedConnection connection, uint[] requestSequence) - { - // get the type - var (hdrCs, rep) = TRU.Parse(tdu.Metadata, 0); - - switch (rep.Identifier) + switch (tru.Identifier) { case TRUIdentifier.Int32: return new AsyncReply(GroupInt32Codec.Decode(tdu.Data.AsSpan( @@ -1200,11 +1331,10 @@ public static class DataDeserializer (int)tdu.Offset, (int)tdu.ContentLength))); default: - var rt = new AsyncBag(); - var runtimeType = rep.GetRuntimeType(connection.Instance.Warehouse); + var list = new AsyncBag(); - rt.ArrayType = runtimeType; + list.ArrayType = tru.GetRuntimeType(connection.Instance.Warehouse); ParsedTDU current; ParsedTDU? previous = null; @@ -1215,7 +1345,6 @@ public static class DataDeserializer while (length > 0) { - current = ParsedTDU.Parse(tdu.Data, offset, ends); if (current.Class == TDUClass.Invalid) @@ -1228,24 +1357,111 @@ public static class DataDeserializer current.Identifier = previous.Value.Identifier; current.Metadata = previous.Value.Metadata; } + else if (current.Identifier == TDUIdentifier.TypeOfTarget) + { + var (idf, mt) = tru.GetMetadata(); + current.Class = TDUClass.Typed; + current.Identifier = idf; + current.Metadata = mt; + current.Index = (int)idf & 0x7; + } - var (cs, reply) = Codec.ParseAsync(tdu.Data, offset, connection, requestSequence); + var (cs, reply) = Codec.ParseAsync(current, connection, requestSequence); - rt.Add(reply); + list.Add(reply); if (cs > 0) { - offset += (uint)cs; - length -= (uint)cs; + offset += (uint)current.TotalLength; + length -= (uint)current.TotalLength; + previous = current; } else throw new Exception("Error while parsing structured data"); } - rt.Seal(); - return rt; + list.Seal(); + return list; } + + } + + public static AsyncReply TypedListParserAsync(ParsedTDU tdu, DistributedConnection connection, uint[] requestSequence) + { + // get the type + var (hdrCs, tru) = TRU.Parse(tdu.Metadata, 0); + + return TypedArrayParserAsync(tdu, tru, connection, requestSequence); + + //switch (rep.Identifier) + //{ + // case TRUIdentifier.Int32: + // return new AsyncReply(GroupInt32Codec.Decode(tdu.Data.AsSpan( + // (int)tdu.Offset, (int)tdu.ContentLength))); + // case TRUIdentifier.Int64: + // return new AsyncReply(GroupInt64Codec.Decode(tdu.Data.AsSpan( + // (int)tdu.Offset, (int)tdu.ContentLength))); + // case TRUIdentifier.Int16: + // return new AsyncReply(GroupInt16Codec.Decode(tdu.Data.AsSpan( + // (int)tdu.Offset, (int)tdu.ContentLength))); + // case TRUIdentifier.UInt32: + // return new AsyncReply(GroupUInt32Codec.Decode(tdu.Data.AsSpan( + // (int)tdu.Offset, (int)tdu.ContentLength))); + // case TRUIdentifier.UInt64: + // return new AsyncReply(GroupUInt64Codec.Decode(tdu.Data.AsSpan( + // (int)tdu.Offset, (int)tdu.ContentLength))); + // case TRUIdentifier.UInt16: + // return new AsyncReply(GroupUInt16Codec.Decode(tdu.Data.AsSpan( + // (int)tdu.Offset, (int)tdu.ContentLength))); + // default: + + // var rt = new AsyncBag(); + + // var runtimeType = rep.GetRuntimeType(connection.Instance.Warehouse); + + // rt.ArrayType = runtimeType; + + // ParsedTDU current; + // ParsedTDU? previous = null; + + // var offset = tdu.Offset; + // var length = tdu.ContentLength; + // var ends = offset + (uint)length; + + // while (length > 0) + // { + + // current = ParsedTDU.Parse(tdu.Data, offset, ends); + + // if (current.Class == TDUClass.Invalid) + // throw new Exception("Unknown type."); + + + // if (current.Identifier == TDUIdentifier.TypeContinuation) + // { + // current.Class = previous.Value.Class; + // current.Identifier = previous.Value.Identifier; + // current.Metadata = previous.Value.Metadata; + // } + + // var (cs, reply) = Codec.ParseAsync(tdu.Data, offset, connection, requestSequence); + + // rt.Add(reply); + + // if (cs > 0) + // { + // offset += (uint)cs; + // length -= (uint)cs; + // } + // else + // throw new Exception("Error while parsing structured data"); + + // } + + // rt.Seal(); + // return rt; + //} } public static object TypedListParser(ParsedTDU tdu, Warehouse warehouse) @@ -1268,7 +1484,7 @@ public static class DataDeserializer var pvs = new List(); for (var i = 0; i < ar.Length; i += 3) - pvs.Add(new PropertyValue(ar[2], (ulong?)ar[0], (DateTime?)ar[1])); + pvs.Add(new PropertyValue(ar[2], Convert.ToUInt64(ar[0]), (DateTime?)ar[1])); rt.Trigger(pvs.ToArray()); diff --git a/Esiur/Data/DataSerializer.cs b/Esiur/Data/DataSerializer.cs index c0f4227..c2b1ccb 100644 --- a/Esiur/Data/DataSerializer.cs +++ b/Esiur/Data/DataSerializer.cs @@ -568,7 +568,7 @@ public static class DataSerializer { var tdu = Codec.ComposeInternal(i, warehouse, connection); - var currentTru = TRU.FromType(i.GetType()); + var currentTru = TRU.FromType(i?.GetType()); if (isTyped && tru.Match(currentTru)) { diff --git a/Esiur/Data/RuntimeCaster.cs b/Esiur/Data/RuntimeCaster.cs new file mode 100644 index 0000000..3e1aff0 --- /dev/null +++ b/Esiur/Data/RuntimeCaster.cs @@ -0,0 +1,370 @@ +using System; +using System.Collections.Generic; +using System.Text; + +#nullable enable + +namespace Esiur.Data; + +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq.Expressions; + +public enum NaNInfinityPolicy +{ + Throw, // Default: throw on NaN/∞ when converting to non-floating types + NullIfNullable, // If target is Nullable, return null; otherwise throw + CoerceZero // Replace NaN/∞ with 0 +} + +public sealed class RuntimeCastOptions +{ + public bool CheckedNumeric { get; set; } = true; + + // For DateTime/DateTimeOffset parsing + public CultureInfo Culture { get; set; } = CultureInfo.InvariantCulture; + public DateTimeStyles DateTimeStyles { get; set; } = DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal; + + // For enums + public bool EnumIgnoreCase { get; set; } = true; + public bool EnumMustBeDefined { get; set; } = false; + + // float/double → decimal behavior + public NaNInfinityPolicy NaNInfinityPolicy { get; set; } = NaNInfinityPolicy.Throw; +} + +public static class RuntimeCaster +{ + + public static readonly RuntimeCastOptions Default = new RuntimeCastOptions(); + + // (fromType, toType) -> converter(value, options) (options captured at call time) + private static readonly ConcurrentDictionary<(Type from, Type to), Func> _cache = new(); + + // Numeric-only compiled converters (fast path), keyed by checked/unchecked + private static readonly ConcurrentDictionary<(Type from, Type to, bool @checked), Func> _numericCache = new(); + + public static object Cast(object value, Type toType, RuntimeCastOptions? options = null) + { + options ??= Default; + + if (toType is null) throw new ArgumentNullException(nameof(toType)); + if (value is null) + { + if (IsNonNullableValueType(toType)) + throw new InvalidCastException($"Cannot cast null to non-nullable {toType}."); + return null; + } + + var fromType = value.GetType(); + if (toType.IsAssignableFrom(fromType)) return value; // already compatible + + var fn = _cache.GetOrAdd((fromType, toType), k => BuildConverter(k.from, k.to)); + return fn(value, options); + } + + // ------------------------ Builder ------------------------ + private static Func BuildConverter(Type fromType, Type toType) + { + // Nullable handling is done inside ConvertCore. + return (value, opts) => ConvertCore(value, fromType, toType, opts); + } + + // ------------------------ Core Routing ------------------------ + private static object ConvertCore(object value, Type fromType, Type toType, RuntimeCastOptions opts) + { + var toUnderlying = Nullable.GetUnderlyingType(toType) ?? toType; + var fromUnderlying = Nullable.GetUnderlyingType(fromType) ?? fromType; + + // If converting nullable source and it's null → result is null if target nullable, else throw + if (!fromType.IsValueType && value is null) + { + if (IsNonNullableValueType(toType)) + throw new InvalidCastException($"Cannot cast null to non-nullable {toType}."); + return null; + } + + // Special cases first + // 1) Enum targets + if (toUnderlying.IsEnum) + return ConvertToEnum(value, fromUnderlying, toType, toUnderlying, opts); + + // 2) Guid targets + if (toUnderlying == typeof(Guid)) + return ConvertToGuid(value, fromUnderlying, toType); + + // 3) DateTime / DateTimeOffset targets + if (toUnderlying == typeof(DateTime)) + return ConvertToDateTime(value, fromUnderlying, toType, opts); + if (toUnderlying == typeof(DateTimeOffset)) + return ConvertToDateTimeOffset(value, fromUnderlying, toType, opts); + + // 4) decimal from float/double with NaN/∞ policy + if (toUnderlying == typeof(decimal) && (fromUnderlying == typeof(float) || fromUnderlying == typeof(double))) + { + var dec = ConvertFloatDoubleToDecimal(value, fromUnderlying, opts, out bool useNull); + if (toType != toUnderlying) // wrap in Nullable + return useNull ? null : (decimal?)dec; + if (useNull) throw new OverflowException("NaN/Infinity cannot be converted to decimal."); + return dec; + } + + // 5) General numeric conversions via compiled expression + if (IsNumeric(fromUnderlying) && IsNumeric(toUnderlying)) + { + var nc = _numericCache.GetOrAdd((fromUnderlying, toUnderlying, opts.CheckedNumeric), + k => BuildNumericConverter(k.from, k.to, k.@checked)); + var result = nc(value); + // Wrap into nullable if needed + if (toType != toUnderlying) return BoxNullable(result, toUnderlying); + return result; + } + + // 6) String <-> other basics (Use TypeConverter first; if no path, fall through) + if (toUnderlying == typeof(string)) + return value?.ToString(); + + // 7) Last-resort: TypeConverter or ChangeType once, inside this compiled path + // Try TypeConverter(target) + var tc = System.ComponentModel.TypeDescriptor.GetConverter(toUnderlying); + if (tc.CanConvertFrom(fromUnderlying)) + { + var r = tc.ConvertFrom(null, opts.Culture, value); + if (toType != toUnderlying) return BoxNullable(r, toUnderlying); + return r!; + } + + // Try TypeConverter(source) + var tc2 = System.ComponentModel.TypeDescriptor.GetConverter(fromUnderlying); + if (tc2.CanConvertTo(toUnderlying)) + { + var r = tc2.ConvertTo(null, opts.Culture, value, toUnderlying); + if (toType != toUnderlying) return BoxNullable(r, toUnderlying); + return r!; + } + + // Try Convert.ChangeType for IConvertible fallbacks + try + { + var r = Convert.ChangeType(value, toUnderlying, opts.Culture); + if (toType != toUnderlying) return BoxNullable(r, toUnderlying); + return r!; + } + catch + { + // Final attempt: assignable cast via reflection (e.g., interfaces) + if (toUnderlying.IsInstanceOfType(value)) + { + if (toType != toUnderlying) return BoxNullable(value, toUnderlying); + return value; + } + throw new InvalidCastException($"Cannot cast {fromType} to {toType}."); + } + } + + // ------------------------ Helpers: Numeric ------------------------ + private static Func BuildNumericConverter(Type from, Type to, bool @checked) + { + var p = Expression.Parameter(typeof(object), "v"); + + Expression val = from.IsValueType + ? Expression.Unbox(p, from) + : Expression.Convert(p, from); + + Expression body; + try + { + body = @checked ? Expression.ConvertChecked(val, to) : Expression.Convert(val, to); + } + catch (InvalidOperationException) + { + // Non-legal numeric convert — should be rare given IsNumeric check. + throw new InvalidCastException($"Numeric conversion not supported: {from} -> {to}"); + } + + // Box result to object + Expression boxed = to.IsValueType ? Expression.Convert(body, typeof(object)) : body; + return Expression.Lambda>(boxed, p).Compile(); + } + + private static bool IsNumeric(Type t) + { + t = Nullable.GetUnderlyingType(t) ?? t; + return t == typeof(byte) || t == typeof(sbyte) || + t == typeof(short) || t == typeof(ushort) || + t == typeof(int) || t == typeof(uint) || + t == typeof(long) || t == typeof(ulong) || + t == typeof(float) || t == typeof(double) || + t == typeof(decimal); + } + + private static bool IsNonNullableValueType(Type t) + => t.IsValueType && Nullable.GetUnderlyingType(t) is null; + + private static object BoxNullable(object value, Type underlying) + { + // Create Nullable(value) then box + var nt = typeof(Nullable<>).MakeGenericType(underlying); + var ctor = nt.GetConstructor(new[] { underlying })!; + return ctor.Invoke(new[] { value }); + } + + // ------------------------ Helpers: NaN/∞ to decimal ------------------------ + private static decimal ConvertFloatDoubleToDecimal(object value, Type fromUnderlying, RuntimeCastOptions opts, out bool useNull) + { + useNull = false; + + if (fromUnderlying == typeof(float)) + { + var f = (float)value; + if (float.IsNaN(f) || float.IsInfinity(f)) + { + switch (opts.NaNInfinityPolicy) + { + case NaNInfinityPolicy.NullIfNullable: useNull = true; return default; + case NaNInfinityPolicy.CoerceZero: return 0m; + default: throw new OverflowException("Cannot convert NaN/Infinity to decimal."); + } + } + return opts.CheckedNumeric ? checked((decimal)f) : (decimal)f; + } + else // double + { + var d = (double)value; + if (double.IsNaN(d) || double.IsInfinity(d)) + { + switch (opts.NaNInfinityPolicy) + { + case NaNInfinityPolicy.NullIfNullable: useNull = true; return default; + case NaNInfinityPolicy.CoerceZero: return 0m; + default: throw new OverflowException("Cannot convert NaN/Infinity to decimal."); + } + } + return opts.CheckedNumeric ? checked((decimal)d) : (decimal)d; + } + } + + // ------------------------ Helpers: Enum ------------------------ + private static object ConvertToEnum(object value, Type fromUnderlying, Type toType, Type enumType, RuntimeCastOptions opts) + { + // Nullable wrapping + bool wrapNullable = toType != enumType; + + // String → Enum + if (fromUnderlying == typeof(string)) + { + var parsed = Enum.Parse(enumType, (string)value, opts.EnumIgnoreCase); + + if (opts.EnumMustBeDefined && !Enum.IsDefined(enumType, parsed!)) + throw new InvalidCastException($"Value '{value}' is not a defined member of {enumType.Name}."); + + return wrapNullable ? BoxNullable(parsed!, enumType) : parsed!; + } + + // Numeric → Enum + if (IsNumeric(fromUnderlying)) + { + // Convert numeric to enum’s underlying integral type first (checked/unchecked handled by compiled numeric path) + var et = Enum.GetUnderlyingType(enumType); + var numConv = _numericCache.GetOrAdd((fromUnderlying, et, true), k => BuildNumericConverter(k.from, k.to, k.@checked)); + var integral = numConv(value); + + var enumObj = Enum.ToObject(enumType, integral); + if (opts.EnumMustBeDefined && !Enum.IsDefined(enumType, enumObj)) + throw new InvalidCastException($"Numeric value {integral} is not a defined member of {enumType.Name}."); + + return wrapNullable ? BoxNullable(enumObj, enumType) : enumObj; + } + + // Fallback: not supported + throw new InvalidCastException($"Cannot cast {fromUnderlying} to enum {enumType.Name}."); + } + + // ------------------------ Helpers: Guid ------------------------ + private static object ConvertToGuid(object value, Type fromUnderlying, Type toType) + { + bool wrapNullable = toType != typeof(Guid); + + if (fromUnderlying == typeof(string)) + { + if (!Guid.TryParse((string)value, out var g)) + throw new InvalidCastException($"Cannot parse '{value}' to Guid."); + return wrapNullable ? (Guid?)g : g; + } + + if (fromUnderlying == typeof(byte[])) + { + var bytes = (byte[])value; + if (bytes.Length != 16) + throw new InvalidCastException("Guid requires a 16-byte array."); + var g = new Guid(bytes); + return wrapNullable ? (Guid?)g : g; + } + + throw new InvalidCastException($"Cannot cast {fromUnderlying} to Guid."); + } + + // ------------------------ Helpers: DateTime / DateTimeOffset ------------------------ + private static object ConvertToDateTime(object value, Type fromUnderlying, Type toType, RuntimeCastOptions opts) + { + bool wrapNullable = toType != typeof(DateTime); + + if (fromUnderlying == typeof(string)) + { + if (!DateTime.TryParse((string)value, opts.Culture, opts.DateTimeStyles, out var dt)) + throw new InvalidCastException($"Cannot parse '{value}' to DateTime."); + return wrapNullable ? (DateTime?)dt : dt; + } + + if (fromUnderlying == typeof(long)) + { + // Treat as ticks + var dt = new DateTime((long)value, DateTimeKind.Unspecified); + return wrapNullable ? (DateTime?)dt : dt; + } + + if (fromUnderlying == typeof(double)) + { + // Treat as OADate if finite + var d = (double)value; + if (double.IsNaN(d) || double.IsInfinity(d)) + throw new InvalidCastException("Cannot convert NaN/Infinity to DateTime."); + var dt = DateTime.FromOADate(d); + return wrapNullable ? (DateTime?)dt : dt; + } + + throw new InvalidCastException($"Cannot cast {fromUnderlying} to DateTime."); + } + + private static object ConvertToDateTimeOffset(object value, Type fromUnderlying, Type toType, RuntimeCastOptions opts) + { + bool wrapNullable = toType != typeof(DateTimeOffset); + + if (fromUnderlying == typeof(string)) + { + if (!DateTimeOffset.TryParse((string)value, opts.Culture, opts.DateTimeStyles, out var dto)) + throw new InvalidCastException($"Cannot parse '{value}' to DateTimeOffset."); + return wrapNullable ? (DateTimeOffset?)dto : dto; + } + + if (fromUnderlying == typeof(long)) + { + // Treat as ticks since 0001-01-01 + var dto = new DateTimeOffset(new DateTime((long)value, DateTimeKind.Unspecified)); + return wrapNullable ? (DateTimeOffset?)dto : dto; + } + + if (fromUnderlying == typeof(double)) + { + var d = (double)value; + if (double.IsNaN(d) || double.IsInfinity(d)) + throw new InvalidCastException("Cannot convert NaN/Infinity to DateTimeOffset."); + var dt = DateTime.FromOADate(d); + var dto = new DateTimeOffset(dt); + return wrapNullable ? (DateTimeOffset?)dto : dto; + } + + throw new InvalidCastException($"Cannot cast {fromUnderlying} to DateTimeOffset."); + } +} diff --git a/Esiur/Data/TRU.cs b/Esiur/Data/TRU.cs index a622dac..9d80353 100644 --- a/Esiur/Data/TRU.cs +++ b/Esiur/Data/TRU.cs @@ -194,6 +194,9 @@ namespace Esiur.Data if (Identifier == TRUIdentifier.TypedList && SubTypes[0].Identifier == TRUIdentifier.UInt8) return false; + if (Identifier == TRUIdentifier.TypedResource) + return false; + return (UUID != null) || (SubTypes != null && SubTypes.Length > 0); } @@ -234,6 +237,7 @@ namespace Esiur.Data SubTypes[0].Compose().Concat(SubTypes[1].Compose()).ToArray()); case TRUIdentifier.Enum: return (TDUIdentifier.TypedEnum, UUID?.Data); + default: throw new NotImplementedException(); diff --git a/Esiur/Net/HTTP/HTTPServer.cs b/Esiur/Net/HTTP/HTTPServer.cs index affe307..f33bdc0 100644 --- a/Esiur/Net/HTTP/HTTPServer.cs +++ b/Esiur/Net/HTTP/HTTPServer.cs @@ -113,7 +113,7 @@ public class HTTPServer : NetworkServer, IResource foreach (var kv in ParameterIndex) { var g = match.Groups[kv.Key]; - args[kv.Value.Position] = DC.CastConvert(g.Value, kv.Value.ParameterType); + args[kv.Value.Position] = RuntimeCaster.Cast(g.Value, kv.Value.ParameterType); } if (SenderIndex != null) diff --git a/Esiur/Net/IIP/DistributedConnection.cs b/Esiur/Net/IIP/DistributedConnection.cs index b26a0ef..c4af607 100644 --- a/Esiur/Net/IIP/DistributedConnection.cs +++ b/Esiur/Net/IIP/DistributedConnection.cs @@ -384,7 +384,7 @@ public partial class DistributedConnection : NetworkConnection, IStore SendRequest(IIPPacketRequest.KeepAlive, now, interval) .Then(x => { - Jitter = (uint)((object[])x)[1]; + Jitter =Convert.ToUInt32(((object[])x)[1]); keepAliveTimer.Start(); }).Error(ex => { @@ -429,7 +429,8 @@ public partial class DistributedConnection : NetworkConnection, IStore if (packet.DataType == null) return offset; - //Console.WriteLine("Incoming: " + packet); + Console.WriteLine("Incoming: " + packet); + if (packet.Method == IIPPacketMethod.Notification) { var dt = packet.DataType.Value; @@ -934,7 +935,7 @@ public partial class DistributedConnection : NetworkConnection, IStore SendParams() .AddUInt8((byte)IIPAuthPacketAcknowledge.NoAuthCredentials) - .AddUInt8Array(Codec.Compose(localHeaders, this.Instance.Warehouse, this)) + .AddUInt8Array(Codec.Compose(localHeaders, Server.Instance.Warehouse, this)) .Done(); } else @@ -1321,7 +1322,7 @@ public partial class DistributedConnection : NetworkConnection, IStore SendParams() .AddUInt8((byte)IIPAuthPacketEvent.IAuthHashed) - .AddUInt8Array(Codec.Compose(args, this.Instance.Warehouse, this)) + .AddUInt8Array(Codec.Compose(args, Server.Instance.Warehouse, this)) .Done(); } diff --git a/Esiur/Net/IIP/DistributedConnectionProtocol.cs b/Esiur/Net/IIP/DistributedConnectionProtocol.cs index b7f35d7..1fee7cc 100644 --- a/Esiur/Net/IIP/DistributedConnectionProtocol.cs +++ b/Esiur/Net/IIP/DistributedConnectionProtocol.cs @@ -402,7 +402,7 @@ partial class DistributedConnection { var (size, rt) = Codec.ParseSync(dataType, Instance.Warehouse); - var resourceId = (uint)rt; + var resourceId = Convert.ToUInt32(rt); if (attachedResources.Contains(resourceId)) { @@ -475,7 +475,7 @@ partial class DistributedConnection DataDeserializer.LimitedCountListParser(data, dataType.Offset, dataType.ContentLength, Instance.Warehouse, 2); - var resourceId = (uint)args[0]; + var resourceId = Convert.ToUInt32(args[0]); var index = (byte)args[1]; Fetch(resourceId, null).Then(r => @@ -522,7 +522,7 @@ partial class DistributedConnection var (_, value) = Codec.ParseSync(dataType, Instance.Warehouse); - var resourceId = (uint)value; + var resourceId = Convert.ToUInt32(value); Instance.Warehouse.GetById(resourceId).Then((res) => { @@ -579,7 +579,8 @@ partial class DistributedConnection DataDeserializer.LimitedCountListParser(data, dataType.Offset, dataType.ContentLength, Instance.Warehouse, 2); - var resourceId = (uint)args[0]; + var resourceId = Convert.ToUInt32(args[0]); + var age = (ulong)args[1]; Instance.Warehouse.GetById(resourceId).Then((res) => @@ -635,7 +636,7 @@ partial class DistributedConnection var (_, value) = Codec.ParseSync(dataType, Instance.Warehouse); - var resourceId = (uint)value; + var resourceId = Convert.ToUInt32(value); Instance.Warehouse.GetById(resourceId).Then((res) => { @@ -727,7 +728,7 @@ partial class DistributedConnection var (_, value) = Codec.ParseSync(dataType, Instance.Warehouse); - var resourceId = (uint)value; + var resourceId = Convert.ToUInt32(value); Instance.Warehouse.GetById(resourceId).Then(r => { @@ -757,7 +758,9 @@ partial class DistributedConnection var (offset, length, args) = DataDeserializer.LimitedCountListParser(data, dataType.Offset, dataType.ContentLength, Instance.Warehouse); - var resourceId = (uint)args[0]; + + var resourceId = Convert.ToUInt32(args[0]); + var name = (string)args[1]; if (name.Contains("/")) @@ -874,7 +877,7 @@ partial class DistributedConnection var (_, value) = Codec.ParseSync(dataType, Instance.Warehouse); - var resourceId = (uint)value; + var resourceId = Convert.ToUInt32(value); Instance.Warehouse.GetById(resourceId).Then((r) => { @@ -1120,7 +1123,7 @@ partial class DistributedConnection var (offset, length, args) = DataDeserializer.LimitedCountListParser(data, dataType.Offset, dataType.ContentLength, Instance.Warehouse, 2); - var resourceId = (uint)args[0]; + var resourceId = Convert.ToUInt32(args[0]); var index = (byte)args[1]; Instance.Warehouse.GetById(resourceId).Then((r) => @@ -1239,7 +1242,7 @@ partial class DistributedConnection for (byte i = 0; i < pis.Length - 1; i++) { if (arguments.ContainsKey(i)) - args[i] = DC.CastConvert(arguments[i], pis[i].ParameterType); + args[i] = RuntimeCaster.Cast(arguments[i], pis[i].ParameterType); else if (ft.Arguments[i].Type.Nullable) args[i] = null; else @@ -1256,7 +1259,7 @@ partial class DistributedConnection for (byte i = 0; i < pis.Length - 1; i++) { if (arguments.ContainsKey(i)) - args[i] = DC.CastConvert(arguments[i], pis[i].ParameterType); + args[i] = RuntimeCaster.Cast(arguments[i], pis[i].ParameterType); else if (ft.Arguments[i].Type.Nullable) args[i] = null; else @@ -1272,7 +1275,7 @@ partial class DistributedConnection for (byte i = 0; i < pis.Length; i++) { if (arguments.ContainsKey(i)) - args[i] = DC.CastConvert(arguments[i], pis[i].ParameterType); + args[i] = RuntimeCaster.Cast(arguments[i], pis[i].ParameterType); else if (ft.Arguments[i].Type.Nullable) //Nullable.GetUnderlyingType(pis[i].ParameterType) != null) args[i] = null; else @@ -1377,7 +1380,7 @@ partial class DistributedConnection var (offset, length, args) = DataDeserializer.LimitedCountListParser(data, dataType.Offset, dataType.ContentLength, Instance.Warehouse); - var resourceId = (uint)args[0]; + var resourceId = Convert.ToUInt32(args[0]); var index = (byte)args[1]; Instance.Warehouse.GetById(resourceId).Then((r) => @@ -1436,7 +1439,7 @@ partial class DistributedConnection var (offset, length, args) = DataDeserializer.LimitedCountListParser(data, dataType.Offset, dataType.ContentLength, Instance.Warehouse); - var resourceId = (uint)args[0]; + var resourceId = Convert.ToUInt32(args[0]); var index = (byte)args[1]; Instance.Warehouse.GetById(resourceId).Then((r) => @@ -1576,7 +1579,7 @@ partial class DistributedConnection else { // cast new value type to property type - value = DC.CastConvert(value, pi.PropertyType); + value = RuntimeCaster.Cast(value, pi.PropertyType); } try @@ -1601,7 +1604,7 @@ partial class DistributedConnection else { // cast new value type to property type - parsed = DC.CastConvert(parsed, pi.PropertyType); + parsed = RuntimeCaster.Cast(parsed, pi.PropertyType); } try @@ -1805,7 +1808,7 @@ partial class DistributedConnection // ClassId, Age, Link, Hops, PropertyValue[] var args = (object[])result; var classId = (UUID)args[0]; - var age = (ulong)args[1]; + var age = Convert.ToUInt64(args[1]); var link = (string)args[2]; var hops = (byte)args[3]; var pvData = (byte[])args[4]; @@ -1820,7 +1823,7 @@ partial class DistributedConnection if (template?.DefinedType != null && template.IsWrapper) dr = Activator.CreateInstance(template.DefinedType, this, id, (ulong)args[1], (string)args[2]) as DistributedResource; else - dr = new DistributedResource(this, id, (ulong)args[1], (string)args[2]); + dr = new DistributedResource(this, id, Convert.ToUInt64( args[1]), (string)args[2]); } else { @@ -2063,7 +2066,7 @@ partial class DistributedConnection dataType.ContentLength, Instance.Warehouse); var peerTime = (DateTime)args[0]; - var interval = (uint)args[1]; + var interval = Convert.ToUInt32(args[1]); uint jitter = 0; diff --git a/Esiur/Resource/Instance.cs b/Esiur/Resource/Instance.cs index 1cc4386..056e28a 100644 --- a/Esiur/Resource/Instance.cs +++ b/Esiur/Resource/Instance.cs @@ -184,7 +184,7 @@ public class Instance if (at != null) if (at.PropertyInfo.CanWrite) - at.PropertyInfo.SetValue(res, DC.CastConvert(kv.Value, at.PropertyInfo.PropertyType)); + at.PropertyInfo.SetValue(res, RuntimeCaster.Cast(kv.Value, at.PropertyInfo.PropertyType)); } } @@ -363,7 +363,7 @@ public class Instance { loading = true; - pt.PropertyInfo.SetValue(res, DC.CastConvert(value, pt.PropertyInfo.PropertyType)); + pt.PropertyInfo.SetValue(res, RuntimeCaster.Cast(value, pt.PropertyInfo.PropertyType)); } catch (Exception ex) {