using AyCode.Core.Compression; using AyCode.Core.Helpers; using AyCode.Core.Serializers.Expressions; using System.Buffers; using System.Collections; using System.Collections.Concurrent; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using AyCode.Core.Serializers.Jsons; using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Binaries; /// /// High-performance binary serializer optimized for speed and memory efficiency. /// Features: /// - VarInt encoding for compact integers (MessagePack-style) /// - String interning for repeated strings /// - Property name table for fast lookup /// - Reference handling for circular/shared references /// - Optional metadata for schema evolution /// - Optimized buffer management with ArrayPool /// - Zero-allocation hot paths using Span and MemoryMarshal /// - Automatic Expression to AcExpressionNode conversion /// - Generic TOutput for output strategy selection (ArrayBinaryOutput / BufferWriterBinaryOutput) /// - Buffer-in-context: _buffer/_position owned by context for zero virtual dispatch on hot path /// public static partial class AcBinarySerializer { // Pre-computed UTF8 encoder for string operations private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); private static readonly Type StringType = typeof(string); private static readonly Type GuidType = typeof(Guid); private static readonly Type DateTimeOffsetType = typeof(DateTimeOffset); private static readonly Type TimeSpanType = typeof(TimeSpan); private static readonly Type IntType = typeof(int); private static readonly Type LongType = typeof(long); private static readonly Type FloatType = typeof(float); private static readonly Type DoubleType = typeof(double); private static readonly Type DecimalType = typeof(decimal); private static readonly Type BoolType = typeof(bool); private static readonly Type DateTimeType = typeof(DateTime); #region Public API #if DEBUG /// /// DEBUG ONLY: Analyzes which string properties have repeated values that would benefit from interning. /// Returns a dictionary where key is "TypeName.PropertyName" and value is the occurrence count. /// Only properties with count > 1 are good candidates for [StringIntern] attribute. /// /// The object graph to analyze. /// Serializer options (UseStringInterning should be enabled). /// Dictionary of property paths to their string occurrence counts. public static Dictionary> AnalyzeStringInternCandidates(T value, AcBinarySerializerOptions? options = null) { ArgumentNullException.ThrowIfNull(value); options ??= AcBinarySerializerOptions.Default; // For analysis, use the provided reference handling mode var analysisOptions = new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.All, MinStringInternLength = options.MinStringInternLength, MaxStringInternLength = options.MaxStringInternLength, ReferenceHandling = options.ReferenceHandling }; var result = new Dictionary>(); var runtimeType = value.GetType(); // Create context without pooling (we need to set up callback) using var context = new BinarySerializationContext(analysisOptions); context.Output = new ArrayBinaryOutput(4096); context.OutputInitialized = true; context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); // Set up tracking callbacks context.OnStringInterned = (propertyPath, stringValue) => { propertyPath ??= "(unknown)"; if (!result.TryGetValue(stringValue, out var properties)) { properties = new Dictionary(); result[stringValue] = properties; } properties[propertyPath] = properties.GetValueOrDefault(propertyPath) + 1; }; // Run serialization to trigger callbacks context.WriteHeader(); WriteValue(value, runtimeType, context, 0); return result; } public static StringBuilder GetAnalyzeStringInternCandidatesLog(T value, AcBinarySerializerOptions? options = null) { var sb = new StringBuilder(); options ??= AcBinarySerializerOptions.Default; var analysis = AnalyzeStringInternCandidates(value, options); // Transform: stringValue → properties TO propertyPath → (stringValue, count) var propertyStats = new Dictionary>(); foreach (var (stringValue, properties) in analysis) { var byteLength = Encoding.UTF8.GetByteCount(stringValue); foreach (var (propPath, count) in properties) { if (!propertyStats.TryGetValue(propPath, out var list)) { list = []; propertyStats[propPath] = list; } list.Add((stringValue, count, byteLength)); } } var refMode = options.ReferenceHandling; // Header sb.AppendLine("+==============================================================================+"); sb.AppendLine($"| STRING INTERN ANALYSIS REPORT (RefMode: {refMode,-12}) |"); sb.AppendLine("+==============================================================================+"); sb.AppendLine(); // Global summary var totalStrings = analysis.Values.Sum(p => p.Values.Sum()); var uniqueStrings = analysis.Count; var repeatedStrings = analysis.Count(kv => kv.Value.Values.Sum() > 1); sb.AppendLine("+-----------------------------------------------------------------------------+"); sb.AppendLine("| STRING SUMMARY |"); sb.AppendLine("+-----------------------------------------------------------------------------+"); sb.AppendLine($"| Total string occurrences: {totalStrings,-10} Unique strings: {uniqueStrings,-10} Repeated: {repeatedStrings,-8} |"); sb.AppendLine("+-----------------------------------------------------------------------------+"); sb.AppendLine(); // Property-focused table // Calculate stats for each property first (for sorting by RepeatSum%) var propertyStatsCalculated = propertyStats.Select(kv => { var propPath = kv.Key; var strings = kv.Value; var total = strings.Sum(s => s.Count); var unique = strings.Count; var repeated = strings.Count(s => s.Count > 1); var repeatSum = strings.Where(s => s.Count > 1).Sum(s => s.Count); // Sum of occurrences of repeated strings var repeatSumPct = total > 0 ? repeatSum * 100.0 / total : 0; // Calculate savings var totalBytes = strings.Sum(s => s.Count * s.ByteLength); var uniqueBytes = strings.Sum(s => s.ByteLength); var indexBytes = total * 2; var savings = totalBytes - (uniqueBytes + indexBytes); return (propPath, strings, total, unique, repeated, repeatSum, repeatSumPct, savings); }).ToList(); sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+"); sb.AppendLine("| Property | Total | RepSum | Unique | Repeated| RepSum % | Savings | Recommend |"); sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+"); foreach (var stat in propertyStatsCalculated.OrderByDescending(x => x.repeatSumPct)) { var savingsStr = stat.savings > 0 ? $"+{stat.savings:N0}B" : $"{stat.savings:N0}B"; var recommend = stat.savings > 100 ? "[INTERN]" : stat.savings > 0 ? " maybe " : " skip "; var propDisplay = stat.propPath.Length > 30 ? stat.propPath[..27] + "..." : stat.propPath; sb.AppendLine($"| {propDisplay,-30} | {stat.total,5} | {stat.repeatSum,7} | {stat.unique,6} | {stat.repeated,7} | {stat.repeatSumPct,8:F1}% | {savingsStr,8} | {recommend,-11} |"); } sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+"); sb.AppendLine(); // Detailed property breakdown (only for properties with significant savings) sb.AppendLine("+-----------------------------------------------------------------------------+"); sb.AppendLine("| DETAILED BREAKDOWN (properties with savings > 100 bytes) |"); sb.AppendLine("+-----------------------------------------------------------------------------+"); foreach (var stat in propertyStatsCalculated .Where(x => x.savings > 100) .OrderByDescending(x => x.repeatSumPct)) { sb.AppendLine(); sb.AppendLine($" {stat.propPath} (RepSum: {stat.repeatSum}/{stat.total} = {stat.repeatSumPct:F1}%):"); foreach (var (strVal, count, _) in stat.strings.OrderByDescending(s => s.Count).Take(10)) { var preview = strVal.Length > 40 ? strVal[..37] + "..." : strVal; var marker = count > 1 ? ">" : " "; sb.AppendLine($" {marker} [{count,4}x] \"{preview}\""); } if (stat.strings.Count > 10) { sb.AppendLine($" ... and {stat.strings.Count - 10} more unique values"); } } sb.AppendLine(); sb.AppendLine("==============================================================================="); sb.AppendLine("Legend: Total=all occurrences, RepSum=sum of repeated string occurrences"); sb.AppendLine(" Unique=distinct values, Repeated=count of values appearing 2+ times"); sb.AppendLine(" RepSum%=percentage of occurrences that are repeated (higher=better for intern)"); sb.AppendLine(" Savings=estimated bytes saved with interning (positive=good)"); sb.AppendLine(" > = repeated string (benefits from interning)"); return sb; } #endif /// /// Serialize object to binary with default options. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte[] Serialize(T value) => Serialize(value, AcBinarySerializerOptions.Default); /// /// Serialize object to an IBufferWriter with default options. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Serialize(T value, IBufferWriter writer) => Serialize(value, writer, AcBinarySerializerOptions.Default); /// /// Serialize object to binary with specified options. /// Uses ArrayBinaryOutput for byte[] result path. /// public static byte[] Serialize(T value, AcBinarySerializerOptions options) { if (value == null) { return [BinaryTypeCode.Null]; } var runtimeType = value.GetType(); // Handle IQueryable types - convert to AcExpressionNode (serialize the Expression) object actualValue = value; if (value is IQueryable queryable) { actualValue = AcSerializerCommon.QueryableToNode(queryable); runtimeType = typeof(AcExpressionNode); } // Handle Expression types - convert to AcExpressionNode else if (AcSerializerCommon.IsExpressionType(runtimeType)) { actualValue = AcExpressionConverter.ToNode((Expression)(object)value); runtimeType = typeof(AcExpressionNode); } var context = BinarySerializationContextPool.Get(options); if (!context.OutputInitialized) { context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity); context.OutputInitialized = true; } context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); try { ScanForDuplicates(actualValue, runtimeType, context); context.WriteHeader(); WriteValue(actualValue, runtimeType, context, 0); // Apply compression if enabled - compress directly from buffer span (1 allocation) if (options.UseCompression != Lz4CompressionMode.None) { return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression); } // No compression - single allocation for result return context.Output.ToArray(context._buffer, context._position); } finally { if (options.UseAsync) BinarySerializationContextPool.ReturnAsync(context); else BinarySerializationContextPool.Return(context); } } /// /// Serialize object to an IBufferWriter for zero-copy scenarios. /// Uses BufferWriterBinaryOutput — writes directly to the caller's buffer. /// Note: Compression is applied if enabled in options. /// public static void Serialize(T value, IBufferWriter writer, AcBinarySerializerOptions options) { if (value == null) { var span = writer.GetSpan(1); span[0] = BinaryTypeCode.Null; writer.Advance(1); return; } var runtimeType = value.GetType(); // Handle IQueryable types - convert to AcExpressionNode (serialize the Expression) object actualValue = value; if (value is IQueryable queryable) { actualValue = AcSerializerCommon.QueryableToNode(queryable); runtimeType = typeof(AcExpressionNode); } // Handle Expression types - convert to AcExpressionNode else if (AcSerializerCommon.IsExpressionType(runtimeType)) { actualValue = AcExpressionConverter.ToNode((Expression)(object)value); runtimeType = typeof(AcExpressionNode); } var context = BinarySerializationContextPool.Get(options); context.Output = new BufferWriterBinaryOutput(writer); context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); try { ScanForDuplicates(actualValue, runtimeType, context); context.WriteHeader(); WriteValue(actualValue, runtimeType, context, 0); // Apply compression if enabled if (options.UseCompression != Lz4CompressionMode.None) { // For compression with BufferWriter, we need to flush first then compress // This path is less common — compression typically uses byte[] path context.Output.Flush(context._buffer, context._position); // Compression with IBufferWriter requires intermediate buffer // Fall back to ArrayBinaryOutput path for compression throw new NotSupportedException( "Compression is not supported with IBufferWriter output. " + "Use the byte[] overload or disable compression."); } context.Output.Flush(context._buffer, context._position); } finally { context.Output = default; if (options.UseAsync) BinarySerializationContextPool.ReturnAsync(context); else BinarySerializationContextPool.Return(context); } } /// /// Get the serialized size without allocating the final array. /// Useful for pre-allocating buffers. /// public static int GetSerializedSize(T value, AcBinarySerializerOptions options) { if (value == null) return 1; var runtimeType = value.GetType(); var context = BinarySerializationContextPool.Get(options); if (!context.OutputInitialized) { context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity); context.OutputInitialized = true; } context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); try { ScanForDuplicates(value, runtimeType, context); context.WriteHeader(); WriteValue(value, runtimeType, context, 0); return context.Position; } finally { if (options.UseAsync) BinarySerializationContextPool.ReturnAsync(context); else BinarySerializationContextPool.Return(context); } } /// /// Serialize object and keep the pooled buffer for zero-copy consumers. /// Caller must dispose the returned result to release the buffer. /// Note: Compression is applied if enabled in options, result will be immutable (not pooled). /// public static BinarySerializationResult SerializeToPooledBuffer(T value, AcBinarySerializerOptions options) { if (value == null) { return BinarySerializationResult.FromImmutable([BinaryTypeCode.Null]); } var runtimeType = value.GetType(); var context = BinarySerializationContextPool.Get(options); if (!context.OutputInitialized) { context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity); context.OutputInitialized = true; } context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); try { ScanForDuplicates(value, runtimeType, context); context.WriteHeader(); WriteValue(value, runtimeType, context, 0); // If compression enabled, compress directly from buffer span (1 allocation) if (options.UseCompression != Lz4CompressionMode.None) { var compressed = Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression); return BinarySerializationResult.FromImmutable(compressed); } return context.Output.DetachResult(context._buffer, context._position); } finally { if (options.UseAsync) BinarySerializationContextPool.ReturnAsync(context); else BinarySerializationContextPool.Return(context); } } #endregion #region Value Writing private static void WriteValue(object? value, Type type, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase { if (value == null) { context.WriteByte(BinaryTypeCode.Null); return; } // Try writing as primitive first if (TryWritePrimitive(value, type, context)) return; WriteValueNonPrimitive(value, type, context, depth); } /// /// Writes a non-primitive value (collection, dictionary, byte[], or complex object). /// Skips null check and TryWritePrimitive — caller guarantees value is non-null and not a primitive type. /// Called from WritePropertyOrSkip default case (PropertyAccessorType.Object) and WriteValue fallback. /// private static void WriteValueNonPrimitive(object value, Type type, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase { // Nullable where T is a value type: boxed value may be a primitive. // Only Nullable can be a value type in the Object accessor path. if (type.IsValueType) { if (TryWritePrimitive(value, value.GetType(), context)) return; } if (depth > context.MaxDepth) { context.WriteByte(BinaryTypeCode.Null); return; } // Handle byte arrays specially (value-like, no reference tracking) if (value is byte[] byteArray) { WriteByteArray(byteArray, context); return; } // Handle dictionaries if (value is IDictionary dictionary) { WriteDictionary(dictionary, context, depth); return; } // Get wrapper once — used by both WriteArray and WriteObject var wrapper = context.GetWrapper(type); // Handle collections/arrays if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) { WriteArray(enumerable, wrapper, context, depth); return; } // Handle complex objects with single-pass reference tracking WriteObject(value, wrapper, context, depth, isNested: depth > 0); } /// /// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache). /// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects. /// private static void WriteValueNonPrimitiveWithWrapper(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase { var type = wrapper.Metadata.SourceType; // Nullable where T is a value type: boxed value may be a primitive. if (type.IsValueType) { if (TryWritePrimitive(value, value.GetType(), context)) return; } if (depth > context.MaxDepth) { context.WriteByte(BinaryTypeCode.Null); return; } // Handle byte arrays specially (value-like, no reference tracking) if (value is byte[] byteArray) { WriteByteArray(byteArray, context); return; } // Handle dictionaries if (value is IDictionary dictionary) { WriteDictionary(dictionary, context, depth); return; } // Handle collections/arrays if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) { WriteArray(enumerable, wrapper, context, depth); return; } // Handle complex objects with single-pass reference tracking WriteObject(value, wrapper, context, depth, isNested: depth > 0); } /// /// Optimized primitive writer using TypeCode dispatch. /// Avoids Nullable.GetUnderlyingType in hot path by using cached type info. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { // Fast path: check TypeCode first (handles most primitives) var typeCode = Type.GetTypeCode(type); switch (typeCode) { case TypeCode.Int32: WriteInt32((int)value, context); return true; case TypeCode.Int64: WriteInt64((long)value, context); return true; case TypeCode.Boolean: context.WriteByte((bool)value ? BinaryTypeCode.True : BinaryTypeCode.False); return true; case TypeCode.Double: WriteFloat64Unsafe((double)value, context); return true; case TypeCode.String: WriteString((string)value, context); return true; case TypeCode.Single: WriteFloat32Unsafe((float)value, context); return true; case TypeCode.Decimal: WriteDecimalUnsafe((decimal)value, context); return true; case TypeCode.DateTime: WriteDateTimeUnsafe((DateTime)value, context); return true; case TypeCode.Byte: context.WriteByte(BinaryTypeCode.UInt8); context.WriteByte((byte)value); return true; case TypeCode.Int16: WriteInt16Unsafe((short)value, context); return true; case TypeCode.UInt16: WriteUInt16Unsafe((ushort)value, context); return true; case TypeCode.UInt32: WriteUInt32((uint)value, context); return true; case TypeCode.UInt64: WriteUInt64((ulong)value, context); return true; case TypeCode.SByte: context.WriteByte(BinaryTypeCode.Int8); context.WriteByte(unchecked((byte)(sbyte)value)); return true; case TypeCode.Char: WriteCharUnsafe((char)value, context); return true; } // Handle special types by reference comparison (faster than type equality) if (ReferenceEquals(type, GuidType)) { WriteGuidUnsafe((Guid)value, context); return true; } if (ReferenceEquals(type, DateTimeOffsetType)) { WriteDateTimeOffsetUnsafe((DateTimeOffset)value, context); return true; } if (ReferenceEquals(type, TimeSpanType)) { WriteTimeSpanUnsafe((TimeSpan)value, context); return true; } if (type.IsEnum) { WriteEnum(value, context); return true; } // Handle nullable types - use cached check instead of GetUnderlyingType // For nullable, value is already unwrapped when boxed, so we can use value.GetType() if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { // When boxed, nullable value types are unwrapped to their underlying type // So we can just call TryWritePrimitive with the actual runtime type return TryWritePrimitive(value, value.GetType(), context); } return false; } #endregion #region Optimized Primitive Writers using MemoryMarshal [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteInt32(int value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny)) { context.WriteByte(tiny); return; } context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteInt64(long value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { if (value >= int.MinValue && value <= int.MaxValue) { WriteInt32((int)value, context); return; } context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong(value); } /// /// Optimized float64 writer using batched write. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteFloat64Unsafe(double value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, value); } /// /// Optimized float32 writer using batched write. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteFloat32Unsafe(float value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteTypeCodeAndRaw(BinaryTypeCode.Float32, value); } /// /// Optimized decimal writer using direct memory copy of bits. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteDecimalUnsafe(decimal value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits(value); } /// /// Optimized DateTime writer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteDateTimeUnsafe(DateTime value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits(value); } /// /// Optimized Guid writer using direct memory copy. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteGuidUnsafe(Guid value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits(value); } /// /// Optimized DateTimeOffset writer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteDateTimeOffsetUnsafe(DateTimeOffset value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteByte(BinaryTypeCode.DateTimeOffset); context.WriteDateTimeOffsetBits(value); } /// /// Optimized TimeSpan writer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteTimeSpanUnsafe(TimeSpan value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, value.Ticks); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteInt16Unsafe(short value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteUInt16Unsafe(ushort value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteUInt32(uint value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteByte(BinaryTypeCode.UInt32); context.WriteVarUInt(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteUInt64(ulong value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteByte(BinaryTypeCode.UInt64); context.WriteVarULong(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteCharUnsafe(char value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteByte(BinaryTypeCode.Char); context.WriteRaw(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteEnum(object value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { // Use direct unboxing instead of Convert.ToInt32 to avoid NumberFormatInfo overhead var intValue = GetEnumAsInt32Fast(value); if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny)) { context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(tiny); return; } context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(intValue); } /// /// Fast enum to int conversion avoiding Convert.ToInt32 overhead. /// Handles all common enum underlying types. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetEnumAsInt32Fast(object enumValue) { var type = enumValue.GetType(); var underlyingType = type.GetEnumUnderlyingType(); if (ReferenceEquals(underlyingType, IntType)) return (int)enumValue; if (ReferenceEquals(underlyingType, typeof(byte))) return (byte)enumValue; if (ReferenceEquals(underlyingType, typeof(sbyte))) return (sbyte)enumValue; if (ReferenceEquals(underlyingType, typeof(short))) return (short)enumValue; if (ReferenceEquals(underlyingType, typeof(ushort))) return (ushort)enumValue; if (ReferenceEquals(underlyingType, typeof(uint))) return unchecked((int)(uint)enumValue); // Fallback for rare cases (long, ulong) return Convert.ToInt32(enumValue); } /// /// Optimized string writer with FixStr for short strings. /// Marker-based interning: write String marker, rewrite to StringInternFirst at end if needed. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteString(string value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { if (value.Length == 0) { context.WriteByte(BinaryTypeCode.StringEmpty); return; } if (context.UseStringInterning && context.IsValidForInterningString(value.Length))// && context.CurrentProperty!.UseStringPropertyInterning(context.Options.UseStringInterning)) { ref var interEntry = ref context.GetInternedStringEntry(value, out bool found); if (found && interEntry.CacheIndex >= 0) { if (interEntry.IsFirstWrite) { // 1st serialize occurrence of a cached string - write StringInternFirst + cacheIndex + data interEntry.IsFirstWrite = false; context.WriteByte(BinaryTypeCode.StringInternFirst); context.WriteVarUInt((uint)interEntry.CacheIndex); context.WriteStringUtf8(value); } else { // 2+ serialize occurrence: write index reference context.WriteByte(BinaryTypeCode.StringInterned); context.WriteVarUInt((uint)interEntry.CacheIndex); } return; } // CacheIndex < 0 or not found → single occurrence, fall through to FixStr/String path #if DEBUG context.OnStringInterned?.Invoke( context.CurrentProperty != null ? $"{context.CurrentProperty.DeclaringType.Name}.{context.CurrentProperty.Name}" : null, value); #endif } // Fast path for short strings: check length first (cheap), then ASCII // FixStr encodes type+length in single byte for strings <= 31 chars var length = value.Length; if (length <= BinaryTypeCode.FixStrMaxLength) { // For short strings, use direct ASCII copy (avoids double validation) context.WriteFixStrDirect(value); return; } // Long strings - standard encoding context.WriteByte(BinaryTypeCode.String); context.WriteStringUtf8(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WriteByteArray(byte[] value, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { context.WriteByte(BinaryTypeCode.ByteArray); context.WriteVarUInt((uint)value.Length); context.WriteBytes(value); } #endregion #region Complex Type Writers private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, bool isNested = false) where TOutput : struct, IBinaryOutputBase { var metadata = wrapper.Metadata; // Wire format: // - UseMetadata=false: [Object][props...] // - UseMetadata=true, első: [ObjectWithMetadata][propNameHash (4b)][propCount (VarUInt)][hash0..N][props...] // - UseMetadata=true, ismételt: [ObjectWithMetadata][propNameHash (4b)][props...] // ObjectRef: [ObjectRef][cacheIndex] // UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking) var isFirstMetadataOccurrence = false; if (context.UseMetadata) { isFirstMetadataOccurrence = BinarySerializationContext.RegisterMetadataType(wrapper); } // Reference handling: lookup entry from scan pass, check IsFirstWrite var cachedObjectCacheIndex = -1; // -1 = not cached, 0+ = cache index for first write if (context.UseTypeReferenceHandling(metadata)) { // Lookup by Id (IId types) or by object identity hash (non-IId types) // Both use IdAccessorType.Int32 - for non-IId, RefIdGetterInt32 returns RuntimeHelpers.GetHashCode switch (metadata.IdAccessorType) { case IdAccessorType.Int32: { var id = wrapper.RefIdGetterInt32!(value); // For IId: skip default Id (0). For non-IId (hash): hash is never 0 for valid objects if ((!metadata.IsIId || id != 0) && wrapper.TryGetEntryInt32(id, out var slotIndex)) { ref var entry = ref wrapper.GetEntryRefInt32(slotIndex); if (entry.CacheIndex >= 0) { if (entry.IsFirstWrite) { entry.IsFirstWrite = false; cachedObjectCacheIndex = entry.CacheIndex; } else { // 2+ occurrence → write ObjectRef context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteVarUInt((uint)entry.CacheIndex); return; } } } break; } case IdAccessorType.Int64: { var id = wrapper.RefIdGetterInt64!(value); if (id != 0 && wrapper.TryGetEntryInt64(id, out var slotIndex)) { ref var entry = ref wrapper.GetEntryRefInt64(slotIndex); if (entry.CacheIndex >= 0) { if (entry.IsFirstWrite) { entry.IsFirstWrite = false; cachedObjectCacheIndex = entry.CacheIndex; } else { context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteVarUInt((uint)entry.CacheIndex); return; } } } break; } case IdAccessorType.Guid: { var id = wrapper.RefIdGetterGuid!(value); if (id != Guid.Empty && wrapper.TryGetEntryGuid(id, out var slotIndex)) { ref var entry = ref wrapper.GetEntryRefGuid(slotIndex); if (entry.CacheIndex >= 0) { if (entry.IsFirstWrite) { entry.IsFirstWrite = false; cachedObjectCacheIndex = entry.CacheIndex; } else { context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteVarUInt((uint)entry.CacheIndex); return; } } } break; } } } // Marker kiírása: // - Cached object first occurrence: ObjectRefFirst/ObjectWithMetadataRefFirst + cacheIndex // - Non-cached: Object/ObjectWithMetadata if (context.UseMetadata) { if (cachedObjectCacheIndex >= 0) { context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst); context.WriteVarUInt((uint)cachedObjectCacheIndex); } else { context.WriteByte(BinaryTypeCode.ObjectWithMetadata); } context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence); } else { if (cachedObjectCacheIndex >= 0) { context.WriteByte(BinaryTypeCode.ObjectRefFirst); context.WriteVarUInt((uint)cachedObjectCacheIndex); } else { context.WriteByte(BinaryTypeCode.Object); } } // Write all properties (startIndex=0, including Id for IId types) var nextDepth = depth + 1; var properties = metadata.Properties; var propCount = properties.Length; var hasPropertyFilter = context.HasPropertyFilter; if (!context.UseMetadata) { // Markerless loop: no extra branching per property for the common case. // Properties with ExpectedTypeCode write raw values (no type marker, no skip). // Properties without ExpectedTypeCode (bool, enum, string, object) use the standard path. for (var i = 0; i < propCount; i++) { var prop = properties[i]; //context.CurrentProperty = prop; if (prop.ExpectedTypeCode.HasValue) { WritePropertyMarkerless(value, prop, context); } else if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop)) { context.WriteByte(BinaryTypeCode.PropertySkip); } else { WritePropertyOrSkip(value, prop, wrapper, context, nextDepth); } } } else { // UseMetadata=true loop — UNCHANGED, zero extra overhead for (var i = 0; i < propCount; i++) { var prop = properties[i]; //context.CurrentProperty = prop; if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop)) { context.WriteByte(BinaryTypeCode.PropertySkip); continue; } WritePropertyOrSkip(value, prop, wrapper, context, nextDepth); } } } /// /// Checks if a property value is null or default without boxing for value types. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsPropertyDefaultOrNull(object obj, BinaryPropertyAccessor prop) { switch (prop.AccessorType) { case PropertyAccessorType.Int32: return prop.GetInt32(obj) == 0; case PropertyAccessorType.Int64: return prop.GetInt64(obj) == 0L; case PropertyAccessorType.Boolean: return !prop.GetBoolean(obj); case PropertyAccessorType.Double: return prop.GetDouble(obj) == 0.0; case PropertyAccessorType.Single: return prop.GetSingle(obj) == 0f; case PropertyAccessorType.Decimal: return prop.GetDecimal(obj) == 0m; case PropertyAccessorType.Byte: return prop.GetByte(obj) == 0; case PropertyAccessorType.Int16: return prop.GetInt16(obj) == 0; case PropertyAccessorType.UInt16: return prop.GetUInt16(obj) == 0; case PropertyAccessorType.UInt32: return prop.GetUInt32(obj) == 0; case PropertyAccessorType.UInt64: return prop.GetUInt64(obj) == 0; case PropertyAccessorType.Guid: return prop.GetGuid(obj) == Guid.Empty; case PropertyAccessorType.Enum: return prop.GetEnumAsInt32(obj) == 0; case PropertyAccessorType.DateTime: // DateTime default is not typically skipped return false; default: // Object type - use regular getter var value = prop.GetValue(obj); if (value == null) return true; if (prop.PropertyTypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value); return false; } } /// /// Writes a property value using typed getters to avoid boxing. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WritePropertyValue(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase { switch (prop.AccessorType) { case PropertyAccessorType.Int32: WriteInt32(prop.GetInt32(obj), context); return; case PropertyAccessorType.Int64: WriteInt64(prop.GetInt64(obj), context); return; case PropertyAccessorType.Boolean: context.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False); return; case PropertyAccessorType.Double: WriteFloat64Unsafe(prop.GetDouble(obj), context); return; case PropertyAccessorType.Single: WriteFloat32Unsafe(prop.GetSingle(obj), context); return; case PropertyAccessorType.Decimal: WriteDecimalUnsafe(prop.GetDecimal(obj), context); return; case PropertyAccessorType.DateTime: WriteDateTimeUnsafe(prop.GetDateTime(obj), context); return; case PropertyAccessorType.Byte: context.WriteByte(BinaryTypeCode.UInt8); context.WriteByte(prop.GetByte(obj)); return; case PropertyAccessorType.Int16: WriteInt16Unsafe(prop.GetInt16(obj), context); return; case PropertyAccessorType.UInt16: WriteUInt16Unsafe(prop.GetUInt16(obj), context); return; case PropertyAccessorType.UInt32: WriteUInt32(prop.GetUInt32(obj), context); return; case PropertyAccessorType.UInt64: WriteUInt64(prop.GetUInt64(obj), context); return; case PropertyAccessorType.Guid: WriteGuidUnsafe(prop.GetGuid(obj), context); return; case PropertyAccessorType.Enum: var enumValue = prop.GetEnumAsInt32(obj); if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny)) { context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(tiny); } else { context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(enumValue); } return; case PropertyAccessorType.String: { // Fast path: typed getter, no boxing, no Type.GetTypeCode() call var strValue = prop.GetString(obj); if (strValue != null) WriteString(strValue, context); else context.WriteByte(BinaryTypeCode.Null); return; } default: // Fallback to object getter for reference types var value = prop.GetValue(obj); WriteValue(value, prop.PropertyType, context, depth); return; } } /// /// Writes a property value OR a skip marker if the value is default/null. /// Single-pass optimization: checks default + writes value in one operation. /// Avoids double getter calls. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WritePropertyOrSkip(object obj, BinaryPropertyAccessor prop, TypeMetadataWrapper parentWrapper, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase { switch (prop.AccessorType) { case PropertyAccessorType.Int32: { int value = prop.GetInt32(obj); if (value == 0) context.WriteByte(BinaryTypeCode.PropertySkip); else WriteInt32(value, context); return; } case PropertyAccessorType.Int64: { long value = prop.GetInt64(obj); if (value == 0L) context.WriteByte(BinaryTypeCode.PropertySkip); else WriteInt64(value, context); return; } case PropertyAccessorType.Boolean: { bool value = prop.GetBoolean(obj); if (!value) context.WriteByte(BinaryTypeCode.PropertySkip); else context.WriteByte(BinaryTypeCode.True); return; } case PropertyAccessorType.Double: { double value = prop.GetDouble(obj); if (value == 0.0) context.WriteByte(BinaryTypeCode.PropertySkip); else WriteFloat64Unsafe(value, context); return; } case PropertyAccessorType.Single: { float value = prop.GetSingle(obj); if (value == 0f) context.WriteByte(BinaryTypeCode.PropertySkip); else WriteFloat32Unsafe(value, context); return; } case PropertyAccessorType.Decimal: { decimal value = prop.GetDecimal(obj); if (value == 0m) context.WriteByte(BinaryTypeCode.PropertySkip); else WriteDecimalUnsafe(value, context); return; } case PropertyAccessorType.DateTime: { DateTime value = prop.GetDateTime(obj); // DateTime always written (no default skip) WriteDateTimeUnsafe(value, context); return; } case PropertyAccessorType.Byte: { byte value = prop.GetByte(obj); if (value == 0) context.WriteByte(BinaryTypeCode.PropertySkip); else { context.WriteByte(BinaryTypeCode.UInt8); context.WriteByte(value); } return; } case PropertyAccessorType.Int16: { short value = prop.GetInt16(obj); if (value == 0) context.WriteByte(BinaryTypeCode.PropertySkip); else WriteInt16Unsafe(value, context); return; } case PropertyAccessorType.UInt16: { ushort value = prop.GetUInt16(obj); if (value == 0) context.WriteByte(BinaryTypeCode.PropertySkip); else WriteUInt16Unsafe(value, context); return; } case PropertyAccessorType.UInt32: { uint value = prop.GetUInt32(obj); if (value == 0) context.WriteByte(BinaryTypeCode.PropertySkip); else WriteUInt32(value, context); return; } case PropertyAccessorType.UInt64: { ulong value = prop.GetUInt64(obj); if (value == 0) context.WriteByte(BinaryTypeCode.PropertySkip); else WriteUInt64(value, context); return; } case PropertyAccessorType.Guid: { Guid value = prop.GetGuid(obj); if (value == Guid.Empty) context.WriteByte(BinaryTypeCode.PropertySkip); else WriteGuidUnsafe(value, context); return; } case PropertyAccessorType.Enum: { int enumValue = prop.GetEnumAsInt32(obj); if (enumValue == 0) { context.WriteByte(BinaryTypeCode.PropertySkip); } else if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny)) { context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(tiny); } else { context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(enumValue); } return; } case PropertyAccessorType.String: { // Fast path: typed getter, no boxing, no Type.GetTypeCode() call string? value = prop.GetString(obj); if (string.IsNullOrEmpty(value)) { context.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty); } else { WriteString(value, context); } return; } default: { // Object type (collection, complex object, byte[], dictionary) // Use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism var value = prop.GetValue(obj); // SKIP marker only for null (reference types) // Empty string, empty collections, etc. are valid values and must be written! if (value == null) { context.WriteByte(BinaryTypeCode.PropertySkip); } else { var runtimeType = value.GetType(); var complexIdx = prop.ComplexPropertyIndex; if (complexIdx >= 0) { var propWrapper = parentWrapper.GetPropertyTypeWrapper(complexIdx, runtimeType); if (propWrapper == null) { propWrapper = context.GetWrapper(runtimeType); parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper); } WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth); } else { // Non-complex in default case (nullable value type, etc.) WriteValueNonPrimitive(value, runtimeType, context, depth); } } return; } } } /// /// Writes a property value without type marker byte (markerless mode, UseMetadata=false). /// All values are written including defaults — no PropertySkip markers. /// Only called for non-nullable value types with ExpectedTypeCode set. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void WritePropertyMarkerless(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { switch (prop.AccessorType) { case PropertyAccessorType.Int32: context.WriteVarInt(prop.GetInt32(obj)); return; case PropertyAccessorType.Int64: context.WriteVarLong(prop.GetInt64(obj)); return; case PropertyAccessorType.Double: context.WriteRaw(prop.GetDouble(obj)); return; case PropertyAccessorType.Single: context.WriteRaw(prop.GetSingle(obj)); return; case PropertyAccessorType.Decimal: context.WriteDecimalBits(prop.GetDecimal(obj)); return; case PropertyAccessorType.DateTime: context.WriteDateTimeBits(prop.GetDateTime(obj)); return; case PropertyAccessorType.Guid: context.WriteGuidBits(prop.GetGuid(obj)); return; case PropertyAccessorType.Byte: context.WriteByte(prop.GetByte(obj)); return; case PropertyAccessorType.Int16: context.WriteRaw(prop.GetInt16(obj)); return; case PropertyAccessorType.UInt16: context.WriteRaw(prop.GetUInt16(obj)); return; case PropertyAccessorType.UInt32: context.WriteVarUInt(prop.GetUInt32(obj)); return; case PropertyAccessorType.UInt64: context.WriteVarULong(prop.GetUInt64(obj)); return; } } #endregion #region Specialized Array Writers /// /// Optimized array writer with specialized paths for primitive arrays. /// private static void WriteArray(IEnumerable enumerable, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase { context.WriteByte(BinaryTypeCode.Array); var nextDepth = depth + 1; // Use pre-computed metadata — no GetWrapper or GetCollectionElementType needed var metadata = wrapper.Metadata; var elementType = metadata.CollectionElementType; // Optimized path for primitive arrays if (elementType != null && metadata.SourceType.IsArray) { if (TryWritePrimitiveArray(enumerable, elementType, context)) return; } // For IList, we can write the count directly if (enumerable is IList list) { var count = list.Count; context.WriteVarUInt((uint)count); for (var i = 0; i < count; i++) { var item = list[i]; var itemType = item?.GetType() ?? typeof(object); WriteValue(item, itemType, context, nextDepth); } return; } // For other IEnumerable, collect first var items = new List(); foreach (var item in enumerable) { items.Add(item); } context.WriteVarUInt((uint)items.Count); foreach (var item in items) { var itemType = item?.GetType() ?? typeof(object); WriteValue(item, itemType, context, nextDepth); } } /// /// Specialized array writer for primitive arrays using bulk memory operations. /// private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { // String array needs context for interning — keep generic path if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray) { context.WriteVarUInt((uint)stringArray.Length); for (var i = 0; i < stringArray.Length; i++) { var s = stringArray[i]; if (s == null) context.WriteByte(BinaryTypeCode.Null); else WriteString(s, context); } return true; } // All other primitive arrays — inline write through context (zero virtual dispatch) return TryWritePrimitiveArrayCore(enumerable, elementType, context); } /// /// Core primitive array writes. Uses context write methods (zero virtual dispatch). /// private static bool TryWritePrimitiveArrayCore(IEnumerable enumerable, Type elementType, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { // Int32 array - very common case if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray) { context.WriteVarUInt((uint)intArray.Length); context.WriteInt32ArrayOptimized(intArray); return true; } // Double array - bulk write as raw bytes if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray) { context.WriteVarUInt((uint)doubleArray.Length); context.WriteDoubleArrayBulk(doubleArray); return true; } // Long array if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray) { context.WriteVarUInt((uint)longArray.Length); context.WriteLongArrayOptimized(longArray); return true; } // Float array - bulk write as raw bytes if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray) { context.WriteVarUInt((uint)floatArray.Length); context.WriteFloatArrayBulk(floatArray); return true; } // Bool array - pack as bytes if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray) { context.WriteVarUInt((uint)boolArray.Length); for (var i = 0; i < boolArray.Length; i++) { context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False); } return true; } // Guid array - bulk write if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray) { context.WriteVarUInt((uint)guidArray.Length); context.WriteGuidArrayBulk(guidArray); return true; } // Decimal array if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray) { context.WriteVarUInt((uint)decimalArray.Length); for (var i = 0; i < decimalArray.Length; i++) { WriteDecimalUnsafe(decimalArray[i], context); } return true; } // DateTime array if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray) { context.WriteVarUInt((uint)dateTimeArray.Length); for (var i = 0; i < dateTimeArray.Length; i++) { WriteDateTimeUnsafe(dateTimeArray[i], context); } return true; } return false; } private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase { context.WriteByte(BinaryTypeCode.Dictionary); context.WriteVarUInt((uint)dictionary.Count); var nextDepth = depth + 1; foreach (DictionaryEntry entry in dictionary) { // Write key var keyType = entry.Key?.GetType() ?? typeof(object); WriteValue(entry.Key, keyType, context, nextDepth); // Write value var valueType = entry.Value?.GetType() ?? typeof(object); WriteValue(entry.Value, valueType, context, nextDepth); } } #endregion #region Serialization Result // Implementation moved to AcBinarySerializer.BinarySerializationResult.cs #endregion #region Context Pool // Implementation moved to AcBinarySerializer.BinarySerializationContext.cs #endregion #region Serialization Context // Implementation moved to AcBinarySerializer.BinarySerializationContext.cs #endregion #region Type Metadata private static Type? GetCollectionElementType(Type type) { if (type.IsArray) { return type.GetElementType(); } if (type.IsGenericType) { var args = type.GetGenericArguments(); if (args.Length == 1) { return args[0]; } } foreach (var iface in type.GetInterfaces()) { if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { return iface.GetGenericArguments()[0]; } } return null; } // Type metadata helpers moved to AcBinarySerializer.BinarySerializeTypeMetadata.cs #endregion }