728 lines
28 KiB
C#
728 lines
28 KiB
C#
using System.Buffers;
|
|
using System.Collections.Concurrent;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Runtime.Serialization;
|
|
using System.Text;
|
|
using AyCode.Core.Interfaces;
|
|
using AyCode.Core.Serializers;
|
|
using AyCode.Core.Serializers.Binaries;
|
|
using AyCode.Core.Serializers.Jsons;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Serialization;
|
|
using static AyCode.Core.Helpers.JsonUtilities;
|
|
using static AyCode.Core.Serializers.Binaries.AcBinaryDeserializer;
|
|
|
|
namespace AyCode.Core.Extensions;
|
|
|
|
/// <summary>
|
|
/// High-performance Base62 encoder for compact $id/$ref values.
|
|
/// </summary>
|
|
internal static class Base62
|
|
{
|
|
private const string Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static string Encode(long value)
|
|
{
|
|
if (value == 0) return "0";
|
|
|
|
var isNegative = value < 0;
|
|
if (isNegative) value = -value;
|
|
|
|
Span<char> buffer = stackalloc char[16];
|
|
var index = buffer.Length;
|
|
|
|
while (value > 0)
|
|
{
|
|
buffer[--index] = Alphabet[(int)(value % 62)];
|
|
value /= 62;
|
|
}
|
|
|
|
if (isNegative) buffer[--index] = '-';
|
|
|
|
return new string(buffer[index..]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// High-performance hybrid reference resolver using Base62 encoded semantic IDs.
|
|
/// </summary>
|
|
public class HybridReferenceResolver : IReferenceResolver
|
|
{
|
|
internal Dictionary<string, object>? _idToObject;
|
|
internal Dictionary<object, string>? _objectToId;
|
|
internal HashSet<string>? _referencedIds;
|
|
|
|
private int _nextNumericId = 1;
|
|
private static readonly ConcurrentDictionary<Type, Func<object, object?>> IdGetterCache = new();
|
|
|
|
public bool IsForMerge { get; }
|
|
private readonly int _estimatedObjectCount;
|
|
|
|
public HybridReferenceResolver(bool isForMerge = false, int estimatedObjectCount = 64)
|
|
{
|
|
IsForMerge = isForMerge;
|
|
_estimatedObjectCount = estimatedObjectCount;
|
|
}
|
|
|
|
internal HashSet<string> ReferencedIds => _referencedIds ??=
|
|
new HashSet<string>(_estimatedObjectCount / 4, StringComparer.Ordinal);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private Dictionary<string, object> GetIdToObject() =>
|
|
_idToObject ??= new Dictionary<string, object>(_estimatedObjectCount, StringComparer.Ordinal);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private Dictionary<object, string> GetObjectToId() =>
|
|
_objectToId ??= new Dictionary<object, string>(_estimatedObjectCount, AyCode.Core.Serializers.ReferenceEqualityComparer.Instance);
|
|
|
|
public void AddReference(object context, string reference, object value)
|
|
{
|
|
GetIdToObject()[reference] = value;
|
|
GetObjectToId()[value] = reference;
|
|
}
|
|
|
|
public string GetReference(object context, object value)
|
|
{
|
|
var objectToId = GetObjectToId();
|
|
if (objectToId.TryGetValue(value, out var existingId))
|
|
{
|
|
if (!IsForMerge) ReferencedIds.Add(existingId);
|
|
return existingId;
|
|
}
|
|
|
|
var type = value.GetType();
|
|
var (isId, idType) = GetIdInfo(type);
|
|
|
|
string newRef;
|
|
if (isId && idType != null)
|
|
{
|
|
var idGetter = GetOrCreateIdGetter(type);
|
|
var idValue = idGetter(value);
|
|
|
|
if (idValue != null && !IsDefaultValue(idValue, idType))
|
|
{
|
|
var typeId = TypeCache.GetTypeId(type);
|
|
var objectIdAsLong = TypeCache.IdToLong(idValue);
|
|
var semanticId = TypeCache.CreateSemanticId(typeId, objectIdAsLong);
|
|
newRef = Base62.Encode(semanticId);
|
|
}
|
|
else
|
|
newRef = Base62.Encode(-_nextNumericId++);
|
|
}
|
|
else
|
|
newRef = Base62.Encode(-_nextNumericId++);
|
|
|
|
GetIdToObject()[newRef] = value;
|
|
objectToId[value] = newRef;
|
|
|
|
return newRef;
|
|
}
|
|
|
|
public bool IsReferenced(object context, object value) => _objectToId?.ContainsKey(value) ?? false;
|
|
|
|
public object ResolveReference(object context, string reference) =>
|
|
_idToObject != null && _idToObject.TryGetValue(reference, out var value) ? value : null!;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static Func<object, object?> GetOrCreateIdGetter(Type type) =>
|
|
IdGetterCache.GetOrAdd(type, static t =>
|
|
{
|
|
var prop = t.GetProperty("Id");
|
|
if (prop == null) return static _ => null;
|
|
var getMethod = prop.GetGetMethod();
|
|
if (getMethod == null) return static _ => null;
|
|
return obj => getMethod.Invoke(obj, null);
|
|
});
|
|
}
|
|
|
|
internal static class JsonReferencePostProcessor
|
|
{
|
|
private const string IdMarker = "\"$id\"";
|
|
|
|
public static string RemoveUnreferencedIds(string json, HashSet<string>? referencedIds)
|
|
{
|
|
if (!json.Contains(IdMarker)) return json;
|
|
return referencedIds == null || referencedIds.Count == 0
|
|
? RemoveAllIdsSpan(json)
|
|
: RemoveUnreferencedIdsSpan(json, referencedIds);
|
|
}
|
|
|
|
private static string RemoveAllIdsSpan(string json)
|
|
{
|
|
var sb = new StringBuilder(json.Length);
|
|
var lastCopyEnd = 0;
|
|
var searchStart = 0;
|
|
|
|
while (searchStart < json.Length)
|
|
{
|
|
var idIndex = json.IndexOf(IdMarker, searchStart, StringComparison.Ordinal);
|
|
if (idIndex < 0) break;
|
|
|
|
if (idIndex > lastCopyEnd) sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
|
|
|
|
var endIndex = SkipIdEntry(json, idIndex);
|
|
lastCopyEnd = endIndex;
|
|
searchStart = endIndex;
|
|
}
|
|
|
|
if (lastCopyEnd < json.Length) sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
|
|
|
|
return sb.Length == json.Length ? json : sb.ToString();
|
|
}
|
|
|
|
private static string RemoveUnreferencedIdsSpan(string json, HashSet<string> referencedIds)
|
|
{
|
|
var sb = new StringBuilder(json.Length);
|
|
var lastCopyEnd = 0;
|
|
var searchStart = 0;
|
|
|
|
while (searchStart < json.Length)
|
|
{
|
|
var idIndex = json.IndexOf(IdMarker, searchStart, StringComparison.Ordinal);
|
|
if (idIndex < 0) break;
|
|
|
|
var valueStart = idIndex + IdMarker.Length;
|
|
while (valueStart < json.Length && (json[valueStart] == ' ' || json[valueStart] == ':'))
|
|
valueStart++;
|
|
|
|
string? idValue = null;
|
|
var valueEnd = valueStart;
|
|
|
|
if (valueStart < json.Length && json[valueStart] == '"')
|
|
{
|
|
valueStart++;
|
|
valueEnd = valueStart;
|
|
while (valueEnd < json.Length && json[valueEnd] != '"') valueEnd++;
|
|
idValue = json.Substring(valueStart, valueEnd - valueStart);
|
|
valueEnd++;
|
|
}
|
|
|
|
while (valueEnd < json.Length && (json[valueEnd] == ' ' || json[valueEnd] == ','))
|
|
valueEnd++;
|
|
|
|
if (idValue != null && referencedIds.Contains(idValue))
|
|
searchStart = valueEnd;
|
|
else
|
|
{
|
|
if (idIndex > lastCopyEnd) sb.Append(json, lastCopyEnd, idIndex - lastCopyEnd);
|
|
lastCopyEnd = valueEnd;
|
|
searchStart = valueEnd;
|
|
}
|
|
}
|
|
|
|
if (lastCopyEnd == 0) return json;
|
|
if (lastCopyEnd < json.Length) sb.Append(json, lastCopyEnd, json.Length - lastCopyEnd);
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static int SkipIdEntry(string json, int idIndex)
|
|
{
|
|
var pos = idIndex + IdMarker.Length;
|
|
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ':')) pos++;
|
|
if (pos < json.Length && json[pos] == '"')
|
|
{
|
|
pos++;
|
|
while (pos < json.Length && json[pos] != '"') pos++;
|
|
if (pos < json.Length) pos++;
|
|
}
|
|
while (pos < json.Length && (json[pos] == ' ' || json[pos] == ',')) pos++;
|
|
return pos;
|
|
}
|
|
|
|
public static HashSet<string> CollectReferencedIds(string json)
|
|
{
|
|
const string refMarker = "\"$ref\"";
|
|
var result = new HashSet<string>(StringComparer.Ordinal);
|
|
var searchStart = 0;
|
|
|
|
while (searchStart < json.Length)
|
|
{
|
|
var refIndex = json.IndexOf(refMarker, searchStart, StringComparison.Ordinal);
|
|
if (refIndex < 0) break;
|
|
|
|
var valueStart = refIndex + refMarker.Length;
|
|
while (valueStart < json.Length && (json[valueStart] == ' ' || json[valueStart] == ':'))
|
|
valueStart++;
|
|
|
|
if (valueStart < json.Length && json[valueStart] == '"')
|
|
{
|
|
valueStart++;
|
|
var valueEnd = valueStart;
|
|
while (valueEnd < json.Length && json[valueEnd] != '"') valueEnd++;
|
|
if (valueEnd > valueStart) result.Add(json.Substring(valueStart, valueEnd - valueStart));
|
|
searchStart = valueEnd + 1;
|
|
}
|
|
else
|
|
searchStart = valueStart;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
internal sealed class PooledStringWriter : StringWriter
|
|
{
|
|
private static readonly ObjectPool<StringBuilder> StringBuilderPool =
|
|
new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy { InitialCapacity = 4096, MaximumRetainedCapacity = 4 * 1024 * 1024 });
|
|
|
|
private readonly StringBuilder _pooledBuilder;
|
|
private bool _disposed;
|
|
|
|
private PooledStringWriter(StringBuilder sb) : base(sb) => _pooledBuilder = sb;
|
|
|
|
public static PooledStringWriter Rent()
|
|
{
|
|
var sb = StringBuilderPool.Get();
|
|
sb.Clear();
|
|
return new PooledStringWriter(sb);
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (!_disposed) { _disposed = true; StringBuilderPool.Return(_pooledBuilder); }
|
|
base.Dispose(disposing);
|
|
}
|
|
}
|
|
|
|
internal interface ObjectPool<T> where T : class { T Get(); void Return(T obj); }
|
|
|
|
internal sealed class DefaultObjectPool<T> : ObjectPool<T> where T : class
|
|
{
|
|
[ThreadStatic] private static T? _threadLocalItem;
|
|
private readonly ConcurrentQueue<T> _pool = new();
|
|
private readonly IPooledObjectPolicy<T> _policy;
|
|
private const int MaxPoolSize = 8;
|
|
|
|
public DefaultObjectPool(IPooledObjectPolicy<T> policy) => _policy = policy;
|
|
|
|
public T Get()
|
|
{
|
|
var item = _threadLocalItem;
|
|
if (item != null) { _threadLocalItem = null; return item; }
|
|
return _pool.TryDequeue(out item) ? item : _policy.Create();
|
|
}
|
|
|
|
public void Return(T obj)
|
|
{
|
|
if (!_policy.Return(obj)) return;
|
|
if (_threadLocalItem == null) { _threadLocalItem = obj; return; }
|
|
if (_pool.Count < MaxPoolSize) _pool.Enqueue(obj);
|
|
}
|
|
}
|
|
|
|
internal interface IPooledObjectPolicy<T> { T Create(); bool Return(T obj); }
|
|
|
|
internal sealed class StringBuilderPooledObjectPolicy : IPooledObjectPolicy<StringBuilder>
|
|
{
|
|
public int InitialCapacity { get; init; } = 256;
|
|
public int MaximumRetainedCapacity { get; init; } = 4 * 1024 * 1024;
|
|
public StringBuilder Create() => new(InitialCapacity);
|
|
public bool Return(StringBuilder obj) { if (obj.Capacity > MaximumRetainedCapacity) return false; obj.Clear(); return true; }
|
|
}
|
|
|
|
public static class SerializeObjectExtensions
|
|
{
|
|
private static readonly UnifiedMergeContractResolver SharedContractResolver = new();
|
|
private static readonly Dictionary<object, object> EmptyContextDict = new();
|
|
|
|
public static JsonSerializerSettings Options => new()
|
|
{
|
|
ContractResolver = SharedContractResolver,
|
|
Context = new StreamingContext(StreamingContextStates.All, EmptyContextDict),
|
|
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
|
ReferenceResolverProvider = () => new HybridReferenceResolver(),
|
|
NullValueHandling = NullValueHandling.Ignore,
|
|
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
|
|
Formatting = Formatting.None,
|
|
};
|
|
|
|
#region JSON Serialization
|
|
|
|
/// <summary>
|
|
/// Serialize object to JSON string with default options.
|
|
/// </summary>
|
|
public static string ToJson<T>(this T source) => AcJsonSerializer.Serialize(source);
|
|
|
|
/// <summary>
|
|
/// Serialize object to JSON string with specified options.
|
|
/// </summary>
|
|
public static string ToJson<T>(this T source, AcJsonSerializerOptions options)
|
|
=> AcJsonSerializer.Serialize(source, options);
|
|
|
|
public static string ToJson<T>(this IQueryable<T> source) where T : class, IAcSerializableToJson
|
|
=> AcJsonSerializer.Serialize(source);
|
|
|
|
public static string ToJson<T>(this IQueryable<T> source, AcJsonSerializerOptions options) where T : class, IAcSerializableToJson
|
|
=> AcJsonSerializer.Serialize(source, options);
|
|
|
|
public static string ToJson<T>(this IEnumerable<T> source) where T : class, IAcSerializableToJson
|
|
=> AcJsonSerializer.Serialize(source);
|
|
|
|
public static string ToJson<T>(this IEnumerable<T> source, AcJsonSerializerOptions options) where T : class, IAcSerializableToJson
|
|
=> AcJsonSerializer.Serialize(source, options);
|
|
|
|
/// <summary>
|
|
/// Deserialize JSON to object with default options.
|
|
/// </summary>
|
|
public static T? JsonTo<T>(this string json)
|
|
{
|
|
json = UnwrapJsonString(json);
|
|
return AcJsonDeserializer.Deserialize<T>(json);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize JSON to object with specified options.
|
|
/// </summary>
|
|
public static T? JsonTo<T>(this string json, AcJsonSerializerOptions options)
|
|
{
|
|
json = UnwrapJsonString(json);
|
|
return AcJsonDeserializer.Deserialize<T>(json, options);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize UTF-8 encoded JSON bytes to object with default options.
|
|
/// Zero-allocation path - no string conversion needed.
|
|
/// </summary>
|
|
public static T? JsonTo<T>(this ReadOnlySpan<byte> utf8Json)
|
|
=> AcJsonDeserializer.Deserialize<T>(utf8Json);
|
|
|
|
/// <summary>
|
|
/// Deserialize UTF-8 encoded JSON bytes to object with specified options.
|
|
/// Zero-allocation path - no string conversion needed.
|
|
/// </summary>
|
|
public static T? JsonTo<T>(this ReadOnlySpan<byte> utf8Json, AcJsonSerializerOptions options)
|
|
=> AcJsonDeserializer.Deserialize<T>(utf8Json, options);
|
|
|
|
/// <summary>
|
|
/// Deserialize UTF-8 encoded JSON bytes to object with default options.
|
|
/// Zero-allocation path - no string conversion needed.
|
|
/// </summary>
|
|
public static T? JsonTo<T>(this byte[] utf8Json)
|
|
=> AcJsonDeserializer.Deserialize<T>(utf8Json.AsSpan());
|
|
|
|
/// <summary>
|
|
/// Deserialize UTF-8 encoded JSON bytes to object with specified options.
|
|
/// Zero-allocation path - no string conversion needed.
|
|
/// </summary>
|
|
public static T? JsonTo<T>(this byte[] utf8Json, AcJsonSerializerOptions options)
|
|
=> AcJsonDeserializer.Deserialize<T>(utf8Json.AsSpan(), options);
|
|
|
|
/// <summary>
|
|
/// Deserialize JSON to specified type with default options.
|
|
/// </summary>
|
|
public static object? JsonTo(this string json, Type toType)
|
|
{
|
|
json = UnwrapJsonString(json);
|
|
return AcJsonDeserializer.Deserialize(json, toType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize JSON to specified type with specified options.
|
|
/// </summary>
|
|
public static object? JsonTo(this string json, Type toType, AcJsonSerializerOptions options)
|
|
{
|
|
json = UnwrapJsonString(json);
|
|
return AcJsonDeserializer.Deserialize(json, toType, options);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populate existing object from JSON with default options.
|
|
/// </summary>
|
|
public static void JsonTo(this string json, object target)
|
|
{
|
|
json = UnwrapJsonString(json);
|
|
AcJsonDeserializer.Populate(json, target);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populate existing object from JSON with specified options.
|
|
/// </summary>
|
|
public static void JsonTo(this string json, object target, AcJsonSerializerOptions options)
|
|
{
|
|
json = UnwrapJsonString(json);
|
|
AcJsonDeserializer.Populate(json, target, options);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a deserialize chain that parses JSON once and allows multiple deserializations.
|
|
/// Efficient for deserializing the same JSON to multiple different types.
|
|
/// Use with 'using' statement or call Dispose() when done.
|
|
/// </summary>
|
|
public static IDeserializeChain<T> JsonToChain<T>(this string json)
|
|
{
|
|
json = UnwrapJsonString(json);
|
|
return AcJsonDeserializer.CreateDeserializeChain<T>(json);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a deserialize chain with options.
|
|
/// </summary>
|
|
public static IDeserializeChain<T> JsonToChain<T>(this string json, AcJsonSerializerOptions options)
|
|
{
|
|
json = UnwrapJsonString(json);
|
|
return AcJsonDeserializer.CreateDeserializeChain<T>(json, options);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a populate chain that parses JSON once and allows populating multiple objects.
|
|
/// Efficient for populating multiple objects from the same JSON source.
|
|
/// Use with 'using' statement or call Dispose() when done.
|
|
/// </summary>
|
|
public static IDeserializeChain<object> JsonToChain(this string json, object target)
|
|
{
|
|
json = UnwrapJsonString(json);
|
|
var chain = AcJsonDeserializer.CreateDeserializeChain<object>(json);
|
|
chain.ThenPopulate(target);
|
|
return chain;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a populate chain with options.
|
|
/// </summary>
|
|
public static IDeserializeChain<object> JsonToChain(this string json, object target, AcJsonSerializerOptions options)
|
|
{
|
|
json = UnwrapJsonString(json);
|
|
var chain = AcJsonDeserializer.CreateDeserializeChain<object>(json, options);
|
|
chain.ThenPopulate(target);
|
|
return chain;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Any (JSON or Binary based on options)
|
|
|
|
public static object ToAny<T>(this T source, AcSerializerOptions options)
|
|
{
|
|
if (options.SerializerType == AcSerializerType.Json) return ToJson(source, (AcJsonSerializerOptions)options);
|
|
return ToBinary(source, (AcBinarySerializerOptions)options);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize data (JSON string or binary byte[]) to object based on options.
|
|
/// </summary>
|
|
public static T? AnyTo<T>(this object data, AcSerializerOptions options)
|
|
{
|
|
if (options.SerializerType == AcSerializerType.Json)
|
|
return ((string)data).JsonTo<T>((AcJsonSerializerOptions)options);
|
|
return ((byte[])data).BinaryTo<T>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize data to specified type based on options.
|
|
/// </summary>
|
|
public static object? AnyTo(this object data, Type targetType, AcSerializerOptions options)
|
|
{
|
|
if (options.SerializerType == AcSerializerType.Json)
|
|
return ((string)data).JsonTo(targetType, (AcJsonSerializerOptions)options);
|
|
return ((byte[])data).BinaryTo(targetType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populate existing object from data based on options.
|
|
/// </summary>
|
|
public static void AnyTo<T>(this object data, T target, AcSerializerOptions options) where T : class
|
|
{
|
|
if (options.SerializerType == AcSerializerType.Json)
|
|
((string)data).JsonTo(target, (AcJsonSerializerOptions)options);
|
|
else
|
|
((byte[])data).BinaryTo(target);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populate existing object with merge semantics based on options.
|
|
/// </summary>
|
|
public static void AnyToMerge<T>(this object data, T target, AcSerializerOptions options) where T : class
|
|
{
|
|
if (options.SerializerType == AcSerializerType.Json)
|
|
((string)data).JsonTo(target, (AcJsonSerializerOptions)options);
|
|
else
|
|
((byte[])data).BinaryToMerge(target);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Binary Serialization
|
|
|
|
/// <summary>
|
|
/// Serialize object to binary byte array with default options.
|
|
/// </summary>
|
|
public static byte[] ToBinary<T>(this T source) => AcBinarySerializer.Serialize(source);
|
|
|
|
/// <summary>
|
|
/// Serialize object to binary byte array with specified options.
|
|
/// </summary>
|
|
public static byte[] ToBinary<T>(this T source, AcBinarySerializerOptions options)
|
|
=> AcBinarySerializer.Serialize(source, options);
|
|
|
|
/// <summary>
|
|
/// Serialize object directly to an IBufferWriter for zero-copy scenarios.
|
|
/// </summary>
|
|
public static void ToBinary<T>(this T source, IBufferWriter<byte> writer)
|
|
=> AcBinarySerializer.Serialize(source, writer, AcBinarySerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Serialize object directly to an IBufferWriter with specified options.
|
|
/// </summary>
|
|
public static void ToBinary<T>(this T source, IBufferWriter<byte> writer, AcBinarySerializerOptions options)
|
|
=> AcBinarySerializer.Serialize(source, writer, options);
|
|
|
|
/// <summary>
|
|
/// Get the serialized binary size without allocating the final array.
|
|
/// </summary>
|
|
public static int GetBinarySize<T>(this T source)
|
|
=> AcBinarySerializer.GetSerializedSize(source, AcBinarySerializerOptions.Default);
|
|
|
|
/// <summary>
|
|
/// Get the serialized binary size with specified options.
|
|
/// </summary>
|
|
public static int GetBinarySize<T>(this T source, AcBinarySerializerOptions options)
|
|
=> AcBinarySerializer.GetSerializedSize(source, options);
|
|
|
|
/// <summary>
|
|
/// Deserialize binary data to object.
|
|
/// </summary>
|
|
public static T? BinaryTo<T>(this byte[] data)
|
|
=> AcBinaryDeserializer.Deserialize<T>(data);
|
|
|
|
/// <summary>
|
|
/// Deserialize binary data to specified type.
|
|
/// </summary>
|
|
public static object? BinaryTo(this byte[] data, Type targetType)
|
|
=> AcBinaryDeserializer.Deserialize(data, targetType);
|
|
|
|
/// <summary>
|
|
/// Populate existing object from binary data.
|
|
/// </summary>
|
|
public static void BinaryTo<T>(this byte[] data, T target) where T : class
|
|
=> AcBinaryDeserializer.Populate(data, target);
|
|
|
|
/// <summary>
|
|
/// Populate existing object from binary data with merge semantics for IId collections.
|
|
/// </summary>
|
|
public static void BinaryToMerge<T>(this byte[] data, T target) where T : class
|
|
=> AcBinaryDeserializer.PopulateMerge(data, target);
|
|
|
|
/// <summary>
|
|
/// Create a deserialize chain that parses binary data once and allows multiple deserializations.
|
|
/// Efficient for deserializing the same binary to multiple different types.
|
|
/// Use with 'using' statement or call Dispose() when done.
|
|
/// </summary>
|
|
public static IDeserializeChain<T> BinaryToChain<T>(this byte[] data)
|
|
=> AcBinaryDeserializer.CreateDeserializeChain<T>(data);
|
|
|
|
/// <summary>
|
|
/// Create a deserialize chain with options.
|
|
/// </summary>
|
|
public static IDeserializeChain<T> BinaryToChain<T>(this byte[] data, AcBinarySerializerOptions options)
|
|
=> AcBinaryDeserializer.CreateDeserializeChain<T>(data, options);
|
|
|
|
/// <summary>
|
|
/// Create a populate chain that parses binary data once and allows populating multiple objects.
|
|
/// Efficient for populating multiple objects from the same binary source.
|
|
/// Use with 'using' statement or call Dispose() when done.
|
|
/// </summary>
|
|
public static IDeserializeChain<T> BinaryToChain<T>(this byte[] data, T target) where T : class
|
|
{
|
|
var chain = AcBinaryDeserializer.CreateDeserializeChain<T>(data);
|
|
chain.ThenPopulate(target);
|
|
return chain;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a populate chain with options.
|
|
/// </summary>
|
|
public static IDeserializeChain<T> BinaryToChain<T>(this byte[] data, T target, AcBinarySerializerOptions options) where T : class
|
|
{
|
|
var chain = AcBinaryDeserializer.CreateDeserializeChain<T>(data, options);
|
|
chain.ThenPopulate(target);
|
|
return chain;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Clone and Copy (Binary-based, zero intermediate allocation)
|
|
|
|
/// <summary>
|
|
/// Clone object via binary serialization (zero intermediate byte[] allocation).
|
|
/// Uses ArrayBufferWriter to serialize directly into a buffer, then deserializes from the span.
|
|
/// </summary>
|
|
public static TDestination? CloneTo<TDestination>(this object? src) where TDestination : class
|
|
{
|
|
if (src == null) return null;
|
|
|
|
var buffer = new ArrayBufferWriter<byte>(256);
|
|
AcBinarySerializer.Serialize(src, buffer, AcBinarySerializerOptions.Default);
|
|
MemoryMarshal.TryGetArray<byte>(buffer.WrittenMemory, out var seg);
|
|
return AcBinaryDeserializer.Deserialize<TDestination>(seg.Array!, seg.Offset, seg.Count);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copy object properties to target via binary serialization (zero intermediate byte[] allocation).
|
|
/// Uses ArrayBufferWriter to serialize directly into a buffer, then populates target from the backing array.
|
|
/// </summary>
|
|
public static void CopyTo(this object? src, object target)
|
|
{
|
|
if (src == null) return;
|
|
|
|
var buffer = new ArrayBufferWriter<byte>(256);
|
|
AcBinarySerializer.Serialize(src, buffer, AcBinarySerializerOptions.Default);
|
|
MemoryMarshal.TryGetArray<byte>(buffer.WrittenMemory, out var seg);
|
|
AcBinaryDeserializer.Populate(seg.Array!, seg.Offset, seg.Count, target);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
public class IgnoreAndRenamePropertySerializerContractResolver : DefaultContractResolver
|
|
{
|
|
private readonly Dictionary<Type, HashSet<string>> _ignores = new();
|
|
private readonly Dictionary<Type, HashSet<string>> _includes = new();
|
|
private readonly Dictionary<Type, Dictionary<string, string>> _renames = new();
|
|
|
|
public void IgnoreProperty(Type type, params string[] jsonPropertyNames)
|
|
{
|
|
if (!_ignores.TryGetValue(type, out var set)) { set = new HashSet<string>(StringComparer.Ordinal); _ignores[type] = set; }
|
|
foreach (var prop in jsonPropertyNames) set.Add(prop);
|
|
}
|
|
|
|
public void IncludesProperty(Type type, params string[] jsonPropertyNames)
|
|
{
|
|
if (!_includes.TryGetValue(type, out var set)) { set = new HashSet<string>(StringComparer.Ordinal); _includes[type] = set; }
|
|
foreach (var prop in jsonPropertyNames) set.Add(prop);
|
|
}
|
|
|
|
public void RenameProperty(Type type, string propertyName, string newJsonPropertyName)
|
|
{
|
|
if (!_renames.TryGetValue(type, out var dict)) { dict = new Dictionary<string, string>(StringComparer.Ordinal); _renames[type] = dict; }
|
|
dict[propertyName] = newJsonPropertyName;
|
|
}
|
|
|
|
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
|
|
{
|
|
var property = base.CreateProperty(member, memberSerialization);
|
|
if (IsIgnored(property.DeclaringType, property.PropertyName) || !IsIncluded(property.DeclaringType, property.PropertyName))
|
|
{ property.ShouldSerialize = _ => false; property.Ignored = true; }
|
|
if (IsRenamed(property.DeclaringType, property.PropertyName, out var newName)) property.PropertyName = newName;
|
|
return property;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private bool IsIgnored(Type? type, string? name) => type != null && name != null && _ignores.TryGetValue(type, out var set) && set.Contains(name);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private bool IsIncluded(Type? type, string? name) => _includes.Count == 0 || (type != null && name != null && _includes.TryGetValue(type, out var set) && set.Contains(name));
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private bool IsRenamed(Type? type, string? name, out string? newName)
|
|
{
|
|
if (type != null && name != null && _renames.TryGetValue(type, out var renames) && renames.TryGetValue(name, out newName)) return true;
|
|
newName = null; return false;
|
|
}
|
|
}
|