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

2380 lines
100 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(options.BufferWriterChunkSize);
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();
var context = AcquireArrayOutputContext(options);
try
{
// SGen fast path: skip IQueryable/Expression check + WriteValue dispatch chain.
// If root type has a GeneratedWriter it cannot be IQueryable/Expression/primitive/collection.
if (options.UseGeneratedCode)
{
var wrapper = context.GetWrapper(runtimeType);
if (wrapper.GeneratedWriter != null)
{
ScanForDuplicates(value, runtimeType, context);
context.WriteHeader();
WriteObject(value, wrapper, context, 0);
if (options.UseCompression != Lz4CompressionMode.None)
return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression);
return context.Output.ToArray(context._buffer, context._position);
}
}
// Full path: IQueryable/Expression conversion, primitive/collection dispatch
var actualValue = ConvertExpressionValue(value, ref runtimeType);
ScanForDuplicates(actualValue, runtimeType, context);
context.WriteHeader();
WriteValue(actualValue, runtimeType, context, 0);
if (options.UseCompression != Lz4CompressionMode.None)
return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression);
return context.Output.ToArray(context._buffer, context._position);
}
finally
{
ReturnContext(context, options);
}
}
/// <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 = AcquireArrayOutputContext(options);
try
{
ScanForDuplicates(value, runtimeType, context);
}
finally
{
ReturnContext(context, options);
}
}
/// <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();
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
{
// SGen fast path: skip IQueryable/Expression check + WriteValue dispatch chain.
// If root type has a GeneratedWriter it cannot be IQueryable/Expression/primitive/collection.
if (options.UseGeneratedCode)
{
var wrapper = context.GetWrapper(runtimeType);
if (wrapper.GeneratedWriter != null)
{
ScanForDuplicates(value, runtimeType, context);
context.WriteHeader();
WriteObject(value, wrapper, context, 0);
if (options.UseCompression != Lz4CompressionMode.None)
ThrowCompressionNotSupportedWithBufferWriter(context);
var bytesWritten = context.Output.GetTotalPosition(context._position);
context.Output.Flush(context._buffer, context._position);
return bytesWritten;
}
}
// Full path: IQueryable/Expression conversion, primitive/collection dispatch
var actualValue = ConvertExpressionValue(value, ref runtimeType);
ScanForDuplicates(actualValue, runtimeType, context);
context.WriteHeader();
WriteValue(actualValue, runtimeType, context, 0);
if (options.UseCompression != Lz4CompressionMode.None)
ThrowCompressionNotSupportedWithBufferWriter(context);
var totalBytesWritten = context.Output.GetTotalPosition(context._position);
context.Output.Flush(context._buffer, context._position);
return totalBytesWritten;
}
finally
{
context.Output = default;
ReturnContext(context, options);
}
}
/// <summary>
/// Serialize to a <see cref="System.IO.Pipelines.Pipe"/> as a chunked stream — pure AcBinary
/// bytes are written via <see cref="AsyncPipeWriterOutput"/> in raw mode (no per-chunk header).
/// The output is byte-compatible with <see cref="Serialize{T}(T, AcBinarySerializerOptions)"/>'s
/// <c>byte[]</c> result; a consumer drains <c>pipe.Reader</c> in its own reader-task and pushes
/// bytes via <see cref="AsyncPipeReaderInput.Feed"/>, then calls
/// <see cref="AcBinaryDeserializer.Deserialize{T}(AsyncPipeReaderInput, AcBinarySerializerOptions)"/>
/// — no extra parser, no special transport adapter.
///
/// <para><b>Why <see cref="System.IO.Pipelines.Pipe"/> instead of <see cref="System.IO.Pipelines.PipeWriter"/>?</b>
/// <c>Pipe.Writer</c> is always the BCL <c>PipeWriterImpl</c>, which is parallel-capable
/// (no <c>_tailMemory</c> reset race like <c>StreamPipeWriter</c>). This overload exposes the
/// <paramref name="flushPolicy"/> + <paramref name="flushTimeout"/> tuning safely.</para>
/// </summary>
/// <param name="value">The value to serialize; <c>null</c> writes a single null marker.</param>
/// <param name="pipe">Target pipe — caller drains <c>pipe.Reader</c> elsewhere.</param>
/// <param name="options">Serializer options (type wrappers, reference handling, interning, etc.).</param>
/// <param name="flushPolicy">
/// Per-chunk flush synchronization — see <see cref="FlushPolicy"/> for the three trade-off
/// points. <see cref="FlushPolicy.PerChunk"/>: strictly bounded ~chunk × 1 peak memory, no
/// producer/flush parallelism. <see cref="FlushPolicy.DoubleBuffered"/> (default): ~chunk × 2
/// peak memory, max producer/flush parallelism. <see cref="FlushPolicy.Coalesced"/>: up to
/// PauseWriterThreshold (~64 KB), highest throughput on bounded payloads.
/// </param>
/// <param name="flushTimeout">
/// Per-flush timeout. <c>null</c> → wait forever. Positive value: throws
/// <see cref="TimeoutException"/> on stuck consumers.
/// </param>
/// <returns>Total serialized bytes written.</returns>
public static int SerializeChunked<T>(T value, System.IO.Pipelines.Pipe pipe, AcBinarySerializerOptions options, FlushPolicy flushPolicy = FlushPolicy.DoubleBuffered, TimeSpan? flushTimeout = null)
{
if (pipe is null) throw new ArgumentNullException(nameof(pipe));
return SerializeToPipeWriterCore(value, pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: false);
}
/// <summary>
/// Serialize to any <see cref="System.IO.Pipelines.PipeWriter"/> as a chunked stream — pure
/// AcBinary bytes, no per-chunk header. The output is byte-compatible with
/// <see cref="Serialize{T}(T, AcBinarySerializerOptions)"/>'s <c>byte[]</c> result.
///
/// <para><b>Flush strategy auto-selected by writer type</b>: <c>StreamPipeWriter</c>
/// (<c>PipeWriter.Create(stream)</c> — NamedPipe / FileStream / NetworkStream / etc.) runs
/// sequentially per chunk because the BCL impl resets <c>_tailMemory</c> on flush completion
/// (race-incompatible with parallel send). Other PipeWriter implementations (Kestrel transport,
/// custom impls) run with the safe <see cref="FlushPolicy.DoubleBuffered"/> default — max parallelism, zero-alloc.</para>
///
/// <para><b>Need runtime tuning of the flush strategy?</b> Build a
/// <see cref="System.IO.Pipelines.Pipe"/> instance and use
/// <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
/// — only Pipe-based writers can guarantee parallel-capable flush behavior.</para>
///
/// <para><b>Need a multiplexed wire format with per-chunk frame headers?</b> See
/// <see cref="SerializeChunkedFramed{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions)"/>.</para>
/// </summary>
/// <param name="value">The value to serialize; <c>null</c> writes a single null marker.</param>
/// <param name="pipeWriter">Target pipe writer.</param>
/// <param name="options">Serializer options (type wrappers, reference handling, interning, etc.).</param>
/// <returns>Total serialized bytes written.</returns>
public static int SerializeChunked<T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options)
=> SerializeToPipeWriterCore(value, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: false);
/// <summary>
/// Serialize a value into a chunked stream where each chunk carries a self-describing
/// frame header — <c>[201][UINT16 size][data]</c> per chunk, with a final <c>[202]</c>
/// end-of-stream marker. The frame headers let the receiver detect chunk boundaries
/// incrementally without knowing the total payload size up front, and let multiple
/// independent messages share a single transport with reliable separation.
///
/// <para><b>Use this when</b> building a multiplexed wire protocol where several logical
/// messages are interleaved on one stream, when the receiver needs to start deserializing
/// as bytes arrive (pipeline parallelism — serialize / network / deserialize overlap), or
/// when the upper layer needs to dispatch each chunk independently. Typical scenarios:
/// real-time RPC, custom Hub-style protocols, event stream multiplexing.</para>
///
/// <para><b>Concrete example</b>: SignalR's <c>BinaryProtocolMode.AsyncSegment</c> uses
/// this exact wire format to interleave many HubMessages over a single connection.</para>
///
/// <para><b>Need a simpler streaming output without per-chunk metadata?</b> Use
/// <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
/// — bit-compatible with <see cref="Serialize{T}(T, AcBinarySerializerOptions)"/>'s
/// <c>byte[]</c> output, no extra parser needed on the receive side.</para>
/// </summary>
/// <param name="value">The value to serialize; <c>null</c> writes a single null marker.</param>
/// <param name="pipe">Target pipe — caller drains <c>pipe.Reader</c> elsewhere.</param>
/// <param name="options">Serializer options.</param>
/// <param name="flushPolicy">See <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>.</param>
/// <param name="flushTimeout">See <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>.</param>
/// <returns>Total serialized data bytes (excluding framing overhead).</returns>
public static int SerializeChunkedFramed<T>(T value, System.IO.Pipelines.Pipe pipe, AcBinarySerializerOptions options, FlushPolicy flushPolicy = FlushPolicy.DoubleBuffered, TimeSpan? flushTimeout = null)
{
if (pipe is null) throw new ArgumentNullException(nameof(pipe));
return SerializeToPipeWriterCore(value, pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: true);
}
/// <summary>
/// Serialize to any <see cref="System.IO.Pipelines.PipeWriter"/> with per-chunk frame headers
/// (multiplexed wire format). See
/// <see cref="SerializeChunkedFramed{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
/// for the wire format details and use-cases.
///
/// <para><b>Flush strategy auto-selected by writer type</b> — see
/// <see cref="SerializeChunked{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions)"/>.</para>
/// </summary>
public static int SerializeChunkedFramed<T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options)
=> SerializeToPipeWriterCore(value, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: true);
/// <summary>
/// Internal flush-tunable framed PipeWriter overload — used by <c>AyCode.Services</c>
/// (SignalR hub protocol) on Kestrel transport output, which is parallel-capable. External
/// callers should use the <see cref="System.IO.Pipelines.Pipe"/> overload to safely tune
/// <paramref name="flushPolicy"/> on a guaranteed parallel-capable writer.
/// </summary>
internal static int SerializeChunkedFramed<T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout)
=> SerializeToPipeWriterCore(value, pipeWriter, options, flushPolicy, flushTimeout, multiMessage: true);
/// <summary>
/// Internal legacy alias for <see cref="SerializeChunkedFramed{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
/// — kept until the SignalR hub protocol (<c>AcBinaryHubProtocol.cs</c>) is migrated to the
/// new name in a separate, isolated step. Identical behavior to <c>SerializeChunkedFramed</c>
/// (framed wire format with <c>[201][UINT16][data]</c> per chunk + <c>[202]</c> end marker).
/// </summary>
internal static int Serialize<T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout)
=> SerializeToPipeWriterCore(value, pipeWriter, options, flushPolicy, flushTimeout, multiMessage: true);
/// <summary>
/// Common pipe-output serialization core. Same loop for both raw (<see cref="SerializeChunked{T}"/>)
/// and framed (<see cref="SerializeChunkedFramed{T}"/>) modes — the only difference flows through
/// <paramref name="multiMessage"/> into the <see cref="AsyncPipeWriterOutput"/> ctor.
/// </summary>
private static int SerializeToPipeWriterCore<T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout, bool multiMessage)
{
if (value == null)
{
if (!multiMessage)
{
// Raw single-message mode: null is just a [BinaryTypeCode.Null] byte on the wire.
// No chunking needed, no [201]/[202] framing.
var span = pipeWriter.GetSpan(1);
span[0] = BinaryTypeCode.Null;
pipeWriter.Advance(1);
return 1;
}
// Framed mode (multiMessage=true): null still needs to flow through AsyncPipeWriterOutput
// so the wire is [201][UINT16=1][BinaryTypeCode.Null][202] — well-formed chunked frame the
// receiver can parse. Bypassing AsyncPipeWriterOutput here would emit a bare [Null] byte that
// breaks framing for any chunked-stream consumer (e.g. AcBinaryHubProtocol AsyncSegment).
var nullOutput = new AsyncPipeWriterOutput(pipeWriter, options.BufferWriterChunkSize, multiMessage: true, flushPolicy, flushTimeout);
nullOutput.Initialize(out var nullBuf, out var nullPos, out _);
nullBuf[nullPos++] = BinaryTypeCode.Null;
nullOutput.Flush(nullBuf, nullPos);
return 1;
}
var runtimeType = value.GetType();
var context = BinarySerializationContextPool<AsyncPipeWriterOutput>.Get(options);
context.Output = new AsyncPipeWriterOutput(pipeWriter, options.BufferWriterChunkSize, multiMessage, flushPolicy, flushTimeout);
context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);
try
{
if (options.UseGeneratedCode)
{
var wrapper = context.GetWrapper(runtimeType);
if (wrapper.GeneratedWriter != null)
{
ScanForDuplicates(value, runtimeType, context);
context.WriteHeader();
WriteObject(value, wrapper, context, 0);
if (options.UseCompression != Compression.Lz4CompressionMode.None)
ThrowCompressionNotSupportedWithPipeWriter(context);
var bytesWritten = context.Output.GetTotalPosition(context._position);
context.Output.Flush(context._buffer, context._position);
return bytesWritten;
}
}
var actualValue = ConvertExpressionValue(value, ref runtimeType);
ScanForDuplicates(actualValue, runtimeType, context);
context.WriteHeader();
WriteValue(actualValue, runtimeType, context, 0);
if (options.UseCompression != Compression.Lz4CompressionMode.None)
ThrowCompressionNotSupportedWithPipeWriter(context);
var totalBytesWritten = context.Output.GetTotalPosition(context._position);
context.Output.Flush(context._buffer, context._position);
return totalBytesWritten;
}
finally
{
context.Output = default;
ReturnContext(context, options);
}
}
/// <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 = AcquireArrayOutputContext(options);
try
{
ScanForDuplicates(value, runtimeType, context);
context.WriteHeader();
WriteValue(value, runtimeType, context, 0);
return context.Position;
}
finally
{
ReturnContext(context, options);
}
}
/// <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 = AcquireArrayOutputContext(options);
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
{
ReturnContext(context, options);
}
}
#region Entry Point Helpers
/// <summary>
/// Acquires a pooled ArrayBinaryOutput context and initializes the output buffer.
/// Reuses pooled ArrayBinaryOutput instance when available.
/// AggressiveInlining: JIT must see buffer init in caller scope for register allocation.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinarySerializationContext<ArrayBinaryOutput> AcquireArrayOutputContext(AcBinarySerializerOptions options)
{
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);
return context;
}
/// <summary>
/// Returns a serialization context to its pool. Uses async return when UseAsync is enabled.
/// </summary>
private static void ReturnContext<TOutput>(BinarySerializationContext<TOutput> context, AcBinarySerializerOptions options)
where TOutput : struct, IBinaryOutputBase
{
if (options.UseAsync) BinarySerializationContextPool<TOutput>.ReturnAsync(context);
else BinarySerializationContextPool<TOutput>.Return(context);
}
/// <summary>
/// Converts IQueryable/Expression values to AcExpressionNode for serialization.
/// Returns the converted value (or original if no conversion needed).
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static object ConvertExpressionValue<T>(T value, ref Type runtimeType)
{
if (value is IQueryable queryable)
{
runtimeType = typeof(AcExpressionNode);
return AcSerializerCommon.QueryableToNode(queryable);
}
if (AcSerializerCommon.IsExpressionType(runtimeType))
{
runtimeType = typeof(AcExpressionNode);
return AcExpressionConverter.ToNode((Expression)(object)value!);
}
return value!;
}
/// <summary>
/// Flushes output and throws NotSupportedException for compression with IBufferWriter.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowCompressionNotSupportedWithBufferWriter(BinarySerializationContext<BufferWriterBinaryOutput> context)
{
context.Output.Flush(context._buffer, context._position);
throw new NotSupportedException(
"Compression is not supported with IBufferWriter output. " +
"Use the byte[] overload or disable compression.");
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowCompressionNotSupportedWithPipeWriter(BinarySerializationContext<AsyncPipeWriterOutput> context)
{
context.Output.Flush(context._buffer, context._position);
throw new NotSupportedException(
"Compression is not supported with PipeWriter output. " +
"Use the byte[] overload or disable compression.");
}
#endregion
#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
}