438 lines
16 KiB
C#
438 lines
16 KiB
C#
using AyCode.Core.Extensions;
|
|
using AyCode.Core.Interfaces;
|
|
using System.Buffers;
|
|
using System.Runtime.CompilerServices;
|
|
using AyCode.Core.Serializers.Jsons;
|
|
using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute;
|
|
using STJIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
|
|
|
namespace AyCode.Services.SignalRs;
|
|
|
|
/// <summary>
|
|
/// Message container for serialized parameter IDs.
|
|
/// Optimized for common primitive types to avoid full JSON overhead.
|
|
/// </summary>
|
|
public class IdMessage
|
|
{
|
|
public List<string> Ids { get; private set; }
|
|
|
|
public IdMessage()
|
|
{
|
|
Ids = [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates IdMessage with multiple parameters serialized directly as JSON.
|
|
/// </summary>
|
|
public IdMessage(object[] ids)
|
|
{
|
|
Ids = new List<string>(ids.Length);
|
|
for (var i = 0; i < ids.Length; i++)
|
|
{
|
|
Ids.Add(SignalRSerializationHelper.SerializePrimitiveToJson(ids[i]));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates IdMessage with a single parameter serialized as JSON.
|
|
/// </summary>
|
|
public IdMessage(object id)
|
|
{
|
|
Ids = [SignalRSerializationHelper.SerializePrimitiveToJson(id)];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates IdMessage with multiple Guid parameters.
|
|
/// </summary>
|
|
public IdMessage(IEnumerable<Guid> ids)
|
|
{
|
|
var idsArray = ids as Guid[] ?? ids.ToArray();
|
|
Ids = new List<string>(idsArray.Length);
|
|
for (var i = 0; i < idsArray.Length; i++)
|
|
{
|
|
Ids.Add(SignalRSerializationHelper.SerializeGuidToJson(idsArray[i]));
|
|
}
|
|
}
|
|
|
|
public override string ToString() => string.Join("; ", Ids);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Message containing JSON-serialized post data.
|
|
/// </summary>
|
|
public class SignalPostJsonMessage
|
|
{
|
|
public string PostDataJson { get; set; } = "";
|
|
|
|
public SignalPostJsonMessage() { }
|
|
protected SignalPostJsonMessage(string postDataJson) => PostDataJson = postDataJson;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generic message containing JSON-serialized post data with typed access.
|
|
/// </summary>
|
|
public class SignalPostJsonDataMessage<TPostDataType> : SignalPostJsonMessage, ISignalPostMessage<TPostDataType>
|
|
{
|
|
[JsonIgnore]
|
|
[STJIgnore]
|
|
private TPostDataType? _postData;
|
|
|
|
[JsonIgnore]
|
|
[STJIgnore]
|
|
public TPostDataType PostData
|
|
{
|
|
get => _postData ??= PostDataJson.JsonTo<TPostDataType>()!;
|
|
private init
|
|
{
|
|
_postData = value;
|
|
PostDataJson = _postData.ToJson();
|
|
}
|
|
}
|
|
|
|
public SignalPostJsonDataMessage() : base() { }
|
|
public SignalPostJsonDataMessage(TPostDataType postData) => PostData = postData;
|
|
public SignalPostJsonDataMessage(string postDataJson) : base(postDataJson) { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple message containing post data.
|
|
/// </summary>
|
|
public class SignalPostMessage<TPostData>(TPostData postData) : ISignalPostMessage<TPostData>
|
|
{
|
|
public TPostData? PostData { get; set; } = postData;
|
|
}
|
|
|
|
public interface ISignalPostMessage<TPostData> : ISignalRMessage
|
|
{
|
|
TPostData? PostData { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Message for requesting by Guid ID.
|
|
/// </summary>
|
|
public class SignalRequestByIdMessage(Guid id) : ISignalRequestMessage<Guid>, IId<Guid>
|
|
{
|
|
public Guid Id { get; set; } = id;
|
|
}
|
|
|
|
public interface ISignalRequestMessage<TRequestId> : ISignalRMessage
|
|
{
|
|
TRequestId Id { get; set; }
|
|
}
|
|
|
|
public interface ISignalRMessage { }
|
|
|
|
public interface ISignalResponseMessage : ISignalRMessage
|
|
{
|
|
int MessageTag { get; set; }
|
|
SignalResponseStatus Status { get; set; }
|
|
}
|
|
|
|
public enum SignalResponseStatus : byte
|
|
{
|
|
Error = 0,
|
|
Success = 5
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unified signal response message that supports both JSON and Binary serialization.
|
|
/// JSON mode uses Brotli compression for reduced payload size.
|
|
/// Optimized: uses pooled buffers for decompression, zero-copy deserialization path.
|
|
/// </summary>
|
|
public sealed class SignalResponseDataMessage : ISignalResponseMessage, IDisposable
|
|
{
|
|
public int MessageTag { get; set; }
|
|
public SignalResponseStatus Status { get; set; }
|
|
public AcSerializerType DataSerializerType { get; set; }
|
|
public byte[]? ResponseData { get; set; }
|
|
|
|
[JsonIgnore] [STJIgnore] private object? _cachedResponseData;
|
|
[JsonIgnore] [STJIgnore] private byte[]? _rentedDecompressedBuffer;
|
|
[JsonIgnore] [STJIgnore] private int _decompressedLength;
|
|
|
|
/// <summary>
|
|
/// Enable diagnostic logging for ResponseData deserialization.
|
|
/// When set, logs hex dump and header info before deserialization.
|
|
/// </summary>
|
|
[JsonIgnore] [STJIgnore]
|
|
public static Action<string>? DiagnosticLogger { get; set; }
|
|
|
|
public SignalResponseDataMessage() { }
|
|
|
|
public SignalResponseDataMessage(int messageTag, SignalResponseStatus status)
|
|
{
|
|
MessageTag = messageTag;
|
|
Status = status;
|
|
}
|
|
|
|
public SignalResponseDataMessage(int messageTag, SignalResponseStatus status, object? responseData, AcSerializerOptions serializerOptions)
|
|
: this(messageTag, status)
|
|
{
|
|
DataSerializerType = serializerOptions.SerializerType;
|
|
ResponseData = SignalRSerializationHelper.CreateResponseData(responseData, serializerOptions);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserializes the ResponseData to the specified type.
|
|
/// Uses cached result for repeated calls.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public T? GetResponseData<T>()
|
|
{
|
|
if (_cachedResponseData != null) return (T)_cachedResponseData;
|
|
if (ResponseData == null) return default;
|
|
|
|
if (DataSerializerType == AcSerializerType.Binary)
|
|
{
|
|
try
|
|
{
|
|
// Log diagnostics if enabled
|
|
LogResponseDataDiagnostics<T>();
|
|
|
|
return (T)(_cachedResponseData = ResponseData.BinaryTo<T>()!);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log detailed error diagnostics
|
|
LogResponseDataError<T>(ex);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// Decompress Brotli to pooled buffer and deserialize directly
|
|
EnsureDecompressed();
|
|
var result = AcJsonDeserializer.Deserialize<T>(new ReadOnlySpan<byte>(_rentedDecompressedBuffer, 0, _decompressedLength));
|
|
_cachedResponseData = result;
|
|
return result;
|
|
}
|
|
|
|
private void LogResponseDataDiagnostics<T>()
|
|
{
|
|
if (DiagnosticLogger == null || ResponseData == null) return;
|
|
|
|
try
|
|
{
|
|
var targetType = typeof(T);
|
|
DiagnosticLogger($"=== RESPONSE DATA DIAGNOSTICS (DESERIALIZE) ===");
|
|
DiagnosticLogger($"Target Type: {targetType.Name}");
|
|
DiagnosticLogger($"Target FullName: {targetType.FullName}");
|
|
DiagnosticLogger($"Target Namespace: {targetType.Namespace}");
|
|
DiagnosticLogger($"Target Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}");
|
|
DiagnosticLogger($"Target AssemblyQualifiedName: {targetType.AssemblyQualifiedName}");
|
|
DiagnosticLogger($"Target Assembly Location: {targetType.Assembly.Location}");
|
|
|
|
// Log element type for collections
|
|
if (targetType.IsGenericType)
|
|
{
|
|
var genericArgs = targetType.GetGenericArguments();
|
|
DiagnosticLogger($"Generic Arguments: [{string.Join(", ", genericArgs.Select(t => t.FullName))}]");
|
|
if (genericArgs.Length == 1)
|
|
{
|
|
var elementType = genericArgs[0];
|
|
DiagnosticLogger($"--- ELEMENT TYPE INFO ---");
|
|
DiagnosticLogger($"Element Type: {elementType.Name}");
|
|
DiagnosticLogger($"Element FullName: {elementType.FullName}");
|
|
DiagnosticLogger($"Element Namespace: {elementType.Namespace}");
|
|
DiagnosticLogger($"Element Assembly: {elementType.Assembly.GetName().Name} v{elementType.Assembly.GetName().Version}");
|
|
DiagnosticLogger($"Element AssemblyQualifiedName: {elementType.AssemblyQualifiedName}");
|
|
DiagnosticLogger($"Element Assembly Location: {elementType.Assembly.Location}");
|
|
DiagnosticLogger($"Element BaseType: {elementType.BaseType?.FullName ?? "null"}");
|
|
|
|
// Log inheritance chain
|
|
var baseType = elementType.BaseType;
|
|
var inheritanceChain = new List<string>();
|
|
while (baseType != null && baseType != typeof(object))
|
|
{
|
|
inheritanceChain.Add($"{baseType.Name} ({baseType.Assembly.GetName().Name})");
|
|
baseType = baseType.BaseType;
|
|
}
|
|
if (inheritanceChain.Count > 0)
|
|
{
|
|
DiagnosticLogger($"Element Inheritance: {string.Join(" -> ", inheritanceChain)}");
|
|
}
|
|
|
|
LogTypeProperties(elementType, "Element");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DiagnosticLogger($"BaseType: {targetType.BaseType?.FullName ?? "null"}");
|
|
LogTypeProperties(targetType, "Target");
|
|
}
|
|
|
|
DiagnosticLogger($"ResponseData.Length: {ResponseData.Length}");
|
|
DiagnosticLogger($"HEX (first 500 bytes): {Convert.ToHexString(ResponseData.AsSpan(0, Math.Min(500, ResponseData.Length)))}");
|
|
|
|
// Parse header with VarInt support
|
|
LogBinaryHeader(ResponseData);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
DiagnosticLogger($"Failed to log diagnostics: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void LogTypeProperties(Type type, string prefix)
|
|
{
|
|
if (DiagnosticLogger == null) return;
|
|
|
|
var props = type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
|
.Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
|
|
.ToArray();
|
|
|
|
// Log in declaration order (not alphabetically) to match serialization order
|
|
DiagnosticLogger($"{prefix} Property Count: {props.Length}");
|
|
for (int i = 0; i < props.Length; i++)
|
|
{
|
|
var p = props[i];
|
|
var declaringType = p.DeclaringType?.Name ?? "?";
|
|
var declaringAssembly = p.DeclaringType?.Assembly.GetName().Name ?? "?";
|
|
DiagnosticLogger($" {prefix}[{i}]: {p.Name} : {p.PropertyType.Name} (declared in {declaringType} @ {declaringAssembly})");
|
|
}
|
|
}
|
|
|
|
private void LogBinaryHeader(byte[] data)
|
|
{
|
|
if (DiagnosticLogger == null || data.Length < 3) return;
|
|
|
|
var version = data[0];
|
|
var marker = data[1];
|
|
DiagnosticLogger($"Binary Version: {version}; Marker: 0x{marker:X2}");
|
|
|
|
// Check if metadata flag is set
|
|
if ((marker & 0x10) == 0)
|
|
{
|
|
DiagnosticLogger("Header: No metadata (property names inline)");
|
|
return;
|
|
}
|
|
|
|
// Read property count as VarUInt
|
|
var pos = 2;
|
|
var (propCount, bytesRead) = ReadVarUIntFromSpan(data.AsSpan(pos));
|
|
pos += bytesRead;
|
|
|
|
DiagnosticLogger($"Header Property Count: {propCount}");
|
|
|
|
for (int i = 0; i < (int)propCount && pos < data.Length; i++)
|
|
{
|
|
// Read string length as VarUInt
|
|
var (strLen, strLenBytes) = ReadVarUIntFromSpan(data.AsSpan(pos));
|
|
pos += strLenBytes;
|
|
|
|
if (pos + (int)strLen <= data.Length)
|
|
{
|
|
var propName = System.Text.Encoding.UTF8.GetString(data, pos, (int)strLen);
|
|
pos += (int)strLen;
|
|
DiagnosticLogger($" Header[{i}]: '{propName}'");
|
|
}
|
|
else
|
|
{
|
|
DiagnosticLogger($" Header[{i}]: <truncated at pos {pos}>");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static (uint value, int bytesRead) ReadVarUIntFromSpan(ReadOnlySpan<byte> span)
|
|
{
|
|
uint value = 0;
|
|
int shift = 0;
|
|
int bytesRead = 0;
|
|
|
|
while (bytesRead < span.Length)
|
|
{
|
|
var b = span[bytesRead++];
|
|
value |= (uint)(b & 0x7F) << shift;
|
|
if ((b & 0x80) == 0)
|
|
break;
|
|
shift += 7;
|
|
if (shift > 35)
|
|
break;
|
|
}
|
|
|
|
return (value, bytesRead);
|
|
}
|
|
|
|
private void LogResponseDataError<T>(Exception error)
|
|
{
|
|
if (DiagnosticLogger == null || ResponseData == null) return;
|
|
|
|
try
|
|
{
|
|
var targetType = typeof(T);
|
|
DiagnosticLogger($"=== RESPONSE DATA DESERIALIZATION ERROR ===");
|
|
DiagnosticLogger($"Error: {error.Message}");
|
|
DiagnosticLogger($"Target Type: {targetType.Name}");
|
|
DiagnosticLogger($"Target FullName: {targetType.FullName}");
|
|
DiagnosticLogger($"Target Namespace: {targetType.Namespace}");
|
|
DiagnosticLogger($"Target Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}");
|
|
DiagnosticLogger($"Target AssemblyQualifiedName: {targetType.AssemblyQualifiedName}");
|
|
|
|
// Log element type for collections
|
|
if (targetType.IsGenericType)
|
|
{
|
|
var genericArgs = targetType.GetGenericArguments();
|
|
DiagnosticLogger($"Generic Arguments: [{string.Join(", ", genericArgs.Select(t => t.FullName))}]");
|
|
if (genericArgs.Length == 1)
|
|
{
|
|
var elementType = genericArgs[0];
|
|
DiagnosticLogger($"Element Type: {elementType.FullName}");
|
|
DiagnosticLogger($"Element Assembly: {elementType.Assembly.GetName().Name}");
|
|
LogTypeProperties(elementType, "Element");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogTypeProperties(targetType, "Target");
|
|
}
|
|
|
|
DiagnosticLogger($"ResponseData.Length: {ResponseData.Length}");
|
|
DiagnosticLogger($"HEX (first 1000 bytes): {Convert.ToHexString(ResponseData.AsSpan(0, Math.Min(1000, ResponseData.Length)))}");
|
|
|
|
// Parse header
|
|
LogBinaryHeader(ResponseData);
|
|
|
|
// Log inner exception if present
|
|
if (error.InnerException != null)
|
|
{
|
|
DiagnosticLogger($"Inner Exception: {error.InnerException.Message}");
|
|
}
|
|
|
|
// Log stack trace
|
|
DiagnosticLogger($"Stack Trace: {error.StackTrace}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
DiagnosticLogger?.Invoke($"Failed to log error diagnostics: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the decompressed JSON bytes as a ReadOnlySpan for direct processing.
|
|
/// </summary>
|
|
public ReadOnlySpan<byte> GetDecompressedJsonSpan()
|
|
{
|
|
if (ResponseData == null) return ReadOnlySpan<byte>.Empty;
|
|
if (DataSerializerType == AcSerializerType.Binary) return ReadOnlySpan<byte>.Empty;
|
|
|
|
EnsureDecompressed();
|
|
return _rentedDecompressedBuffer.AsSpan(0, _decompressedLength);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void EnsureDecompressed()
|
|
{
|
|
if (_rentedDecompressedBuffer != null) return;
|
|
|
|
(_rentedDecompressedBuffer, _decompressedLength) = SignalRSerializationHelper.DecompressToRentedBuffer(ResponseData!);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
public interface IAcSignalRHubClient : IAcSignalRHubBase
|
|
{
|
|
Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId);
|
|
} |