AyCode.Core/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs

2143 lines
85 KiB
C#

using AyCode.Core.Compression;
using AyCode.Core.Serializers.Expressions;
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// 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
/// </summary>
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);
private static readonly Type ShortType = typeof(short);
private static readonly Type UShortType = typeof(ushort);
private static readonly Type UIntType = typeof(uint);
private static readonly Type ULongType = typeof(ulong);
private static readonly Type ByteType = typeof(byte);
private static readonly Type SByteType = typeof(sbyte);
private static readonly Type CharType = typeof(char);
#region Public API
#if DEBUG
/// <summary>
/// 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.
/// </summary>
/// <param name="value">The object graph to analyze.</param>
/// <param name="options">Serializer options (UseStringInterning should be enabled).</param>
/// <returns>Dictionary of property paths to their string occurrence counts.</returns>
public static Dictionary<string, Dictionary<string, int>> AnalyzeStringInternCandidates<T>(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<string, Dictionary<string, int>>();
var runtimeType = value.GetType();
// Create context without pooling (we need to set up callback)
using var context = new BinarySerializationContext<ArrayBinaryOutput>(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<string, int>();
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>(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<string, List<(string StringValue, int Count, int ByteLength)>>();
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
/// <summary>
/// Registers a source-generated binary writer for the specified type.
/// Once registered, WriteObject bypasses the runtime switch/delegate property loop
/// and calls the generated writer directly — eliminating Func&lt;&gt;.Invoke() overhead.
/// Call once at startup (e.g., in a static constructor or module initializer).
/// </summary>
/// <typeparam name="T">The type to register the writer for.</typeparam>
/// <param name="writer">The generated writer instance (typically a singleton).</param>
internal static void RegisterGeneratedWriter<T>(IGeneratedBinaryWriter writer)
{
ArgumentNullException.ThrowIfNull(writer);
GeneratedWriterRegistry.Register(typeof(T), writer);
}
/// <summary>
/// Registers a source-generated binary writer for the specified type.
/// </summary>
internal static void RegisterGeneratedWriter(Type type, IGeneratedBinaryWriter writer)
{
ArgumentNullException.ThrowIfNull(type);
ArgumentNullException.ThrowIfNull(writer);
GeneratedWriterRegistry.Register(type, writer);
}
#region SGen Slot Allocation
/// <summary>
/// Number of runtime wrapper slots reserved for polymorphic type cache (indices 0..RuntimeSlotCount-1).
/// SGen compile-time slots start at RuntimeSlotCount and above.
/// Easily modifiable — all code references this constant instead of literal values.
/// </summary>
internal const int RuntimeSlotCount = BinaryTypeCode.SlotCount;
/// <summary>
/// Next available wrapper slot index. Starts at RuntimeSlotCount so SGen slots
/// don't collide with runtime polymorphic slots (0..RuntimeSlotCount-1).
/// </summary>
internal static int s_nextWrapperSlot = RuntimeSlotCount + 1;
/// <summary>
/// Allocates a unique slot index for SGen wrapper cache.
/// Returns RuntimeSlotCount, RuntimeSlotCount+1, RuntimeSlotCount+2, ...
/// </summary>
internal static int AllocateWrapperSlot() => Interlocked.Increment(ref s_nextWrapperSlot) - 1;
#endregion
/// <summary>
/// Thread-safe registry for generated writers. Looked up once per TypeMetadataWrapper creation.
/// </summary>
internal static class GeneratedWriterRegistry
{
private static readonly ConcurrentDictionary<Type, IGeneratedBinaryWriter> Writers = new();
internal static void Register(Type type, IGeneratedBinaryWriter writer) => Writers[type] = writer;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static IGeneratedBinaryWriter? TryGet(Type type) =>
Writers.TryGetValue(type, out var writer) ? writer : null;
}
/// <summary>
/// Serialize object to binary with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] Serialize<T>(T value) => Serialize(value, AcBinarySerializerOptions.Default);
/// <summary>
/// Serialize object to an IBufferWriter with default options. Returns bytes written.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Serialize<T>(T value, IBufferWriter<byte> writer) => Serialize(value, writer, AcBinarySerializerOptions.Default);
/// <summary>
/// Serialize object to binary with specified options.
/// Uses ArrayBinaryOutput for byte[] result path.
/// </summary>
public static byte[] Serialize<T>(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<ArrayBinaryOutput>.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<ArrayBinaryOutput>.ReturnAsync(context);
else BinarySerializationContextPool<ArrayBinaryOutput>.Return(context);
}
}
/// <summary>
/// Runs only the scan pass (ScanForDuplicates) without writing.
/// For benchmarking scan pass overhead in isolation.
/// </summary>
internal static void ScanOnly<T>(T value, AcBinarySerializerOptions options)
{
if (value == null) return;
var runtimeType = value.GetType();
var context = BinarySerializationContextPool<ArrayBinaryOutput>.Get(options);
if (!context.OutputInitialized)
{
context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity);
context.OutputInitialized = true;
}
try
{
ScanForDuplicates(value, runtimeType, context);
}
finally
{
BinarySerializationContextPool<ArrayBinaryOutput>.Return(context);
}
}
/// <summary>
/// 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.
/// </summary>
public static int Serialize<T>(T value, IBufferWriter<byte> writer, AcBinarySerializerOptions options)
{
if (value == null)
{
var span = writer.GetSpan(1);
span[0] = BinaryTypeCode.Null;
writer.Advance(1);
return 1;
}
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<BufferWriterBinaryOutput>.Get(options);
context.Output = new BufferWriterBinaryOutput(writer, options.BufferWriterChunkSize);
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)
{
context.Output.Flush(context._buffer, context._position);
throw new NotSupportedException(
"Compression is not supported with IBufferWriter output. " +
"Use the byte[] overload or disable compression.");
}
var bytesWritten = context.Output.GetTotalPosition(context._position);
context.Output.Flush(context._buffer, context._position);
return bytesWritten;
}
finally
{
context.Output = default;
if (options.UseAsync) BinarySerializationContextPool<BufferWriterBinaryOutput>.ReturnAsync(context);
else BinarySerializationContextPool<BufferWriterBinaryOutput>.Return(context);
}
}
/// <summary>
/// Get the serialized size without allocating the final array.
/// Useful for pre-allocating buffers.
/// </summary>
public static int GetSerializedSize<T>(T value, AcBinarySerializerOptions options)
{
if (value == null) return 1;
var runtimeType = value.GetType();
var context = BinarySerializationContextPool<ArrayBinaryOutput>.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<ArrayBinaryOutput>.ReturnAsync(context);
else BinarySerializationContextPool<ArrayBinaryOutput>.Return(context);
}
}
/// <summary>
/// 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).
/// </summary>
public static BinarySerializationResult SerializeToPooledBuffer<T>(T value, AcBinarySerializerOptions options)
{
if (value == null)
{
return BinarySerializationResult.FromImmutable([BinaryTypeCode.Null]);
}
var runtimeType = value.GetType();
var context = BinarySerializationContextPool<ArrayBinaryOutput>.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<ArrayBinaryOutput>.ReturnAsync(context);
else BinarySerializationContextPool<ArrayBinaryOutput>.Return(context);
}
}
#endregion
#region Generated Writer Bridge Methods
/// <summary>
/// Bridge for generated writers: writes a non-null complex/collection value directly.
/// Skips null check (caller handles it) and TryWritePrimitive (caller knows it's complex).
/// Equivalent to the runtime's WriteValueNonPrimitive path.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void WriteValueGenerated<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase
{
WriteValueNonPrimitive(value, type, context, depth);
}
/// <summary>
/// Bridge for generated writers to call the runtime WriteString.
/// Matches WritePropertyOrSkip String case exactly: null → PropertySkip, empty → StringEmpty.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void WriteStringGenerated<TOutput>(string? value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
if (string.IsNullOrEmpty(value))
{
context.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty);
return;
}
WriteString(value, context);
}
/// <summary>
/// Bridge for generated writers: writes a non-null complex OBJECT directly.
/// Skips WriteValueNonPrimitive type dispatch (is byte[]?, is IDictionary?, is IEnumerable?, GetWrapper)
/// because the SrcGen knows at compile-time that the property is a complex object.
/// Uses pre-resolved wrapper type to avoid GetWrapper dictionary lookup.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void WriteObjectGenerated<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase
{
if (depth > context.MaxDepth)
{
context.WriteByte(BinaryTypeCode.Null);
return;
}
var wrapper = context.GetWrapper(type);
WriteObject(value, wrapper, context, depth);
}
/// <summary>
/// Bridge for generated writers: writes a non-null complex OBJECT using slot-based wrapper lookup.
/// SGen types pass their compile-time known slot index — avoids GetWrapper dictionary lookup.
/// First call per slot per context: populates slot from GetWrapper. Subsequent calls: direct array index.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void WriteObjectGenerated<TOutput>(object value, Type type, int wrapperSlot, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase
{
if (depth > context.MaxDepth)
{
context.WriteByte(BinaryTypeCode.Null);
return;
}
var wrapper = context.GetWrapper(type, wrapperSlot);
WriteObject(value, wrapper, context, depth);
}
#endregion
#region Value Writing
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteValue<TOutput>(object? value, Type type, BinarySerializationContext<TOutput> 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);
}
/// <summary>
/// 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.
/// </summary>
private static void WriteValueNonPrimitive<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase
{
// Nullable<T> where T is a value type: boxed value may be a primitive.
// Only Nullable<T> 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);
}
/// <summary>
/// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache).
/// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects.
/// </summary>
private static void WriteValueNonPrimitiveWithWrapper<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase
{
var type = wrapper.Metadata.SourceType;
// Nullable<T> 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
WriteObject(value, wrapper, context, depth);
}
/// <summary>
/// Polymorphic variant of WriteValueNonPrimitiveWithWrapper.
/// Cold path: polymorphism is rare. Writes poly prefix for non-object types,
/// delegates to WriteObjectPolymorphic for combined poly+ref marker handling.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void WriteValueNonPrimitiveWithWrapperPoly<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type polyRuntimeType)
where TOutput : struct, IBinaryOutputBase
{
var type = wrapper.Metadata.SourceType;
if (type.IsValueType)
{
if (TryWritePrimitive(value, value.GetType(), context))
return;
}
if (depth > context.MaxDepth)
{
context.WritePolymorphicPrefix(polyRuntimeType);
context.WriteByte(BinaryTypeCode.Null);
return;
}
if (value is byte[] byteArray)
{
context.WritePolymorphicPrefix(polyRuntimeType);
WriteByteArray(byteArray, context);
return;
}
if (value is IDictionary dictionary)
{
context.WritePolymorphicPrefix(polyRuntimeType);
WriteDictionary(dictionary, context, depth);
return;
}
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
{
context.WritePolymorphicPrefix(polyRuntimeType);
WriteArray(enumerable, wrapper, context, depth);
return;
}
// Complex object — handles combined poly+ref markers
WriteObjectPolymorphic(value, wrapper, context, depth, polyRuntimeType);
}
/// <summary>
/// Optimized primitive writer using TypeCode dispatch.
/// Avoids Nullable.GetUnderlyingType in hot path by using cached type info.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryWritePrimitive<TOutput>(object value, Type type, BinarySerializationContext<TOutput> 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<TOutput>(int value, BinarySerializationContext<TOutput> 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<TOutput>(long value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
if (value >= int.MinValue && value <= int.MaxValue)
{
WriteInt32((int)value, context);
return;
}
context.WriteByte(BinaryTypeCode.Int64);
context.WriteVarLong(value);
}
/// <summary>
/// Optimized float64 writer using batched write.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteFloat64Unsafe<TOutput>(double value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, value);
}
/// <summary>
/// Optimized float32 writer using batched write.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteFloat32Unsafe<TOutput>(float value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteTypeCodeAndRaw(BinaryTypeCode.Float32, value);
}
/// <summary>
/// Optimized decimal writer using direct memory copy of bits.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDecimalUnsafe<TOutput>(decimal value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteByte(BinaryTypeCode.Decimal);
context.WriteDecimalBits(value);
}
/// <summary>
/// Optimized DateTime writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDateTimeUnsafe<TOutput>(DateTime value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteByte(BinaryTypeCode.DateTime);
context.WriteDateTimeBits(value);
}
/// <summary>
/// Optimized Guid writer using direct memory copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteGuidUnsafe<TOutput>(Guid value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteByte(BinaryTypeCode.Guid);
context.WriteGuidBits(value);
}
/// <summary>
/// Optimized DateTimeOffset writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDateTimeOffsetUnsafe<TOutput>(DateTimeOffset value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteByte(BinaryTypeCode.DateTimeOffset);
context.WriteDateTimeOffsetBits(value);
}
/// <summary>
/// Optimized TimeSpan writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteTimeSpanUnsafe<TOutput>(TimeSpan value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, value.Ticks);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteInt16Unsafe<TOutput>(short value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteUInt16Unsafe<TOutput>(ushort value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteUInt32<TOutput>(uint value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteByte(BinaryTypeCode.UInt32);
context.WriteVarUInt(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteUInt64<TOutput>(ulong value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteByte(BinaryTypeCode.UInt64);
context.WriteVarULong(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteCharUnsafe<TOutput>(char value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteByte(BinaryTypeCode.Char);
context.WriteRaw(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteEnum<TOutput>(object value, BinarySerializationContext<TOutput> 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);
}
/// <summary>
/// Fast enum to int conversion avoiding Convert.ToInt32 overhead.
/// Handles all common enum underlying types.
/// </summary>
[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);
}
/// <summary>
/// Optimized string writer with FixStr for short strings.
/// Uses pre-computed WriteDuplicateEntry cursor for interning (no IdentityMap lookup).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteString<TOutput>(string value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
if (value.Length == 0)
{
context.WriteByte(BinaryTypeCode.StringEmpty);
return;
}
// Read and immediately reset — prevents flag from leaking to subsequent WriteString calls
// (e.g. from TryWritePrimitive, WriteDictionary, or when IsValidForInterningString is false)
var internEligible = context.StringInternEligible;
context.StringInternEligible = false;
if (internEligible && context.IsValidForInterningString(value.Length))
{
if (context.TryConsumeWritePlanEntry(out var planEntry))
{
ValidateWritePlanString(in planEntry, value);
if (planEntry.IsFirst)
{
// StringFirst: write interned string + cache index + data (Value holds the string)
context.WriteByte(BinaryTypeCode.StringInternFirst);
context.WriteVarUInt((uint)planEntry.CacheMapIndex);
context.WriteStringUtf8(planEntry.Value ?? value);
}
else
{
WriteStringInternRef(context, planEntry.CacheMapIndex);
}
return;
}
// No plan entry → single occurrence, fall through to FixStr/String path
#if DEBUG
context.OnStringInterned?.Invoke(null, value);
#endif
}
// FastWire: skip FixStr optimization (UTF-8 specific), write String marker + UTF-16 data
if (context.FastWire)
{
context.WriteByte(BinaryTypeCode.String);
context.WriteStringUtf8(value);
return;
}
// 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<TOutput>(byte[] value, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
context.WriteByte(BinaryTypeCode.ByteArray);
context.WriteVarUInt((uint)value.Length);
context.WriteBytes(value);
}
/// <summary>
/// String intern 2nd occurrence — cold path, just writes reference index.
/// </summary>
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteStringInternRef<TOutput>(BinarySerializationContext<TOutput> context, int cacheMapIndex)
where TOutput : struct, IBinaryOutputBase
{
context.WriteByte(BinaryTypeCode.StringInterned);
context.WriteVarUInt((uint)cacheMapIndex);
}
/// <summary>
/// Object ref 2nd occurrence — cold path, just writes reference index.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteObjectRef<TOutput>(BinarySerializationContext<TOutput> context, int cacheMapIndex)
where TOutput : struct, IBinaryOutputBase
{
context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarUInt((uint)cacheMapIndex);
}
#endregion
#region Complex Type Writers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase
{
var metadata = wrapper.Metadata;
var useMetaForType = context.UseMetadata && metadata.EnableMetadataFeature;
// Only IId types with ref handling enabled go to cold path
if (context.UseTypeReferenceHandling(metadata))
{
if (useMetaForType)
WriteObjectWithRefHandlingMeta(value, wrapper, context, depth);
else
WriteObjectWithRefHandling(value, wrapper, context, depth);
return;
}
if (useMetaForType)
{
// UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking)
var isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper);
// Marker kiírása — no ref handling, no cachedObjectCacheIndex
context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence);
}
else
{
// FixObj: assign slot on first occurrence this session
if (!wrapper.PolymorphicSeen)
{
wrapper.PolymorphicSeen = true;
wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
}
if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
else
context.WriteByte(BinaryTypeCode.Object);
}
WriteObjectProperties(value, wrapper, context, depth, useMetaForType);
}
/// <summary>
/// WriteObject variant with reference handling, no metadata.
/// Cold path: only IId types with ref tracking enabled.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteObjectWithRefHandling<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase
{
// Reference handling: consume pre-computed write plan entry from scan pass cursor
var cachedObjectCacheIndex = -1;
if (context.TryConsumeWritePlanEntry(out var planEntry))
{
ValidateWritePlanObject(in planEntry, value, wrapper);
if (planEntry.IsFirst)
{
cachedObjectCacheIndex = planEntry.CacheMapIndex;
}
else
{
WriteObjectRef(context, planEntry.CacheMapIndex);
return;
}
}
// Marker kiírása — no metadata
if (cachedObjectCacheIndex >= 0)
{
context.WriteByte(BinaryTypeCode.ObjectRefFirst);
context.WriteVarUInt((uint)cachedObjectCacheIndex);
}
else
{
if (!wrapper.PolymorphicSeen)
{
wrapper.PolymorphicSeen = true;
wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
}
if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
else
context.WriteByte(BinaryTypeCode.Object);
}
WriteObjectProperties(value, wrapper, context, depth, useMetaForType: false);
}
/// <summary>
/// WriteObject variant with reference handling + metadata.
/// Cold path: IId types with ref tracking + UseMetadata enabled.
/// </summary>
//[MethodImpl(MethodImplOptions.NoInlining)]
private static void WriteObjectWithRefHandlingMeta<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase
{
var isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper);
// Reference handling: consume pre-computed write plan entry from scan pass cursor
var cachedObjectCacheIndex = -1;
if (context.TryConsumeWritePlanEntry(out var planEntry))
{
ValidateWritePlanObject(in planEntry, value, wrapper);
if (planEntry.IsFirst)
{
cachedObjectCacheIndex = planEntry.CacheMapIndex;
}
else
{
WriteObjectRef(context, planEntry.CacheMapIndex);
return;
}
}
// Marker kiírása — with metadata
if (cachedObjectCacheIndex >= 0)
{
context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);
context.WriteVarUInt((uint)cachedObjectCacheIndex);
}
else
{
context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
}
context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence);
WriteObjectProperties(value, wrapper, context, depth, useMetaForType: true);
}
/// <summary>
/// Shared property writing loop — used by WriteObject, WriteObjectWithRefHandling, WriteObjectPolymorphic.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteObjectProperties<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, bool useMetaForType)
where TOutput : struct, IBinaryOutputBase
{
var nextDepth = depth + 1;
if (context.UseGeneratedCode)
{
var generatedWriter = wrapper.GeneratedWriter;
if (generatedWriter != null)
{
generatedWriter.WriteProperties(value, context, nextDepth);
return;
}
}
if (!useMetaForType)
{
WritePropertiesMarkerless(value, wrapper, context, nextDepth);
}
else
{
WritePropertiesWithMeta(value, wrapper, context, nextDepth);
}
}
private static void WritePropertiesWithMeta<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int nextDepth) where TOutput : struct, IBinaryOutputBase
{
var properties = wrapper.Metadata.Properties;
var propCount = properties.Length;
var hasPropertyFilter = context.HasPropertyFilter;
for (var i = 0; i < propCount; i++)
{
var prop = properties[i];
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
{
context.WriteByte(BinaryTypeCode.PropertySkip);
continue;
}
WritePropertyOrSkip(value, prop, wrapper, context, nextDepth);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WritePropertiesMarkerless<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int nextDepth) where TOutput : struct, IBinaryOutputBase
{
var properties = wrapper.Metadata.Properties;
var propCount = properties.Length;
var hasPropertyFilter = context.HasPropertyFilter;
for (var i = 0; i < propCount; i++)
{
var prop = properties[i];
if (!prop.ExpectedTypeCode.HasValue && hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
{
context.WriteByte(BinaryTypeCode.PropertySkip);
}
else
{
WritePropertyOrSkip(value, prop, wrapper, context, nextDepth);
}
}
}
/// <summary>
/// Polymorphic marker writing — extracted from WriteObject to keep hot path small.
/// Cold path: polymorphism is rare, NoInlining call overhead acceptable.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void WritePolymorphicMarker<TOutput>(BinarySerializationContext<TOutput> context, Type polyRuntimeType, int cachedObjectCacheIndex)
where TOutput : struct, IBinaryOutputBase
{
if (cachedObjectCacheIndex >= 0)
{
// Combined poly + RefFirst marker (69/71)
context.WritePolymorphicPrefix(polyRuntimeType, cachedObjectCacheIndex);
}
else
{
var rtWrapper = context.GetWrapper(polyRuntimeType);
if (rtWrapper.PolymorphicSeen && rtWrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
{
// 2+ poly in this session → FixObj (1 byte)
context.WriteByte((byte)rtWrapper.PolymorphicCacheIndex);
}
else
{
// First poly in this session → ObjectWithTypeName + assigns slot
context.WritePolymorphicPrefix(polyRuntimeType);
context.WriteByte(BinaryTypeCode.Object);
}
}
}
/// <summary>
/// Polymorphic object writing — handles combined poly+ref markers.
/// Cold path: polymorphism is rare, NoInlining acceptable.
/// Poly always implies UseMetadata=false (checked in WritePropertyOrSkip).
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void WriteObjectPolymorphic<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type polyRuntimeType)
where TOutput : struct, IBinaryOutputBase
{
var metadata = wrapper.Metadata;
// Reference handling
var cachedObjectCacheIndex = -1;
if (context.UseTypeReferenceHandling(metadata))
{
if (context.TryConsumeWritePlanEntry(out var planEntry))
{
ValidateWritePlanObject(in planEntry, value, wrapper);
if (planEntry.IsFirst)
{
cachedObjectCacheIndex = planEntry.CacheMapIndex;
}
else
{
WriteObjectRef(context, planEntry.CacheMapIndex);
return;
}
}
}
// Poly marker (handles combined poly+ref)
WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex);
WriteObjectProperties(value, wrapper, context, depth, false);
}
/// <summary>
/// Checks if a property value is null or default without boxing for value types.
/// </summary>
[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;
}
}
/// <summary>
/// Writes a property value using typed getters to avoid boxing.
/// </summary>
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//private static void WritePropertyValue<TOutput>(object obj, BinaryPropertyAccessor prop, BinarySerializationContext<TOutput> 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;
// }
//}
/// <summary>
/// Writes a property value OR a skip marker if the value is default/null.
/// Delegates to PropertyWriter bridge methods which handle UseMetadata internally:
/// UseMetadata=true: skip marker for defaults, type code + value for non-defaults.
/// UseMetadata=false (markerless): raw value only, no skip markers.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WritePropertyOrSkip<TOutput>(object obj, BinaryPropertyAccessor prop, TypeMetadataWrapper<BinarySerializeTypeMetadata> parentWrapper, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase
{
switch (prop.AccessorType)
{
case PropertyAccessorType.Int32:
context.WriteInt32Property(prop.GetInt32(obj));
return;
case PropertyAccessorType.Int64:
context.WriteInt64Property(prop.GetInt64(obj));
return;
case PropertyAccessorType.Boolean:
context.WriteBoolProperty(prop.GetBoolean(obj));
return;
case PropertyAccessorType.Double:
context.WriteFloat64Property(prop.GetDouble(obj));
return;
case PropertyAccessorType.Single:
context.WriteFloat32Property(prop.GetSingle(obj));
return;
case PropertyAccessorType.Decimal:
context.WriteDecimalProperty(prop.GetDecimal(obj));
return;
case PropertyAccessorType.DateTime:
context.WriteDateTimeProperty(prop.GetDateTime(obj));
return;
case PropertyAccessorType.Byte:
context.WriteByteProperty(prop.GetByte(obj));
return;
case PropertyAccessorType.Int16:
context.WriteInt16Property(prop.GetInt16(obj));
return;
case PropertyAccessorType.UInt16:
context.WriteUInt16Property(prop.GetUInt16(obj));
return;
case PropertyAccessorType.UInt32:
context.WriteUInt32Property(prop.GetUInt32(obj));
return;
case PropertyAccessorType.UInt64:
context.WriteUInt64Property(prop.GetUInt64(obj));
return;
case PropertyAccessorType.Guid:
context.WriteGuidProperty(prop.GetGuid(obj));
return;
case PropertyAccessorType.Enum:
context.WriteEnumInt32Property(prop.GetEnumAsInt32(obj));
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
{
context.StringInternEligible = prop.UseStringPropertyInterning(context.InternBit);
WriteString(value, context);
}
return;
}
default:
{
// Object type (collection, complex object, byte[], dictionary)
// Use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism
// Set interning eligibility for string collection elements
context.StringInternEligible = prop.IsStringCollectionProperty && prop.UseStringPropertyInterning(context.InternBit);
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);
}
if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType)
WriteValueNonPrimitiveWithWrapperPoly(value, propWrapper, context, depth, runtimeType);
else
WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth);
}
else
{
// Non-complex in default case (nullable value type, etc.)
if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType)
context.WritePolymorphicPrefix(runtimeType);
WriteValueNonPrimitive(value, runtimeType, context, depth);
}
}
return;
}
}
}
#endregion
#region Specialized Array Writers
/// <summary>
/// Optimized array writer with specialized paths for primitive collections.
/// </summary>
private static void WriteArray<TOutput>(IEnumerable enumerable, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> 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 T[] and List<T> — unified via ReadOnlySpan<T>
if (elementType != null)
{
if (TryWritePrimitiveCollection(enumerable, elementType, context))
return;
}
// For IList, we can write the count directly and use indexed access
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 ICollection (HashSet<T>, Queue<T>, SortedSet<T>, etc.) — has Count, no indexer
if (enumerable is ICollection collection)
{
context.WriteVarUInt((uint)collection.Count);
foreach (var item in enumerable)
{
var itemType = item?.GetType() ?? typeof(object);
WriteValue(item, itemType, context, nextDepth);
}
return;
}
// For other IEnumerable (rare: custom IEnumerable without ICollection), collect first
var items = new List<object?>();
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);
}
}
/// <summary>
/// Unified primitive collection writer for both T[] and List&lt;T&gt;.
/// Extracts ReadOnlySpan&lt;T&gt; from either source (zero-cost implicit conversion for arrays,
/// CollectionsMarshal.AsSpan for lists) and writes using bulk context methods with EnsureCapacity.
/// </summary>
private static bool TryWritePrimitiveCollection<TOutput>(IEnumerable enumerable, Type elementType, BinarySerializationContext<TOutput> context)
where TOutput : struct, IBinaryOutputBase
{
if (ReferenceEquals(elementType, IntType))
{
ReadOnlySpan<int> span;
if (enumerable is int[] arr) span = arr;
else if (enumerable is List<int> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
context.WriteInt32ArrayOptimized(span);
return true;
}
if (ReferenceEquals(elementType, LongType))
{
ReadOnlySpan<long> span;
if (enumerable is long[] arr) span = arr;
else if (enumerable is List<long> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
context.WriteLongArrayOptimized(span);
return true;
}
if (ReferenceEquals(elementType, DoubleType))
{
ReadOnlySpan<double> span;
if (enumerable is double[] arr) span = arr;
else if (enumerable is List<double> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
context.WriteDoubleArrayBulk(span);
return true;
}
if (ReferenceEquals(elementType, FloatType))
{
ReadOnlySpan<float> span;
if (enumerable is float[] arr) span = arr;
else if (enumerable is List<float> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
context.WriteFloatArrayBulk(span);
return true;
}
if (ReferenceEquals(elementType, BoolType))
{
ReadOnlySpan<bool> span;
if (enumerable is bool[] arr) span = arr;
else if (enumerable is List<bool> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
context.WriteByte(span[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
return true;
}
if (ReferenceEquals(elementType, GuidType))
{
ReadOnlySpan<Guid> span;
if (enumerable is Guid[] arr) span = arr;
else if (enumerable is List<Guid> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
context.WriteGuidArrayBulk(span);
return true;
}
if (ReferenceEquals(elementType, DecimalType))
{
ReadOnlySpan<decimal> span;
if (enumerable is decimal[] arr) span = arr;
else if (enumerable is List<decimal> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
WriteDecimalUnsafe(span[i], context);
return true;
}
if (ReferenceEquals(elementType, DateTimeType))
{
ReadOnlySpan<DateTime> span;
if (enumerable is DateTime[] arr) span = arr;
else if (enumerable is List<DateTime> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
WriteDateTimeUnsafe(span[i], context);
return true;
}
if (ReferenceEquals(elementType, StringType))
{
ReadOnlySpan<string?> span;
if (enumerable is string?[] arr) span = arr;
else if (enumerable is List<string?> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
{
var s = span[i];
if (s == null)
context.WriteByte(BinaryTypeCode.Null);
else
WriteString(s, context);
}
return true;
}
if (ReferenceEquals(elementType, ShortType))
{
ReadOnlySpan<short> span;
if (enumerable is short[] arr) span = arr;
else if (enumerable is List<short> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
WriteInt16Unsafe(span[i], context);
return true;
}
if (ReferenceEquals(elementType, UShortType))
{
ReadOnlySpan<ushort> span;
if (enumerable is ushort[] arr) span = arr;
else if (enumerable is List<ushort> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
WriteUInt16Unsafe(span[i], context);
return true;
}
if (ReferenceEquals(elementType, UIntType))
{
ReadOnlySpan<uint> span;
if (enumerable is uint[] arr) span = arr;
else if (enumerable is List<uint> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
WriteUInt32(span[i], context);
return true;
}
if (ReferenceEquals(elementType, ULongType))
{
ReadOnlySpan<ulong> span;
if (enumerable is ulong[] arr) span = arr;
else if (enumerable is List<ulong> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
WriteUInt64(span[i], context);
return true;
}
if (ReferenceEquals(elementType, ByteType))
{
// Note: top-level byte[] goes through WriteByteArray (BinaryTypeCode.ByteArray).
// This handles List<byte> and byte[] when reached via WriteArray (element-level encoding).
ReadOnlySpan<byte> span;
if (enumerable is byte[] arr) span = arr;
else if (enumerable is List<byte> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
{
context.WriteByte(BinaryTypeCode.UInt8);
context.WriteByte(span[i]);
}
return true;
}
if (ReferenceEquals(elementType, SByteType))
{
ReadOnlySpan<sbyte> span;
if (enumerable is sbyte[] arr) span = arr;
else if (enumerable is List<sbyte> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
{
context.WriteByte(BinaryTypeCode.Int8);
context.WriteByte(unchecked((byte)span[i]));
}
return true;
}
if (ReferenceEquals(elementType, CharType))
{
ReadOnlySpan<char> span;
if (enumerable is char[] arr) span = arr;
else if (enumerable is List<char> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
WriteCharUnsafe(span[i], context);
return true;
}
if (ReferenceEquals(elementType, DateTimeOffsetType))
{
ReadOnlySpan<DateTimeOffset> span;
if (enumerable is DateTimeOffset[] arr) span = arr;
else if (enumerable is List<DateTimeOffset> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
WriteDateTimeOffsetUnsafe(span[i], context);
return true;
}
if (ReferenceEquals(elementType, TimeSpanType))
{
ReadOnlySpan<TimeSpan> span;
if (enumerable is TimeSpan[] arr) span = arr;
else if (enumerable is List<TimeSpan> list) span = CollectionsMarshal.AsSpan(list);
else return false;
context.WriteVarUInt((uint)span.Length);
for (var i = 0; i < span.Length; i++)
WriteTimeSpanUnsafe(span[i], context);
return true;
}
// Enum collections: iterate without GetWrapper/WriteValue dispatch
if (elementType.IsEnum)
{
if (enumerable is IList enumList)
{
context.WriteVarUInt((uint)enumList.Count);
for (var i = 0; i < enumList.Count; i++)
WriteEnum(enumList[i]!, context);
}
else if (enumerable is ICollection enumCol)
{
context.WriteVarUInt((uint)enumCol.Count);
foreach (var item in enumerable)
WriteEnum(item!, context);
}
else
{
var items = new List<object>();
foreach (var item in enumerable)
items.Add(item!);
context.WriteVarUInt((uint)items.Count);
for (var i = 0; i < items.Count; i++)
WriteEnum(items[i], context);
}
return true;
}
return false;
}
private static void WriteDictionary<TOutput>(IDictionary dictionary, BinarySerializationContext<TOutput> 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
#region WritePlan Debug Validation
/// <summary>
/// DEBUG ONLY: Validates that the WritePlan entry's string value matches the actual string being written.
/// Catches scan/write pass visit index misalignment immediately.
/// </summary>
[Conditional("DEBUG")]
private static void ValidateWritePlanString(in WriteDuplicateEntry planEntry, string actualValue)
{
if (planEntry.IsFirst && planEntry.Value != null && !string.Equals(planEntry.Value, actualValue, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
$"plan string=\"{planEntry.Value}\", actual=\"{actualValue}\". " +
$"Scan/write pass visit order diverged.");
}
}
/// <summary>
/// DEBUG ONLY: Validates that the WritePlan entry's IId matches the actual object being written.
/// Uses the typed Id getter to extract the Id and compares against the scan pass IdentityMap.
/// </summary>
[Conditional("DEBUG")]
private static void ValidateWritePlanObject(in WriteDuplicateEntry planEntry, object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper)
{
var metadata = wrapper.Metadata;
switch (metadata.IdAccessorType)
{
case IdAccessorType.Int32:
{
var actualId = wrapper.RefIdGetterInt32!(value);
if (!wrapper.TryGetEntryInt32(actualId, out var slotIndex))
throw new InvalidOperationException(
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
$"Int32 Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");
ref var entry = ref wrapper.GetEntryRefInt32(slotIndex);
if (entry.CacheIndex != planEntry.CacheMapIndex)
throw new InvalidOperationException(
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
$"Int32 Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
break;
}
case IdAccessorType.Int64:
{
var actualId = wrapper.RefIdGetterInt64!(value);
if (!wrapper.TryGetEntryInt64(actualId, out var slotIndex))
throw new InvalidOperationException(
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
$"Int64 Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");
ref var entry = ref wrapper.GetEntryRefInt64(slotIndex);
if (entry.CacheIndex != planEntry.CacheMapIndex)
throw new InvalidOperationException(
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
$"Int64 Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
break;
}
case IdAccessorType.Guid:
{
var actualId = wrapper.RefIdGetterGuid!(value);
if (!wrapper.TryGetEntryGuid(actualId, out var slotIndex))
throw new InvalidOperationException(
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
$"Guid Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");
ref var entry = ref wrapper.GetEntryRefGuid(slotIndex);
if (entry.CacheIndex != planEntry.CacheMapIndex)
throw new InvalidOperationException(
$"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
$"Guid Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
break;
}
}
}
#endregion
}