289 lines
9.5 KiB
C#
289 lines
9.5 KiB
C#
using System.Buffers;
|
|
using System.Runtime.CompilerServices;
|
|
using AyCode.Core.Compression;
|
|
using AyCode.Core.Extensions;
|
|
using AyCode.Core.Serializers;
|
|
using AyCode.Core.Serializers.Binaries;
|
|
using AyCode.Core.Serializers.Jsons;
|
|
|
|
namespace AyCode.Services.SignalRs;
|
|
|
|
/// <summary>
|
|
/// Centralized helper for SignalR serialization operations.
|
|
/// Provides optimized primitives for JSON/Binary serialization with pooled buffers.
|
|
/// </summary>
|
|
public static class SignalRSerializationHelper
|
|
{
|
|
// Pre-boxed boolean values to avoid repeated boxing
|
|
private static readonly string JsonTrue = "true";
|
|
private static readonly string JsonFalse = "false";
|
|
|
|
#region Primitive JSON Serialization
|
|
|
|
/// <summary>
|
|
/// Serialize a primitive value to JSON string with minimal overhead.
|
|
/// Falls back to full JSON serialization for complex types.
|
|
/// </summary>
|
|
[Obsolete("Use direct object[] binary serialization instead of JSON-encoded primitives.")]
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static string SerializePrimitiveToJson(object value)
|
|
{
|
|
return value switch
|
|
{
|
|
int i => i.ToString(),
|
|
long l => l.ToString(),
|
|
Guid g => SerializeGuidToJson(g),
|
|
bool b => b ? JsonTrue : JsonFalse,
|
|
// Strings need proper JSON escaping for special characters
|
|
string => value.ToJson(),
|
|
_ => value.ToJson()
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serialize a Guid to JSON string with pre-allocated buffer.
|
|
/// </summary>
|
|
[Obsolete("Use direct object[] binary serialization instead of JSON-encoded Guids.")]
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static string SerializeGuidToJson(Guid g)
|
|
{
|
|
// Pre-allocate exact size: 38 chars = 2 quotes + 36 guid chars
|
|
return string.Create(38, g, static (span, guid) =>
|
|
{
|
|
span[0] = '"';
|
|
guid.TryFormat(span[1..], out _);
|
|
span[37] = '"';
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Binary Serialization
|
|
|
|
/// <summary>
|
|
/// Serialize object to binary using pooled ArrayBufferWriter.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static byte[] SerializeToBinary<T>(T value, AcBinarySerializerOptions? options = null)
|
|
{
|
|
var writer = new ArrayBufferWriter<byte>(256);
|
|
value.ToBinary(writer, options ?? AcBinarySerializerOptions.Default);
|
|
return writer.WrittenSpan.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serialize object to binary and write to existing ArrayBufferWriter.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void SerializeToBinary<T>(T value, ArrayBufferWriter<byte> writer, AcBinarySerializerOptions? options = null)
|
|
{
|
|
value.ToBinary(writer, options ?? AcBinarySerializerOptions.Default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize binary data to object.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static T? DeserializeFromBinary<T>(byte[] data)
|
|
{
|
|
return data.BinaryTo<T>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize binary data from ReadOnlySpan.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static T? DeserializeFromBinary<T>(ReadOnlySpan<byte> data)
|
|
{
|
|
return data.BinaryTo<T>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serialize method parameters as individually length-prefixed binary segments.
|
|
/// Format: [VarUInt count] [for each: INT32 length + AcBinary bytes]
|
|
/// This allows the server to deserialize each parameter with its known target type.
|
|
/// </summary>
|
|
public static byte[] SerializeParametersToBinary(object[] parameters, AcBinarySerializerOptions? options = null)
|
|
{
|
|
var opts = options ?? AcBinarySerializerOptions.Default;
|
|
var writer = new ArrayBufferWriter<byte>(256);
|
|
|
|
// Write parameter count as VarUInt
|
|
WriteVarUInt(writer, (uint)parameters.Length);
|
|
|
|
// Write each parameter with INT32 length prefix
|
|
for (var i = 0; i < parameters.Length; i++)
|
|
{
|
|
var paramWriter = new ArrayBufferWriter<byte>(128);
|
|
parameters[i].ToBinary(paramWriter, opts);
|
|
|
|
// Write length prefix
|
|
var lenSpan = writer.GetSpan(4);
|
|
System.Runtime.CompilerServices.Unsafe.WriteUnaligned(ref lenSpan[0], paramWriter.WrittenCount);
|
|
writer.Advance(4);
|
|
|
|
// Write parameter bytes
|
|
var paramSpan = writer.GetSpan(paramWriter.WrittenCount);
|
|
paramWriter.WrittenSpan.CopyTo(paramSpan);
|
|
writer.Advance(paramWriter.WrittenCount);
|
|
}
|
|
|
|
return writer.WrittenSpan.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize method parameters from length-prefixed binary format, using target types.
|
|
/// </summary>
|
|
public static object?[] DeserializeParametersFromBinary(byte[] data, System.Reflection.ParameterInfo[] paramInfos)
|
|
{
|
|
var span = data.AsSpan();
|
|
var pos = 0;
|
|
|
|
// Read parameter count
|
|
var count = (int)ReadVarUInt(span, ref pos);
|
|
var result = new object?[paramInfos.Length];
|
|
|
|
for (var i = 0; i < count && i < paramInfos.Length; i++)
|
|
{
|
|
// Read length prefix
|
|
var len = System.Runtime.CompilerServices.Unsafe.ReadUnaligned<int>(ref System.Runtime.InteropServices.MemoryMarshal.GetReference(span.Slice(pos)));
|
|
pos += 4;
|
|
|
|
if (len > 0)
|
|
{
|
|
var paramBytes = span.Slice(pos, len).ToArray();
|
|
result[i] = paramBytes.BinaryTo(paramInfos[i].ParameterType);
|
|
pos += len;
|
|
}
|
|
}
|
|
|
|
// Fill remaining with defaults
|
|
for (var i = count; i < paramInfos.Length; i++)
|
|
{
|
|
if (paramInfos[i].HasDefaultValue)
|
|
result[i] = paramInfos[i].DefaultValue;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static void WriteVarUInt(ArrayBufferWriter<byte> writer, uint value)
|
|
{
|
|
while (value >= 0x80)
|
|
{
|
|
var span = writer.GetSpan(1);
|
|
span[0] = (byte)(value | 0x80);
|
|
writer.Advance(1);
|
|
value >>= 7;
|
|
}
|
|
var lastSpan = writer.GetSpan(1);
|
|
lastSpan[0] = (byte)value;
|
|
writer.Advance(1);
|
|
}
|
|
|
|
private static uint ReadVarUInt(ReadOnlySpan<byte> span, ref int pos)
|
|
{
|
|
uint value = 0;
|
|
var shift = 0;
|
|
while (true)
|
|
{
|
|
var b = span[pos++];
|
|
value |= (uint)(b & 0x7F) << shift;
|
|
if ((b & 0x80) == 0)
|
|
return value;
|
|
shift += 7;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region JSON Serialization with Brotli
|
|
|
|
/// <summary>
|
|
/// Serialize object to JSON and compress with Brotli.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static byte[] SerializeToCompressedJson<T>(T value, AcJsonSerializerOptions? options = null)
|
|
{
|
|
var json = value.ToJson(options ?? AcJsonSerializerOptions.Default);
|
|
return GzipHelper.Compress(json);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decompress Brotli data and deserialize JSON to object.
|
|
/// Uses pooled buffer for decompression.
|
|
/// </summary>
|
|
public static T? DeserializeFromCompressedJson<T>(byte[] compressedData)
|
|
{
|
|
var (buffer, length) = GzipHelper.DecompressToRentedBuffer(compressedData.AsSpan());
|
|
try
|
|
{
|
|
return AcJsonDeserializer.Deserialize<T>(new ReadOnlySpan<byte>(buffer, 0, length));
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(buffer);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decompress Brotli data to rented buffer for direct processing.
|
|
/// Caller must return buffer to ArrayPool.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static (byte[] Buffer, int Length) DecompressToRentedBuffer(byte[] compressedData)
|
|
{
|
|
return GzipHelper.DecompressToRentedBuffer(compressedData.AsSpan());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Response Data Helpers
|
|
|
|
/// <summary>
|
|
/// Check if string appears to be valid JSON (starts with { or [).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool IsValidJsonString(ReadOnlySpan<char> text)
|
|
{
|
|
var trimmed = text.Trim();
|
|
return trimmed.Length > 1 &&
|
|
(trimmed[0] == '{' || trimmed[0] == '[') &&
|
|
(trimmed[^1] == '}' || trimmed[^1] == ']');
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create response binary data based on serializer type.
|
|
/// </summary>
|
|
public static byte[]? CreateResponseData(object? responseData, AcSerializerOptions serializerOptions)
|
|
{
|
|
if (responseData == null)
|
|
return null;
|
|
|
|
if (serializerOptions.SerializerType == AcSerializerType.Binary)
|
|
{
|
|
if (responseData is byte[] byteData)
|
|
return byteData;
|
|
|
|
var binaryOptions = serializerOptions as AcBinarySerializerOptions ?? AcBinarySerializerOptions.Default;
|
|
return SerializeToBinary(responseData, binaryOptions);
|
|
}
|
|
|
|
// JSON mode with Brotli compression
|
|
string json;
|
|
if (responseData is string strData && IsValidJsonString(strData.AsSpan()))
|
|
{
|
|
json = strData;
|
|
}
|
|
else
|
|
{
|
|
var jsonOptions = serializerOptions as AcJsonSerializerOptions ?? AcJsonSerializerOptions.Default;
|
|
json = responseData.ToJson(jsonOptions);
|
|
}
|
|
|
|
return GzipHelper.Compress(json);
|
|
}
|
|
|
|
#endregion
|
|
}
|