Enhance AcBinary: property filter, string interning, arrays
- Add property-level filtering via BinaryPropertyFilter delegate and context - Improve string interning with new StringInternNew type code and promotion logic - Optimize array and dictionary serialization for primitive types - Expose strongly-typed property accessors for primitives and enums - Add new benchmarks for serialization modes - Refactor buffer pooling and cleanup code - All new features are opt-in; maintains backward compatibility
This commit is contained in:
parent
5601c0d3e2
commit
271f23d0f6
|
|
@ -465,4 +465,80 @@ public class SizeComparisonBenchmark
|
|||
|
||||
[Benchmark(Description = "Placeholder")]
|
||||
public int Placeholder() => 1; // Just to make BenchmarkDotNet happy
|
||||
}
|
||||
|
||||
public enum BinaryBenchmarkMode
|
||||
{
|
||||
Default,
|
||||
NoReferenceHandling,
|
||||
FastMode
|
||||
}
|
||||
|
||||
public abstract class AcBinaryOptionsBenchmarkBase
|
||||
{
|
||||
protected TestOrder TestOrder = null!;
|
||||
protected AcBinarySerializerOptions BinaryOptions = null!;
|
||||
protected MessagePackSerializerOptions MsgPackOptions = null!;
|
||||
protected byte[] AcBinaryData = null!;
|
||||
protected byte[] MsgPackData = null!;
|
||||
|
||||
[Params(BinaryBenchmarkMode.Default, BinaryBenchmarkMode.NoReferenceHandling, BinaryBenchmarkMode.FastMode)]
|
||||
public BinaryBenchmarkMode Mode { get; set; }
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
TestOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 4,
|
||||
palletsPerItem: 3,
|
||||
measurementsPerPallet: 3,
|
||||
pointsPerMeasurement: 6);
|
||||
|
||||
BinaryOptions = CreateBinaryOptions(Mode);
|
||||
MsgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||
|
||||
AcBinaryData = AcBinarySerializer.Serialize(TestOrder, BinaryOptions);
|
||||
MsgPackData = MessagePackSerializer.Serialize(TestOrder, MsgPackOptions);
|
||||
|
||||
var ratio = MsgPackData.Length == 0 ? 0 : 100.0 * AcBinaryData.Length / MsgPackData.Length;
|
||||
Console.WriteLine($"[BenchmarkSetup] Mode={Mode} | AcBinary={AcBinaryData.Length} bytes | MessagePack={MsgPackData.Length} bytes | Ratio={ratio:F1}%");
|
||||
}
|
||||
|
||||
private static AcBinarySerializerOptions CreateBinaryOptions(BinaryBenchmarkMode mode) => mode switch
|
||||
{
|
||||
BinaryBenchmarkMode.Default => new AcBinarySerializerOptions(),
|
||||
BinaryBenchmarkMode.NoReferenceHandling => AcBinarySerializerOptions.WithoutReferenceHandling(),
|
||||
BinaryBenchmarkMode.FastMode => new AcBinarySerializerOptions
|
||||
{
|
||||
UseMetadata = false,
|
||||
UseStringInterning = false,
|
||||
UseReferenceHandling = false
|
||||
},
|
||||
_ => new AcBinarySerializerOptions()
|
||||
};
|
||||
}
|
||||
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
public class AcBinaryOptionsSerializeBenchmark : AcBinaryOptionsBenchmarkBase
|
||||
{
|
||||
[Benchmark(Description = "MessagePack Serialize", Baseline = true)]
|
||||
public byte[] Serialize_MessagePack() => MessagePackSerializer.Serialize(TestOrder, MsgPackOptions);
|
||||
|
||||
[Benchmark(Description = "AcBinary Serialize")]
|
||||
public byte[] Serialize_AcBinary() => AcBinarySerializer.Serialize(TestOrder, BinaryOptions);
|
||||
}
|
||||
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
public class AcBinaryOptionsDeserializeBenchmark : AcBinaryOptionsBenchmarkBase
|
||||
{
|
||||
[Benchmark(Description = "MessagePack Deserialize", Baseline = true)]
|
||||
public TestOrder? Deserialize_MessagePack() => MessagePackSerializer.Deserialize<TestOrder>(MsgPackData, MsgPackOptions);
|
||||
|
||||
[Benchmark(Description = "AcBinary Deserialize")]
|
||||
public TestOrder? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize<TestOrder>(AcBinaryData);
|
||||
}
|
||||
|
|
@ -67,9 +67,10 @@ public static class AcBinaryDeserializer
|
|||
RegisterReader(BinaryTypeCode.Float64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe());
|
||||
RegisterReader(BinaryTypeCode.Decimal, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe());
|
||||
RegisterReader(BinaryTypeCode.Char, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe());
|
||||
RegisterReader(BinaryTypeCode.String, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndInternString(ref ctx));
|
||||
RegisterReader(BinaryTypeCode.String, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadPlainString(ref ctx));
|
||||
RegisterReader(BinaryTypeCode.StringInterned, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()));
|
||||
RegisterReader(BinaryTypeCode.StringEmpty, static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty);
|
||||
RegisterReader(BinaryTypeCode.StringInternNew, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndRegisterInternedString(ref ctx));
|
||||
RegisterReader(BinaryTypeCode.DateTime, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe());
|
||||
RegisterReader(BinaryTypeCode.DateTimeOffset, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe());
|
||||
RegisterReader(BinaryTypeCode.TimeSpan, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe());
|
||||
|
|
@ -281,6 +282,30 @@ public static class AcBinaryDeserializer
|
|||
context.Position, targetType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sima string olvasása - NEM regisztrál az intern táblába.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string ReadPlainString(ref BinaryDeserializationContext context)
|
||||
{
|
||||
var length = (int)context.ReadVarUInt();
|
||||
if (length == 0) return string.Empty;
|
||||
return context.ReadStringUtf8(length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Új internált string olvasása és regisztrálása az intern táblába.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string ReadAndRegisterInternedString(ref BinaryDeserializationContext context)
|
||||
{
|
||||
var length = (int)context.ReadVarUInt();
|
||||
if (length == 0) return string.Empty;
|
||||
var str = context.ReadStringUtf8(length);
|
||||
context.RegisterInternedString(str);
|
||||
return str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a string and register it in the intern table for future references.
|
||||
/// </summary>
|
||||
|
|
@ -1109,12 +1134,16 @@ public static class AcBinaryDeserializer
|
|||
context.Skip(16);
|
||||
return;
|
||||
case BinaryTypeCode.String:
|
||||
// CRITICAL FIX: Must register string in intern table even when skipping!
|
||||
SkipAndInternString(ref context);
|
||||
// Sima string - nem regisztrálunk
|
||||
SkipPlainString(ref context);
|
||||
return;
|
||||
case BinaryTypeCode.StringInterned:
|
||||
context.ReadVarUInt();
|
||||
return;
|
||||
case BinaryTypeCode.StringInternNew:
|
||||
// Új internált string - regisztrálni kell még skip esetén is
|
||||
SkipAndRegisterInternedString(ref context);
|
||||
return;
|
||||
case BinaryTypeCode.ByteArray:
|
||||
var byteLen = (int)context.ReadVarUInt();
|
||||
context.Skip(byteLen);
|
||||
|
|
@ -1139,6 +1168,31 @@ public static class AcBinaryDeserializer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sima string kihagyása - NEM regisztrál.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void SkipPlainString(ref BinaryDeserializationContext context)
|
||||
{
|
||||
var byteLen = (int)context.ReadVarUInt();
|
||||
if (byteLen > 0)
|
||||
{
|
||||
context.Skip(byteLen);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Új internált string kihagyása - DE regisztrálni kell!
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void SkipAndRegisterInternedString(ref BinaryDeserializationContext context)
|
||||
{
|
||||
var byteLen = (int)context.ReadVarUInt();
|
||||
if (byteLen == 0) return;
|
||||
var str = context.ReadStringUtf8(byteLen);
|
||||
context.RegisterInternedString(str);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skip a string but still register it in the intern table if it meets the length threshold.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,17 @@ public static class AcBinarySerializer
|
|||
|
||||
// Pre-computed UTF8 encoder for string operations
|
||||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
private static readonly Type StringType = typeof(string);
|
||||
private static readonly Type GuidType = typeof(Guid);
|
||||
private static readonly Type DateTimeOffsetType = typeof(DateTimeOffset);
|
||||
private static readonly Type TimeSpanType = typeof(TimeSpan);
|
||||
private static readonly Type IntType = typeof(int);
|
||||
private static readonly Type LongType = typeof(long);
|
||||
private static readonly Type FloatType = typeof(float);
|
||||
private static readonly Type DoubleType = typeof(double);
|
||||
private static readonly Type DecimalType = typeof(decimal);
|
||||
private static readonly Type BoolType = typeof(bool);
|
||||
private static readonly Type DateTimeType = typeof(DateTime);
|
||||
|
||||
#region Public API
|
||||
|
||||
|
|
@ -186,6 +197,11 @@ public static class AcBinarySerializer
|
|||
var metadata = GetTypeMetadata(type);
|
||||
foreach (var prop in metadata.Properties)
|
||||
{
|
||||
if (!context.ShouldSerializeProperty(value, prop))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var propValue = prop.GetValue(value);
|
||||
if (propValue != null)
|
||||
ScanReferences(propValue, context, depth + 1);
|
||||
|
|
@ -223,6 +239,11 @@ public static class AcBinarySerializer
|
|||
var metadata = GetTypeMetadata(type);
|
||||
foreach (var prop in metadata.Properties)
|
||||
{
|
||||
if (!context.ShouldIncludePropertyInMetadata(prop))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
context.RegisterPropertyName(prop.Name);
|
||||
|
||||
if (TryResolveNestedMetadataType(prop.PropertyType, out var nestedType))
|
||||
|
|
@ -574,20 +595,28 @@ public static class AcBinarySerializer
|
|||
return;
|
||||
}
|
||||
|
||||
// Try string interning - but only write ref if already interned
|
||||
if (context.UseStringInterning && value.Length >= context.MinStringInternLength)
|
||||
{
|
||||
if (context.TryGetInternedStringIndex(value, out var index))
|
||||
{
|
||||
// Már regisztrált string - csak index
|
||||
context.WriteByte(BinaryTypeCode.StringInterned);
|
||||
context.WriteVarUInt((uint)index);
|
||||
return;
|
||||
}
|
||||
// Register for future references
|
||||
context.RegisterInternedString(value);
|
||||
|
||||
if (context.TryPromoteInternCandidate(value, out var promotedIndex))
|
||||
{
|
||||
// Második előfordulás - StringInternNew: teljes tartalom + regisztráció
|
||||
context.WriteByte(BinaryTypeCode.StringInternNew);
|
||||
context.WriteStringUtf8(value);
|
||||
return;
|
||||
}
|
||||
|
||||
context.TrackInternCandidate(value);
|
||||
}
|
||||
|
||||
// Write inline string with optimized encoding
|
||||
// Első előfordulás vagy nincs interning - sima string
|
||||
context.WriteByte(BinaryTypeCode.String);
|
||||
context.WriteStringUtf8(value);
|
||||
}
|
||||
|
|
@ -634,7 +663,9 @@ public static class AcBinarySerializer
|
|||
|
||||
for (var i = 0; i < propCount; i++)
|
||||
{
|
||||
if (IsPropertyDefaultOrNull(value, properties[i]))
|
||||
var property = properties[i];
|
||||
|
||||
if (!context.ShouldSerializeProperty(value, property) || IsPropertyDefaultOrNull(value, property))
|
||||
{
|
||||
propertyStates[i] = 0;
|
||||
continue;
|
||||
|
|
@ -660,7 +691,7 @@ public static class AcBinarySerializer
|
|||
}
|
||||
else
|
||||
{
|
||||
WriteString(prop.Name, context);
|
||||
context.WritePreencodedPropertyName(prop.NameUtf8);
|
||||
}
|
||||
|
||||
WritePropertyValue(value, prop, context, nextDepth);
|
||||
|
|
@ -790,974 +821,6 @@ public static class AcBinarySerializer
|
|||
|
||||
#endregion
|
||||
|
||||
#region VarInt Encoding (Static Methods for Direct Use)
|
||||
|
||||
/// <summary>
|
||||
/// Write variable-length signed integer (ZigZag encoding).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int WriteVarInt(Span<byte> buffer, int value)
|
||||
{
|
||||
// ZigZag encoding
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
return WriteVarUInt(buffer, encoded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write variable-length unsigned integer.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int WriteVarUInt(Span<byte> buffer, uint value)
|
||||
{
|
||||
var i = 0;
|
||||
while (value >= 0x80)
|
||||
{
|
||||
buffer[i++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
buffer[i++] = (byte)value;
|
||||
return i;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write variable-length signed long (ZigZag encoding).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int WriteVarLong(Span<byte> buffer, long value)
|
||||
{
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
return WriteVarULong(buffer, encoded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write variable-length unsigned long.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int WriteVarULong(Span<byte> buffer, ulong value)
|
||||
{
|
||||
var i = 0;
|
||||
while (value >= 0x80)
|
||||
{
|
||||
buffer[i++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
buffer[i++] = (byte)value;
|
||||
return i;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Type Metadata
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static BinaryTypeMetadata GetTypeMetadata(Type type)
|
||||
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsDefaultValueFast(object value, TypeCode typeCode, Type propertyType)
|
||||
{
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.Int32: return (int)value == 0;
|
||||
case TypeCode.Int64: return (long)value == 0L;
|
||||
case TypeCode.Double: return (double)value == 0.0;
|
||||
case TypeCode.Decimal: return (decimal)value == 0m;
|
||||
case TypeCode.Single: return (float)value == 0f;
|
||||
case TypeCode.Byte: return (byte)value == 0;
|
||||
case TypeCode.Int16: return (short)value == 0;
|
||||
case TypeCode.UInt16: return (ushort)value == 0;
|
||||
case TypeCode.UInt32: return (uint)value == 0;
|
||||
case TypeCode.UInt64: return (ulong)value == 0;
|
||||
case TypeCode.SByte: return (sbyte)value == 0;
|
||||
case TypeCode.Boolean: return (bool)value == false;
|
||||
case TypeCode.String: return string.IsNullOrEmpty((string)value);
|
||||
}
|
||||
|
||||
if (propertyType.IsEnum) return Convert.ToInt32(value) == 0;
|
||||
if (ReferenceEquals(propertyType, GuidType)) return (Guid)value == Guid.Empty;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal sealed class BinaryTypeMetadata
|
||||
{
|
||||
public BinaryPropertyAccessor[] Properties { get; }
|
||||
|
||||
public BinaryTypeMetadata(Type type)
|
||||
{
|
||||
Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead &&
|
||||
p.GetIndexParameters().Length == 0 &&
|
||||
!HasJsonIgnoreAttribute(p))
|
||||
.Select(p => new BinaryPropertyAccessor(p))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized property accessor with typed getters to avoid boxing for common value types.
|
||||
/// </summary>
|
||||
internal sealed class BinaryPropertyAccessor
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly byte[] NameUtf8;
|
||||
public readonly Type PropertyType;
|
||||
public readonly TypeCode TypeCode;
|
||||
|
||||
// Generic getter (used for reference types and fallback)
|
||||
private readonly Func<object, object?> _objectGetter;
|
||||
|
||||
// Typed getters to avoid boxing - null if not applicable
|
||||
private readonly Delegate? _typedGetter;
|
||||
private readonly PropertyAccessorType _accessorType;
|
||||
|
||||
public BinaryPropertyAccessor(PropertyInfo prop)
|
||||
{
|
||||
Name = prop.Name;
|
||||
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
|
||||
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||
TypeCode = Type.GetTypeCode(PropertyType);
|
||||
|
||||
var declaringType = prop.DeclaringType!;
|
||||
|
||||
// Create typed getter for value types to avoid boxing
|
||||
(_typedGetter, _accessorType) = CreateTypedGetter(declaringType, prop);
|
||||
|
||||
// Always create object getter as fallback
|
||||
_objectGetter = CreateObjectGetter(declaringType, prop);
|
||||
}
|
||||
|
||||
private static (Delegate?, PropertyAccessorType) CreateTypedGetter(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var propType = prop.PropertyType;
|
||||
var underlyingType = Nullable.GetUnderlyingType(propType);
|
||||
var isNullable = underlyingType != null;
|
||||
var actualType = underlyingType ?? propType;
|
||||
|
||||
// For nullable types, we need special handling
|
||||
if (isNullable)
|
||||
{
|
||||
return (null, PropertyAccessorType.Object);
|
||||
}
|
||||
|
||||
// Check enum FIRST before TypeCode (enums have TypeCode.Int32 etc. based on underlying type)
|
||||
if (actualType.IsEnum)
|
||||
{
|
||||
return (CreateEnumGetterDelegate(declaringType, prop), PropertyAccessorType.Enum);
|
||||
}
|
||||
|
||||
// Check for Guid (no TypeCode)
|
||||
if (ReferenceEquals(actualType, GuidType))
|
||||
{
|
||||
return (CreateTypedGetterDelegate<Guid>(declaringType, prop), PropertyAccessorType.Guid);
|
||||
}
|
||||
|
||||
// Create typed getters for common value types
|
||||
var typeCode = Type.GetTypeCode(actualType);
|
||||
return typeCode switch
|
||||
{
|
||||
TypeCode.Int32 => (CreateTypedGetterDelegate<int>(declaringType, prop), PropertyAccessorType.Int32),
|
||||
TypeCode.Int64 => (CreateTypedGetterDelegate<long>(declaringType, prop), PropertyAccessorType.Int64),
|
||||
TypeCode.Boolean => (CreateTypedGetterDelegate<bool>(declaringType, prop), PropertyAccessorType.Boolean),
|
||||
TypeCode.Double => (CreateTypedGetterDelegate<double>(declaringType, prop), PropertyAccessorType.Double),
|
||||
TypeCode.Single => (CreateTypedGetterDelegate<float>(declaringType, prop), PropertyAccessorType.Single),
|
||||
TypeCode.Decimal => (CreateTypedGetterDelegate<decimal>(declaringType, prop), PropertyAccessorType.Decimal),
|
||||
TypeCode.DateTime => (CreateTypedGetterDelegate<DateTime>(declaringType, prop), PropertyAccessorType.DateTime),
|
||||
TypeCode.Byte => (CreateTypedGetterDelegate<byte>(declaringType, prop), PropertyAccessorType.Byte),
|
||||
TypeCode.Int16 => (CreateTypedGetterDelegate<short>(declaringType, prop), PropertyAccessorType.Int16),
|
||||
TypeCode.UInt16 => (CreateTypedGetterDelegate<ushort>(declaringType, prop), PropertyAccessorType.UInt16),
|
||||
TypeCode.UInt32 => (CreateTypedGetterDelegate<uint>(declaringType, prop), PropertyAccessorType.UInt32),
|
||||
TypeCode.UInt64 => (CreateTypedGetterDelegate<ulong>(declaringType, prop), PropertyAccessorType.UInt64),
|
||||
_ => (null, PropertyAccessorType.Object)
|
||||
};
|
||||
}
|
||||
|
||||
private static Delegate CreateTypedGetterDelegate<T>(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||||
var castExpr = Expression.Convert(objParam, declaringType);
|
||||
var propAccess = Expression.Property(castExpr, prop);
|
||||
return Expression.Lambda<Func<object, T>>(propAccess, objParam).Compile();
|
||||
}
|
||||
|
||||
private static Delegate CreateEnumGetterDelegate(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||||
var castExpr = Expression.Convert(objParam, declaringType);
|
||||
var propAccess = Expression.Property(castExpr, prop);
|
||||
// Convert enum to int
|
||||
var convertToInt = Expression.Convert(propAccess, typeof(int));
|
||||
return Expression.Lambda<Func<object, int>>(convertToInt, objParam).Compile();
|
||||
}
|
||||
|
||||
private static Func<object, object?> CreateObjectGetter(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||||
var castExpr = Expression.Convert(objParam, declaringType);
|
||||
var propAccess = Expression.Property(castExpr, prop);
|
||||
var boxed = Expression.Convert(propAccess, typeof(object));
|
||||
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object? GetValue(object obj) => _objectGetter(obj);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the accessor type for optimized writing without boxing.
|
||||
/// </summary>
|
||||
public PropertyAccessorType AccessorType => _accessorType;
|
||||
|
||||
// Typed getter methods - these avoid boxing
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetInt64(object obj) => ((Func<object, long>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool GetBoolean(object obj) => ((Func<object, bool>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double GetDouble(object obj) => ((Func<object, double>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float GetSingle(object obj) => ((Func<object, float>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public decimal GetDecimal(object obj) => ((Func<object, decimal>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public DateTime GetDateTime(object obj) => ((Func<object, DateTime>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte GetByte(object obj) => ((Func<object, byte>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public short GetInt16(object obj) => ((Func<object, short>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ushort GetUInt16(object obj) => ((Func<object, ushort>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public uint GetUInt32(object obj) => ((Func<object, uint>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ulong GetUInt64(object obj) => ((Func<object, ulong>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Guid GetGuid(object obj) => ((Func<object, Guid>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetEnumAsInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the type of property accessor for optimized dispatch.
|
||||
/// </summary>
|
||||
internal enum PropertyAccessorType : byte
|
||||
{
|
||||
Object = 0,
|
||||
Int32,
|
||||
Int64,
|
||||
Boolean,
|
||||
Double,
|
||||
Single,
|
||||
Decimal,
|
||||
DateTime,
|
||||
Byte,
|
||||
Int16,
|
||||
UInt16,
|
||||
UInt32,
|
||||
UInt64,
|
||||
Guid,
|
||||
Enum
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Context Pool
|
||||
|
||||
private static class BinarySerializationContextPool
|
||||
{
|
||||
private static readonly ConcurrentQueue<BinarySerializationContext> Pool = new();
|
||||
private const int MaxPoolSize = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static BinarySerializationContext Get(AcBinarySerializerOptions options)
|
||||
{
|
||||
if (Pool.TryDequeue(out var context))
|
||||
{
|
||||
context.Reset(options);
|
||||
return context;
|
||||
}
|
||||
return new BinarySerializationContext(options);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Return(BinarySerializationContext context)
|
||||
{
|
||||
if (Pool.Count < MaxPoolSize)
|
||||
{
|
||||
context.Clear();
|
||||
Pool.Enqueue(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization Context
|
||||
|
||||
/// <summary>
|
||||
/// Optimized serialization context with direct memory operations.
|
||||
/// Uses ArrayPool for buffer management and MemoryMarshal for zero-copy writes.
|
||||
/// </summary>
|
||||
internal sealed class BinarySerializationContext : IDisposable
|
||||
{
|
||||
private byte[] _buffer;
|
||||
private int _position;
|
||||
private int _initialBufferSize;
|
||||
|
||||
// Minimum buffer size for ArrayPool (reduces fragmentation)
|
||||
private const int MinBufferSize = 256;
|
||||
private const int PropertyIndexBufferMaxCache = 512;
|
||||
private const int PropertyStateBufferMaxCache = 512;
|
||||
|
||||
// Reference handling
|
||||
private Dictionary<object, int>? _scanOccurrences;
|
||||
private Dictionary<object, int>? _writtenRefs;
|
||||
private HashSet<object>? _multiReferenced;
|
||||
private int _nextRefId;
|
||||
|
||||
// String interning
|
||||
private Dictionary<string, int>? _internedStrings;
|
||||
private List<string>? _internedStringList;
|
||||
|
||||
// Property name table
|
||||
private Dictionary<string, int>? _propertyNames;
|
||||
private List<string>? _propertyNameList;
|
||||
private int[]? _propertyIndexBuffer;
|
||||
private byte[]? _propertyStateBuffer;
|
||||
|
||||
public bool UseReferenceHandling { get; private set; }
|
||||
public bool UseStringInterning { get; private set; }
|
||||
public bool UseMetadata { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
public byte MinStringInternLength { get; private set; }
|
||||
|
||||
public BinarySerializationContext(AcBinarySerializerOptions options)
|
||||
{
|
||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
||||
Reset(options);
|
||||
}
|
||||
|
||||
public void Reset(AcBinarySerializerOptions options)
|
||||
{
|
||||
_position = 0;
|
||||
_nextRefId = 1;
|
||||
UseReferenceHandling = options.UseReferenceHandling;
|
||||
UseStringInterning = options.UseStringInterning;
|
||||
UseMetadata = options.UseMetadata;
|
||||
MaxDepth = options.MaxDepth;
|
||||
MinStringInternLength = options.MinStringInternLength;
|
||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
||||
|
||||
if (_buffer.Length < _initialBufferSize)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_position = 0;
|
||||
_nextRefId = 1;
|
||||
|
||||
// Clear collections and trim if they grew too large
|
||||
// This prevents memory bloat when reusing pooled contexts
|
||||
ClearAndTrimIfNeeded(_scanOccurrences, InitialReferenceCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_writtenRefs, InitialReferenceCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_multiReferenced, InitialMultiRefCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
|
||||
|
||||
_internedStringList?.Clear();
|
||||
_propertyNameList?.Clear();
|
||||
|
||||
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
|
||||
{
|
||||
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
|
||||
_propertyIndexBuffer = null;
|
||||
}
|
||||
|
||||
if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
||||
_propertyStateBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_buffer != null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = null!;
|
||||
}
|
||||
|
||||
if (_propertyIndexBuffer != null)
|
||||
{
|
||||
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
|
||||
_propertyIndexBuffer = null;
|
||||
}
|
||||
|
||||
if (_propertyStateBuffer != null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
||||
_propertyStateBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
#region Optimized Buffer Writing
|
||||
|
||||
/// <summary>
|
||||
/// Ensures buffer has capacity, growing by doubling if needed.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void EnsureCapacity(int additionalBytes)
|
||||
{
|
||||
var required = _position + additionalBytes;
|
||||
if (required <= _buffer.Length) return;
|
||||
|
||||
GrowBuffer(required);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void GrowBuffer(int required)
|
||||
{
|
||||
var newSize = Math.Max(_buffer.Length * 2, required);
|
||||
var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
|
||||
_buffer.AsSpan(0, _position).CopyTo(newBuffer);
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = newBuffer;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteByte(byte value)
|
||||
{
|
||||
EnsureCapacity(1);
|
||||
_buffer[_position++] = value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteBytes(ReadOnlySpan<byte> data)
|
||||
{
|
||||
EnsureCapacity(data.Length);
|
||||
data.CopyTo(_buffer.AsSpan(_position));
|
||||
_position += data.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a blittable value type directly to buffer using MemoryMarshal.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteRaw<T>(T value) where T : unmanaged
|
||||
{
|
||||
var size = Unsafe.SizeOf<T>();
|
||||
EnsureCapacity(size);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
||||
_position += size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized decimal writer using GetBits.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDecimalBits(decimal value)
|
||||
{
|
||||
EnsureCapacity(16);
|
||||
Span<int> bits = stackalloc int[4];
|
||||
decimal.TryGetBits(value, bits, out _);
|
||||
|
||||
var destSpan = _buffer.AsSpan(_position, 16);
|
||||
MemoryMarshal.AsBytes(bits).CopyTo(destSpan);
|
||||
_position += 16;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized DateTime writer.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDateTimeBits(DateTime value)
|
||||
{
|
||||
EnsureCapacity(9);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
|
||||
_buffer[_position + 8] = (byte)value.Kind;
|
||||
_position += 9;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized Guid writer using TryWriteBytes.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteGuidBits(Guid value)
|
||||
{
|
||||
EnsureCapacity(16);
|
||||
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized DateTimeOffset writer.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDateTimeOffsetBits(DateTimeOffset value)
|
||||
{
|
||||
EnsureCapacity(10);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
|
||||
_position += 10;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarInt(int value)
|
||||
{
|
||||
EnsureCapacity(5);
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
WriteVarUIntInternal(encoded);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarUInt(uint value)
|
||||
{
|
||||
EnsureCapacity(5);
|
||||
WriteVarUIntInternal(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteVarUIntInternal(uint value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
_buffer[_position++] = (byte)value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarLong(long value)
|
||||
{
|
||||
EnsureCapacity(10);
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
WriteVarULongInternal(encoded);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarULong(ulong value)
|
||||
{
|
||||
EnsureCapacity(10);
|
||||
WriteVarULongInternal(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteVarULongInternal(ulong value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
_buffer[_position++] = (byte)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized string writer using span-based UTF8 encoding.
|
||||
/// Uses stackalloc for small strings to avoid allocations.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteStringUtf8(string value)
|
||||
{
|
||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
||||
WriteVarUInt((uint)byteCount);
|
||||
EnsureCapacity(byteCount);
|
||||
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
|
||||
_position += byteCount;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bulk Array Writers
|
||||
|
||||
/// <summary>
|
||||
/// Writes int32 array with optimized tiny int encoding.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteInt32ArrayOptimized(int[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
||||
{
|
||||
WriteByte(tiny);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int32);
|
||||
WriteVarInt(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes long array with optimized encoding (falls back to int32 when possible).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteLongArrayOptimized(long[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
if (value >= int.MinValue && value <= int.MaxValue)
|
||||
{
|
||||
var intValue = (int)value;
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
||||
{
|
||||
WriteByte(tiny);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int32);
|
||||
WriteVarInt(intValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int64);
|
||||
WriteVarLong(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes double array as bulk raw bytes - most efficient for large arrays.
|
||||
/// Each double is written with type code prefix for deserializer compatibility.
|
||||
/// </summary>
|
||||
public void WriteDoubleArrayBulk(double[] array)
|
||||
{
|
||||
// Each double needs 1 byte type code + 8 bytes data = 9 bytes
|
||||
EnsureCapacity(array.Length * 9);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float64;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
_position += 8;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes float array as bulk raw bytes.
|
||||
/// </summary>
|
||||
public void WriteFloatArrayBulk(float[] array)
|
||||
{
|
||||
// Each float needs 1 byte type code + 4 bytes data = 5 bytes
|
||||
EnsureCapacity(array.Length * 5);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float32;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
_position += 4;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes Guid array as bulk raw bytes.
|
||||
/// </summary>
|
||||
public void WriteGuidArrayBulk(Guid[] array)
|
||||
{
|
||||
// Each Guid needs 1 byte type code + 16 bytes data = 17 bytes
|
||||
EnsureCapacity(array.Length * 17);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Guid;
|
||||
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Header and Metadata
|
||||
|
||||
private int _headerPosition;
|
||||
|
||||
public void WriteHeaderPlaceholder()
|
||||
{
|
||||
// Reserve space for: version (1) + flags (1)
|
||||
EnsureCapacity(2);
|
||||
_headerPosition = _position;
|
||||
_position += 2;
|
||||
}
|
||||
|
||||
public void WriteMetadata()
|
||||
{
|
||||
// Write version at header position
|
||||
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
||||
|
||||
var hasPropertyNames = _propertyNameList != null && _propertyNameList.Count > 0;
|
||||
|
||||
// Build flags byte
|
||||
byte flags = BinaryTypeCode.HeaderFlagsBase;
|
||||
if (UseMetadata && hasPropertyNames)
|
||||
{
|
||||
flags |= BinaryTypeCode.HeaderFlag_Metadata;
|
||||
}
|
||||
if (UseReferenceHandling)
|
||||
{
|
||||
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
|
||||
}
|
||||
|
||||
_buffer[_headerPosition + 1] = flags;
|
||||
|
||||
// Write property names if metadata is enabled
|
||||
if ((flags & BinaryTypeCode.HeaderFlag_Metadata) != 0)
|
||||
{
|
||||
// Write property name count
|
||||
WriteVarUInt((uint)_propertyNameList!.Count);
|
||||
|
||||
// Write property names
|
||||
foreach (var name in _propertyNameList)
|
||||
{
|
||||
WriteStringUtf8(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reference Handling
|
||||
|
||||
// Smaller initial capacity to reduce memory allocation when not many references
|
||||
private const int InitialReferenceCapacity = 16;
|
||||
private const int InitialMultiRefCapacity = 8;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrackForScanning(object obj)
|
||||
{
|
||||
_scanOccurrences ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced ??= new HashSet<object>(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance);
|
||||
|
||||
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
count++;
|
||||
_multiReferenced.Add(obj);
|
||||
return false;
|
||||
}
|
||||
count = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool ShouldWriteRef(object obj, out int refId)
|
||||
{
|
||||
if (_multiReferenced != null && _multiReferenced.Contains(obj))
|
||||
{
|
||||
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
|
||||
if (!_writtenRefs.ContainsKey(obj))
|
||||
{
|
||||
refId = _nextRefId++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
refId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MarkAsWritten(object obj, int refId)
|
||||
{
|
||||
_writtenRefs![obj] = refId;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetExistingRef(object obj, out int refId)
|
||||
{
|
||||
if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId))
|
||||
return true;
|
||||
refId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region String Interning
|
||||
|
||||
// Smaller initial capacity for string interning
|
||||
private const int InitialInternCapacity = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetInternedStringIndex(string value, out int index)
|
||||
{
|
||||
if (_internedStrings != null && _internedStrings.TryGetValue(value, out index))
|
||||
return true;
|
||||
index = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterInternedString(string value)
|
||||
{
|
||||
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
||||
_internedStringList ??= new List<string>(InitialInternCapacity);
|
||||
|
||||
if (!_internedStrings.ContainsKey(value))
|
||||
{
|
||||
var index = _internedStringList.Count;
|
||||
_internedStrings[value] = index;
|
||||
_internedStringList.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Names
|
||||
|
||||
// Smaller initial capacity for property names
|
||||
private const int InitialPropertyNameCapacity = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterPropertyName(string name)
|
||||
{
|
||||
_propertyNames ??= new Dictionary<string, int>(InitialPropertyNameCapacity, StringComparer.Ordinal);
|
||||
_propertyNameList ??= new List<string>(InitialPropertyNameCapacity);
|
||||
|
||||
if (!_propertyNames.ContainsKey(name))
|
||||
{
|
||||
_propertyNames[name] = _propertyNameList.Count;
|
||||
_propertyNameList.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetPropertyNameIndex(string name)
|
||||
{
|
||||
return _propertyNames!.TryGetValue(name, out var index) ? index : -1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int[] RentPropertyIndexBuffer(int minimumLength)
|
||||
{
|
||||
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length >= minimumLength)
|
||||
{
|
||||
var buffer = _propertyIndexBuffer;
|
||||
_propertyIndexBuffer = null;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return ArrayPool<int>.Shared.Rent(minimumLength);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ReturnPropertyIndexBuffer(int[] buffer)
|
||||
{
|
||||
if (_propertyIndexBuffer == null && buffer.Length <= PropertyIndexBufferMaxCache)
|
||||
{
|
||||
_propertyIndexBuffer = buffer;
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayPool<int>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte[] RentPropertyStateBuffer(int minimumLength)
|
||||
{
|
||||
if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= minimumLength)
|
||||
{
|
||||
var buffer = _propertyStateBuffer;
|
||||
_propertyStateBuffer = null;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return ArrayPool<byte>.Shared.Rent(minimumLength);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ReturnPropertyStateBuffer(byte[] buffer)
|
||||
{
|
||||
if (_propertyStateBuffer == null && buffer.Length <= PropertyStateBufferMaxCache)
|
||||
{
|
||||
_propertyStateBuffer = buffer;
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public byte[] ToArray()
|
||||
{
|
||||
var result = GC.AllocateUninitializedArray<byte>(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public BinarySerializationResult DetachResult()
|
||||
{
|
||||
var buffer = _buffer;
|
||||
var length = _position;
|
||||
var result = new BinarySerializationResult(buffer, length, pooled: true);
|
||||
|
||||
var newSize = Math.Max(_initialBufferSize, MinBufferSize);
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(newSize);
|
||||
_position = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void WriteTo(IBufferWriter<byte> writer)
|
||||
{
|
||||
// Directly write the internal buffer to the IBufferWriter
|
||||
var span = writer.GetSpan(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(span);
|
||||
writer.Advance(_position);
|
||||
}
|
||||
|
||||
public int Position => _position;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ClearAndTrimIfNeeded<TKey, TValue>(Dictionary<TKey, TValue>? dict, int maxCapacity) where TKey : notnull
|
||||
{
|
||||
if (dict == null) return;
|
||||
dict.Clear();
|
||||
if (dict.EnsureCapacity(0) > maxCapacity)
|
||||
{
|
||||
dict.TrimExcess();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ClearAndTrimIfNeeded<T>(HashSet<T>? set, int maxCapacity)
|
||||
{
|
||||
if (set == null) return;
|
||||
set.Clear();
|
||||
if (set.EnsureCapacity(0) > maxCapacity)
|
||||
{
|
||||
set.TrimExcess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization Result
|
||||
|
||||
public sealed class BinarySerializationResult : IDisposable
|
||||
|
|
@ -1965,4 +1028,984 @@ public static class AcBinarySerializer
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Type Metadata
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static BinaryTypeMetadata GetTypeMetadata(Type type)
|
||||
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsPrimitiveOrStringFast(Type type)
|
||||
{
|
||||
if (type.IsPrimitive || ReferenceEquals(type, StringType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(type, DecimalType) ||
|
||||
ReferenceEquals(type, DateTimeType) ||
|
||||
ReferenceEquals(type, GuidType) ||
|
||||
ReferenceEquals(type, DateTimeOffsetType) ||
|
||||
ReferenceEquals(type, TimeSpanType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var underlying = Nullable.GetUnderlyingType(type);
|
||||
return underlying != null && IsPrimitiveOrStringFast(underlying);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsDictionaryType(Type type, out Type? keyType, out Type? valueType)
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var definition = type.GetGenericTypeDefinition();
|
||||
if (definition == typeof(Dictionary<,>) || definition == typeof(IDictionary<,>))
|
||||
{
|
||||
var args = type.GetGenericArguments();
|
||||
keyType = args[0];
|
||||
valueType = args[1];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var iface in type.GetInterfaces())
|
||||
{
|
||||
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IDictionary<,>))
|
||||
{
|
||||
var args = iface.GetGenericArguments();
|
||||
keyType = args[0];
|
||||
valueType = args[1];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
keyType = null;
|
||||
valueType = null;
|
||||
return typeof(IDictionary).IsAssignableFrom(type);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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;
|
||||
}
|
||||
|
||||
internal sealed class BinaryTypeMetadata
|
||||
{
|
||||
public BinaryPropertyAccessor[] Properties { get; }
|
||||
|
||||
public BinaryTypeMetadata(Type type)
|
||||
{
|
||||
Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead &&
|
||||
p.GetIndexParameters().Length == 0 &&
|
||||
!HasJsonIgnoreAttribute(p))
|
||||
.Select(p => new BinaryPropertyAccessor(p))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class BinaryPropertyAccessor
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly byte[] NameUtf8;
|
||||
public readonly Type PropertyType;
|
||||
public readonly TypeCode TypeCode;
|
||||
public readonly Type DeclaringType;
|
||||
|
||||
private readonly Func<object, object?> _objectGetter;
|
||||
private readonly Delegate? _typedGetter;
|
||||
private readonly PropertyAccessorType _accessorType;
|
||||
|
||||
public BinaryPropertyAccessor(PropertyInfo prop)
|
||||
{
|
||||
Name = prop.Name;
|
||||
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
|
||||
DeclaringType = prop.DeclaringType!;
|
||||
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||
TypeCode = Type.GetTypeCode(PropertyType);
|
||||
|
||||
(_typedGetter, _accessorType) = CreateTypedGetter(DeclaringType, prop);
|
||||
_objectGetter = CreateObjectGetter(DeclaringType, prop);
|
||||
}
|
||||
|
||||
public PropertyAccessorType AccessorType => _accessorType;
|
||||
public Func<object, object?> ObjectGetter => _objectGetter;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object? GetValue(object obj) => _objectGetter(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetInt64(object obj) => ((Func<object, long>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool GetBoolean(object obj) => ((Func<object, bool>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double GetDouble(object obj) => ((Func<object, double>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float GetSingle(object obj) => ((Func<object, float>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public decimal GetDecimal(object obj) => ((Func<object, decimal>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public DateTime GetDateTime(object obj) => ((Func<object, DateTime>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte GetByte(object obj) => ((Func<object, byte>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public short GetInt16(object obj) => ((Func<object, short>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ushort GetUInt16(object obj) => ((Func<object, ushort>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public uint GetUInt32(object obj) => ((Func<object, uint>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ulong GetUInt64(object obj) => ((Func<object, ulong>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Guid GetGuid(object obj) => ((Func<object, Guid>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetEnumAsInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
|
||||
|
||||
private static (Delegate?, PropertyAccessorType) CreateTypedGetter(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var propType = prop.PropertyType;
|
||||
var underlying = Nullable.GetUnderlyingType(propType);
|
||||
if (underlying != null)
|
||||
{
|
||||
return (null, PropertyAccessorType.Object);
|
||||
}
|
||||
|
||||
if (propType.IsEnum)
|
||||
{
|
||||
return (CreateEnumGetter(declaringType, prop), PropertyAccessorType.Enum);
|
||||
}
|
||||
|
||||
if (ReferenceEquals(propType, GuidType))
|
||||
{
|
||||
return (CreateTypedGetterDelegate<Guid>(declaringType, prop), PropertyAccessorType.Guid);
|
||||
}
|
||||
|
||||
var typeCode = Type.GetTypeCode(propType);
|
||||
return typeCode switch
|
||||
{
|
||||
TypeCode.Int32 => (CreateTypedGetterDelegate<int>(declaringType, prop), PropertyAccessorType.Int32),
|
||||
TypeCode.Int64 => (CreateTypedGetterDelegate<long>(declaringType, prop), PropertyAccessorType.Int64),
|
||||
TypeCode.Boolean => (CreateTypedGetterDelegate<bool>(declaringType, prop), PropertyAccessorType.Boolean),
|
||||
TypeCode.Double => (CreateTypedGetterDelegate<double>(declaringType, prop), PropertyAccessorType.Double),
|
||||
TypeCode.Single => (CreateTypedGetterDelegate<float>(declaringType, prop), PropertyAccessorType.Single),
|
||||
TypeCode.Decimal => (CreateTypedGetterDelegate<decimal>(declaringType, prop), PropertyAccessorType.Decimal),
|
||||
TypeCode.DateTime => (CreateTypedGetterDelegate<DateTime>(declaringType, prop), PropertyAccessorType.DateTime),
|
||||
TypeCode.Byte => (CreateTypedGetterDelegate<byte>(declaringType, prop), PropertyAccessorType.Byte),
|
||||
TypeCode.Int16 => (CreateTypedGetterDelegate<short>(declaringType, prop), PropertyAccessorType.Int16),
|
||||
TypeCode.UInt16 => (CreateTypedGetterDelegate<ushort>(declaringType, prop), PropertyAccessorType.UInt16),
|
||||
TypeCode.UInt32 => (CreateTypedGetterDelegate<uint>(declaringType, prop), PropertyAccessorType.UInt32),
|
||||
TypeCode.UInt64 => (CreateTypedGetterDelegate<ulong>(declaringType, prop), PropertyAccessorType.UInt64),
|
||||
_ => (null, PropertyAccessorType.Object)
|
||||
};
|
||||
}
|
||||
|
||||
private static Delegate CreateEnumGetter(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||||
var castExpr = Expression.Convert(objParam, declaringType);
|
||||
var propAccess = Expression.Property(castExpr, prop);
|
||||
var convertToInt = Expression.Convert(propAccess, typeof(int));
|
||||
return Expression.Lambda<Func<object, int>>(convertToInt, objParam).Compile();
|
||||
}
|
||||
|
||||
private static Func<object, TProperty> CreateTypedGetterDelegate<TProperty>(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||||
var castExpr = Expression.Convert(objParam, declaringType);
|
||||
var propAccess = Expression.Property(castExpr, prop);
|
||||
var convertExpr = Expression.Convert(propAccess, typeof(TProperty));
|
||||
return Expression.Lambda<Func<object, TProperty>>(convertExpr, objParam).Compile();
|
||||
}
|
||||
|
||||
private static Func<object, object?> CreateObjectGetter(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||||
var castExpr = Expression.Convert(objParam, declaringType);
|
||||
var propAccess = Expression.Property(castExpr, prop);
|
||||
var boxed = Expression.Convert(propAccess, typeof(object));
|
||||
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
|
||||
}
|
||||
}
|
||||
|
||||
internal enum PropertyAccessorType : byte
|
||||
{
|
||||
Object = 0,
|
||||
Int32,
|
||||
Int64,
|
||||
Boolean,
|
||||
Double,
|
||||
Single,
|
||||
Decimal,
|
||||
DateTime,
|
||||
Byte,
|
||||
Int16,
|
||||
UInt16,
|
||||
UInt32,
|
||||
UInt64,
|
||||
Guid,
|
||||
Enum
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Context Pool
|
||||
|
||||
private static class BinarySerializationContextPool
|
||||
{
|
||||
private static readonly ConcurrentQueue<BinarySerializationContext> Pool = new();
|
||||
private const int MaxPoolSize = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static BinarySerializationContext Get(AcBinarySerializerOptions options)
|
||||
{
|
||||
if (Pool.TryDequeue(out var context))
|
||||
{
|
||||
context.Reset(options);
|
||||
return context;
|
||||
}
|
||||
|
||||
return new BinarySerializationContext(options);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Return(BinarySerializationContext context)
|
||||
{
|
||||
if (Pool.Count < MaxPoolSize)
|
||||
{
|
||||
context.Clear();
|
||||
Pool.Enqueue(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization Context
|
||||
|
||||
internal sealed class BinarySerializationContext : IDisposable
|
||||
{
|
||||
private byte[] _buffer;
|
||||
private int _position;
|
||||
private int _initialBufferSize;
|
||||
|
||||
private const int MinBufferSize = 256;
|
||||
private const int PropertyIndexBufferMaxCache = 512;
|
||||
private const int PropertyStateBufferMaxCache = 512;
|
||||
|
||||
// Reference handling
|
||||
private Dictionary<object, int>? _scanOccurrences;
|
||||
private Dictionary<object, int>? _writtenRefs;
|
||||
private HashSet<object>? _multiReferenced;
|
||||
private int _nextRefId;
|
||||
|
||||
// String interning
|
||||
private Dictionary<string, int>? _internedStrings;
|
||||
private List<string>? _internedStringList;
|
||||
private HashSet<string>? _internCandidates;
|
||||
|
||||
// Property name table
|
||||
private Dictionary<string, int>? _propertyNames;
|
||||
private List<string>? _propertyNameList;
|
||||
private int[]? _propertyIndexBuffer;
|
||||
private byte[]? _propertyStateBuffer;
|
||||
|
||||
public bool UseReferenceHandling { get; private set; }
|
||||
public bool UseStringInterning { get; private set; }
|
||||
public bool UseMetadata { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
public byte MinStringInternLength { get; private set; }
|
||||
public BinaryPropertyFilter? PropertyFilter { get; private set; }
|
||||
|
||||
public BinarySerializationContext(AcBinarySerializerOptions options)
|
||||
{
|
||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
||||
Reset(options);
|
||||
}
|
||||
|
||||
public void Reset(AcBinarySerializerOptions options)
|
||||
{
|
||||
_position = 0;
|
||||
_nextRefId = 1;
|
||||
UseReferenceHandling = options.UseReferenceHandling;
|
||||
UseStringInterning = options.UseStringInterning;
|
||||
UseMetadata = options.UseMetadata;
|
||||
MaxDepth = options.MaxDepth;
|
||||
MinStringInternLength = options.MinStringInternLength;
|
||||
PropertyFilter = options.PropertyFilter;
|
||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
||||
|
||||
if (_buffer.Length < _initialBufferSize)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_position = 0;
|
||||
_nextRefId = 1;
|
||||
|
||||
ClearAndTrimIfNeeded(_scanOccurrences, InitialReferenceCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_writtenRefs, InitialReferenceCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_multiReferenced, InitialMultiRefCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
|
||||
|
||||
_internedStringList?.Clear();
|
||||
_propertyNameList?.Clear();
|
||||
_internCandidates?.Clear();
|
||||
|
||||
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
|
||||
{
|
||||
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
|
||||
_propertyIndexBuffer = null;
|
||||
}
|
||||
|
||||
if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
||||
_propertyStateBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_buffer != null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = null!;
|
||||
}
|
||||
|
||||
if (_propertyIndexBuffer != null)
|
||||
{
|
||||
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
|
||||
_propertyIndexBuffer = null;
|
||||
}
|
||||
|
||||
if (_propertyStateBuffer != null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
||||
_propertyStateBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
#region Property Filtering
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool ShouldSerializeProperty(object instance, BinaryPropertyAccessor property)
|
||||
{
|
||||
if (PropertyFilter == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var context = new BinaryPropertyFilterContext(
|
||||
instance,
|
||||
property.DeclaringType,
|
||||
property.Name,
|
||||
property.PropertyType,
|
||||
property.ObjectGetter);
|
||||
return PropertyFilter(context);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool ShouldIncludePropertyInMetadata(BinaryPropertyAccessor property)
|
||||
{
|
||||
if (PropertyFilter == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var context = new BinaryPropertyFilterContext(
|
||||
null,
|
||||
property.DeclaringType,
|
||||
property.Name,
|
||||
property.PropertyType,
|
||||
null);
|
||||
return PropertyFilter(context);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optimized Buffer Writing
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void EnsureCapacity(int additionalBytes)
|
||||
{
|
||||
var required = _position + additionalBytes;
|
||||
if (required <= _buffer.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GrowBuffer(required);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void GrowBuffer(int required)
|
||||
{
|
||||
var newSize = Math.Max(_buffer.Length * 2, required);
|
||||
var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
|
||||
_buffer.AsSpan(0, _position).CopyTo(newBuffer);
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = newBuffer;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteByte(byte value)
|
||||
{
|
||||
EnsureCapacity(1);
|
||||
_buffer[_position++] = value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteBytes(ReadOnlySpan<byte> data)
|
||||
{
|
||||
EnsureCapacity(data.Length);
|
||||
data.CopyTo(_buffer.AsSpan(_position));
|
||||
_position += data.Length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteRaw<T>(T value) where T : unmanaged
|
||||
{
|
||||
var size = Unsafe.SizeOf<T>();
|
||||
EnsureCapacity(size);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
||||
_position += size;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDecimalBits(decimal value)
|
||||
{
|
||||
EnsureCapacity(16);
|
||||
Span<int> bits = stackalloc int[4];
|
||||
decimal.TryGetBits(value, bits, out _);
|
||||
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDateTimeBits(DateTime value)
|
||||
{
|
||||
EnsureCapacity(9);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
|
||||
_buffer[_position + 8] = (byte)value.Kind;
|
||||
_position += 9;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteGuidBits(Guid value)
|
||||
{
|
||||
EnsureCapacity(16);
|
||||
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDateTimeOffsetBits(DateTimeOffset value)
|
||||
{
|
||||
EnsureCapacity(10);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
|
||||
_position += 10;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarInt(int value)
|
||||
{
|
||||
EnsureCapacity(5);
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
WriteVarUIntInternal(encoded);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarUInt(uint value)
|
||||
{
|
||||
EnsureCapacity(5);
|
||||
WriteVarUIntInternal(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteVarUIntInternal(uint value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
|
||||
_buffer[_position++] = (byte)value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarLong(long value)
|
||||
{
|
||||
EnsureCapacity(10);
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
WriteVarULongInternal(encoded);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarULong(ulong value)
|
||||
{
|
||||
EnsureCapacity(10);
|
||||
WriteVarULongInternal(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteVarULongInternal(ulong value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
|
||||
_buffer[_position++] = (byte)value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteStringUtf8(string value)
|
||||
{
|
||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
||||
WriteVarUInt((uint)byteCount);
|
||||
EnsureCapacity(byteCount);
|
||||
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
|
||||
_position += byteCount;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
|
||||
{
|
||||
WriteByte(BinaryTypeCode.String);
|
||||
WriteVarUInt((uint)utf8Name.Length);
|
||||
WriteBytes(utf8Name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bulk Array Writers
|
||||
|
||||
public void WriteInt32ArrayOptimized(int[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
||||
{
|
||||
WriteByte(tiny);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int32);
|
||||
WriteVarInt(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteLongArrayOptimized(long[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
if (value >= int.MinValue && value <= int.MaxValue)
|
||||
{
|
||||
var intValue = (int)value;
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
||||
{
|
||||
WriteByte(tiny);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int32);
|
||||
WriteVarInt(intValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int64);
|
||||
WriteVarLong(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteDoubleArrayBulk(double[] array)
|
||||
{
|
||||
EnsureCapacity(array.Length * 9);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float64;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
_position += 8;
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteFloatArrayBulk(float[] array)
|
||||
{
|
||||
EnsureCapacity(array.Length * 5);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float32;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
_position += 4;
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteGuidArrayBulk(Guid[] array)
|
||||
{
|
||||
EnsureCapacity(array.Length * 17);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Guid;
|
||||
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Header and Metadata
|
||||
|
||||
private int _headerPosition;
|
||||
|
||||
public void WriteHeaderPlaceholder()
|
||||
{
|
||||
EnsureCapacity(2);
|
||||
_headerPosition = _position;
|
||||
_position += 2;
|
||||
}
|
||||
|
||||
public void WriteMetadata()
|
||||
{
|
||||
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
||||
|
||||
var hasPropertyNames = _propertyNameList != null && _propertyNameList.Count > 0;
|
||||
|
||||
byte flags = BinaryTypeCode.HeaderFlagsBase;
|
||||
if (UseMetadata && hasPropertyNames)
|
||||
{
|
||||
flags |= BinaryTypeCode.HeaderFlag_Metadata;
|
||||
}
|
||||
|
||||
if (UseReferenceHandling)
|
||||
{
|
||||
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
|
||||
}
|
||||
|
||||
_buffer[_headerPosition + 1] = flags;
|
||||
|
||||
if ((flags & BinaryTypeCode.HeaderFlag_Metadata) != 0)
|
||||
{
|
||||
WriteVarUInt((uint)_propertyNameList!.Count);
|
||||
foreach (var name in _propertyNameList)
|
||||
{
|
||||
WriteStringUtf8(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reference Handling
|
||||
|
||||
private const int InitialReferenceCapacity = 16;
|
||||
private const int InitialMultiRefCapacity = 8;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrackForScanning(object obj)
|
||||
{
|
||||
_scanOccurrences ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced ??= new HashSet<object>(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance);
|
||||
|
||||
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
count++;
|
||||
_multiReferenced.Add(obj);
|
||||
return false;
|
||||
}
|
||||
|
||||
count = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool ShouldWriteRef(object obj, out int refId)
|
||||
{
|
||||
if (_multiReferenced != null && _multiReferenced.Contains(obj))
|
||||
{
|
||||
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
|
||||
if (!_writtenRefs.ContainsKey(obj))
|
||||
{
|
||||
refId = _nextRefId++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
refId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MarkAsWritten(object obj, int refId)
|
||||
{
|
||||
_writtenRefs![obj] = refId;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetExistingRef(object obj, out int refId)
|
||||
{
|
||||
if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
refId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region String Interning
|
||||
|
||||
private const int InitialInternCapacity = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetInternedStringIndex(string value, out int index)
|
||||
{
|
||||
if (_internedStrings != null && _internedStrings.TryGetValue(value, out index))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
index = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterInternedString(string value)
|
||||
{
|
||||
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
||||
_internedStringList ??= new List<string>(InitialInternCapacity);
|
||||
|
||||
if (!_internedStrings.ContainsKey(value))
|
||||
{
|
||||
var index = _internedStringList.Count;
|
||||
_internedStrings[value] = index;
|
||||
_internedStringList.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void TrackInternCandidate(string value)
|
||||
{
|
||||
_internCandidates ??= new HashSet<string>(InitialInternCapacity, StringComparer.Ordinal);
|
||||
_internCandidates.Add(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryPromoteInternCandidate(string value, out int index)
|
||||
{
|
||||
if (_internCandidates != null && _internCandidates.Remove(value))
|
||||
{
|
||||
RegisterInternedString(value);
|
||||
return TryGetInternedStringIndex(value, out index);
|
||||
}
|
||||
|
||||
index = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Names
|
||||
|
||||
private const int InitialPropertyNameCapacity = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterPropertyName(string name)
|
||||
{
|
||||
_propertyNames ??= new Dictionary<string, int>(InitialPropertyNameCapacity, StringComparer.Ordinal);
|
||||
_propertyNameList ??= new List<string>(InitialPropertyNameCapacity);
|
||||
|
||||
if (!_propertyNames.ContainsKey(name))
|
||||
{
|
||||
_propertyNames[name] = _propertyNameList.Count;
|
||||
_propertyNameList.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetPropertyNameIndex(string name)
|
||||
{
|
||||
return _propertyNames!.TryGetValue(name, out var index) ? index : -1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int[] RentPropertyIndexBuffer(int minimumLength)
|
||||
{
|
||||
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length >= minimumLength)
|
||||
{
|
||||
var buffer = _propertyIndexBuffer;
|
||||
_propertyIndexBuffer = null;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return ArrayPool<int>.Shared.Rent(minimumLength);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ReturnPropertyIndexBuffer(int[] buffer)
|
||||
{
|
||||
if (_propertyIndexBuffer == null && buffer.Length <= PropertyIndexBufferMaxCache)
|
||||
{
|
||||
_propertyIndexBuffer = buffer;
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayPool<int>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte[] RentPropertyStateBuffer(int minimumLength)
|
||||
{
|
||||
if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= minimumLength)
|
||||
{
|
||||
var buffer = _propertyStateBuffer;
|
||||
_propertyStateBuffer = null;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return ArrayPool<byte>.Shared.Rent(minimumLength);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ReturnPropertyStateBuffer(byte[] buffer)
|
||||
{
|
||||
if (_propertyStateBuffer == null && buffer.Length <= PropertyStateBufferMaxCache)
|
||||
{
|
||||
_propertyStateBuffer = buffer;
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public byte[] ToArray()
|
||||
{
|
||||
var result = GC.AllocateUninitializedArray<byte>(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public BinarySerializationResult DetachResult()
|
||||
{
|
||||
var buffer = _buffer;
|
||||
var length = _position;
|
||||
var result = new BinarySerializationResult(buffer, length, pooled: true);
|
||||
|
||||
var newSize = Math.Max(_initialBufferSize, MinBufferSize);
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(newSize);
|
||||
_position = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void WriteTo(IBufferWriter<byte> writer)
|
||||
{
|
||||
var span = writer.GetSpan(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(span);
|
||||
writer.Advance(_position);
|
||||
}
|
||||
|
||||
public int Position => _position;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ClearAndTrimIfNeeded<TKey, TValue>(Dictionary<TKey, TValue>? dict, int maxCapacity) where TKey : notnull
|
||||
{
|
||||
if (dict == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dict.Clear();
|
||||
if (dict.EnsureCapacity(0) > maxCapacity)
|
||||
{
|
||||
dict.TrimExcess();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ClearAndTrimIfNeeded<T>(HashSet<T>? set, int maxCapacity)
|
||||
{
|
||||
if (set == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
set.Clear();
|
||||
if (set.EnsureCapacity(0) > maxCapacity)
|
||||
{
|
||||
set.TrimExcess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Extensions;
|
||||
|
|
@ -69,6 +70,12 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
/// </summary>
|
||||
public int InitialBufferCapacity { get; init; } = 4096;
|
||||
|
||||
/// <summary>
|
||||
/// Optional property-level filter invoked before metadata registration and serialization.
|
||||
/// Return false to exclude the property from the payload.
|
||||
/// </summary>
|
||||
public BinaryPropertyFilter? PropertyFilter { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with specified max depth.
|
||||
/// </summary>
|
||||
|
|
@ -117,6 +124,7 @@ internal static class BinaryTypeCode
|
|||
public const byte String = 16; // Inline UTF8 string
|
||||
public const byte StringInterned = 17; // Reference to interned string by index
|
||||
public const byte StringEmpty = 18; // Empty string marker
|
||||
public const byte StringInternNew = 19; // New interned string - full content + register in table
|
||||
|
||||
// Date/Time types (20-23)
|
||||
public const byte DateTime = 20;
|
||||
|
|
@ -190,3 +198,62 @@ internal static class BinaryTypeCode
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate used to decide whether a property should be serialized.
|
||||
/// </summary>
|
||||
public delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Provides property metadata and lazy value access for property filter evaluations.
|
||||
/// </summary>
|
||||
public readonly struct BinaryPropertyFilterContext
|
||||
{
|
||||
private readonly object? _instance;
|
||||
private readonly Func<object, object?>? _valueGetter;
|
||||
|
||||
internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func<object, object?>? valueGetter)
|
||||
{
|
||||
_instance = instance;
|
||||
DeclaringType = declaringType;
|
||||
PropertyName = propertyName;
|
||||
PropertyType = propertyType;
|
||||
_valueGetter = valueGetter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the declaring type of the property.
|
||||
/// </summary>
|
||||
public Type DeclaringType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property name.
|
||||
/// </summary>
|
||||
public string PropertyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property type.
|
||||
/// </summary>
|
||||
public Type PropertyType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance being serialized when available. Null during metadata registration.
|
||||
/// </summary>
|
||||
public object? Instance => _instance;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the filter is invoked during metadata registration (when no instance is available).
|
||||
/// </summary>
|
||||
public bool IsMetadataPhase => _instance is null;
|
||||
|
||||
/// <summary>
|
||||
/// Lazily obtains the current property value. Returns null when invoked during metadata registration.
|
||||
/// </summary>
|
||||
public object? GetValue()
|
||||
{
|
||||
if (_instance == null || _valueGetter == null)
|
||||
return null;
|
||||
|
||||
return _valueGetter(_instance);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue