114 lines
5.0 KiB
C#
114 lines
5.0 KiB
C#
using System.Reflection;
|
||
using AyCode.Core.Extensions;
|
||
using AyCode.Core.Serializers;
|
||
using AyCode.Core.Serializers.Attributes;
|
||
using AyCode.Core.Serializers.Binaries;
|
||
|
||
namespace AyCode.Services.SignalRs;
|
||
|
||
/// <summary>
|
||
/// Base interface for SignalR message parameters (metadata).
|
||
/// </summary>
|
||
public interface ISignalParams { }
|
||
|
||
/// <summary>
|
||
/// SignalR message metadata + optional method parameters.
|
||
/// Travels as a separate hub argument (AcBinary serialized).
|
||
/// Parameters and data are independent — both can be null or filled in any direction.
|
||
///
|
||
/// Parameter serialization follows the PostDataJson pattern:
|
||
/// - Wire format: Parameters (byte[]) — AcBinary fast-path
|
||
/// - Client: SetParameterValues(object[]) packs object[] → byte[][] → byte[]
|
||
/// - Server: GetParameterValues(ParameterInfo[]) unpacks byte[] → byte[][] → object[]
|
||
/// The protocol never sees byte[][] — only byte[].
|
||
/// </summary>
|
||
[AcBinarySerializable]
|
||
public class SignalParams : ISignalParams
|
||
{
|
||
public SignalResponseStatus Status { get; set; }
|
||
public AcSerializerType DataSerializerType { get; set; }
|
||
|
||
/// <summary>
|
||
/// Wire format: serialized byte[][] as a single byte[].
|
||
/// AcBinary handles this as byte[] fast-path (zero-copy potential).
|
||
/// Use SetParameterValues/GetParameterValues for typed access.
|
||
/// </summary>
|
||
public byte[]? Parameters { get; set; }
|
||
|
||
/// <summary>
|
||
/// Client sets true when requesting raw byte[] (e.g. DataSource populate/merge).
|
||
/// Server: reads this from client's SignalParams → serializes object → byte[] directly.
|
||
/// Protocol: reads this from response SignalParams → skips deserialization, returns raw byte[].
|
||
/// Result: single serialize (server) → single deserialize (consumer). No double ser/deser.
|
||
/// </summary>
|
||
public bool IsRawBytesData { get; set; }
|
||
|
||
/// <summary>
|
||
/// Cached deserialized byte[][] from Parameters.
|
||
/// </summary>
|
||
private byte[][]? _parameterValues;
|
||
|
||
/// <summary>
|
||
/// Client-side: packs object[] into Parameters (byte[]).
|
||
/// Each parameter is individually binary-serialized via ToBinary().
|
||
///
|
||
/// PERF: N× ToBinary() = N× context pool acquire/release + N× ArrayBinaryOutput allocation.
|
||
/// For many small primitives (int, bool) this overhead may exceed a single bulk serialization call.
|
||
/// Consider a batch fast-path (single context, write all params) if benchmarks confirm regression.
|
||
/// </summary>
|
||
public void SetParameterValues(object[] parameters)
|
||
{
|
||
// N× pool roundtrip — see PERF note in summary
|
||
var paramBytes = new byte[parameters.Length][];
|
||
for (var i = 0; i < parameters.Length; i++)
|
||
{
|
||
// Pass explicit runtime type — parameters[i] is statically object, so the generic
|
||
// ToBinary<T>() overload would infer T = object and emit an object-typed wire payload
|
||
// (instead of the concrete int/string/DTO type). Explicit null-handling avoids the
|
||
// null-conditional + fallback indirection and produces the Null marker directly.
|
||
var p = parameters[i];
|
||
paramBytes[i] = p == null ? [BinaryTypeCode.Null] : p.ToBinary(p.GetType());
|
||
}
|
||
|
||
_parameterValues = paramBytes;
|
||
Parameters = paramBytes.ToBinary();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Server-side: unpacks Parameters (byte[]) into typed object[].
|
||
/// Each parameter is deserialized with BinaryTo(targetType) from method signature.
|
||
/// Fills trailing optional parameters with defaults, throws for missing required ones.
|
||
/// Caches the intermediate byte[][] for repeated access.
|
||
///
|
||
/// NOTE: Currently AcBinary only. JSON parameter deserialization not supported
|
||
/// (would require dispatching on serializer type + AcJsonSerializer reference).
|
||
///
|
||
/// PERF: Non-generic BinaryTo(Type) uses ThreadLocal + ConcurrentDictionary type cache per call.
|
||
/// N parameters = N× pool roundtrip + N× type-dispatch lookup.
|
||
/// For many small primitives this may be slower than a single bulk deserialization.
|
||
/// Consider a batch fast-path (single context, read all params) if benchmarks confirm regression.
|
||
/// </summary>
|
||
public object[]? GetParameterValues(ParameterInfo[] paramInfos)
|
||
{
|
||
if (paramInfos is not { Length: > 0 })
|
||
return null;
|
||
|
||
// Deserialize byte[] → byte[][] (cached)
|
||
_parameterValues ??= Parameters is { Length: > 0 } ? Parameters.BinaryTo<byte[][]>() : null;
|
||
|
||
if (_parameterValues is null or { Length: 0 })
|
||
return null;
|
||
|
||
// N× pool roundtrip + type-dispatch — see PERF note in summary
|
||
var result = new object?[paramInfos.Length];
|
||
for (var i = 0; i < _parameterValues.Length && i < paramInfos.Length; i++)
|
||
result[i] = _parameterValues[i].BinaryTo(paramInfos[i].ParameterType);
|
||
|
||
// Fill trailing optional parameters with defaults
|
||
for (var i = _parameterValues.Length; i < paramInfos.Length; i++)
|
||
result[i] = paramInfos[i].HasDefaultValue ? paramInfos[i].DefaultValue : null;
|
||
|
||
return result;
|
||
}
|
||
}
|