Add binary serialization support for SignalR responses
Introduces SignalResponseBinaryMessage for efficient binary serialization of response data alongside existing JSON support. Adds utilities to detect serializer type and updates both server and client logic to handle JSON or binary formats automatically. Refactors response creation, logging, and deserialization for consistency and performance. Updates benchmarks and ensures all MessagePack operations use ContractlessStandardResolver.Options. Improves robustness and backward compatibility in client callback handling.
This commit is contained in:
parent
2147d981db
commit
09a4604e52
|
|
@ -15,11 +15,6 @@ public static class SignalRMessageFactory
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly MessagePackSerializerOptions ContractlessOptions = ContractlessStandardResolver.Options;
|
public static readonly MessagePackSerializerOptions ContractlessOptions = ContractlessStandardResolver.Options;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cached MessagePack options for Standard resolver
|
|
||||||
/// </summary>
|
|
||||||
public static readonly MessagePackSerializerOptions StandardOptions = MessagePackSerializerOptions.Standard;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a MessagePack message for multiple parameters using IdMessage format.
|
/// Creates a MessagePack message for multiple parameters using IdMessage format.
|
||||||
/// Each parameter is serialized directly as JSON.
|
/// Each parameter is serialized directly as JSON.
|
||||||
|
|
@ -47,7 +42,7 @@ public static class SignalRMessageFactory
|
||||||
{
|
{
|
||||||
var json = obj.ToJson();
|
var json = obj.ToJson();
|
||||||
var postMessage = new SignalRPostMessageDto { PostDataJson = json };
|
var postMessage = new SignalRPostMessageDto { PostDataJson = json };
|
||||||
return MessagePackSerializer.Serialize(postMessage, StandardOptions);
|
return MessagePackSerializer.Serialize(postMessage, ContractlessOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,73 @@ public static class JsonUtilities
|
||||||
|
|
||||||
#region Type Checking Methods
|
#region Type Checking Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detects the serializer type from the response data.
|
||||||
|
/// Checks the first byte after MessagePack deserialization to determine if it's JSON or Binary format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="responseData">The response data (string for JSON, byte[] for Binary)</param>
|
||||||
|
/// <returns>The detected serializer type</returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static AcSerializerType DetectSerializerType(object? responseData)
|
||||||
|
{
|
||||||
|
return responseData switch
|
||||||
|
{
|
||||||
|
byte[] => AcSerializerType.Binary,
|
||||||
|
string => AcSerializerType.Json,
|
||||||
|
null => AcSerializerType.Json, // Default to JSON for null
|
||||||
|
_ => AcSerializerType.Json // Default to JSON for unknown types
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detects if byte array contains JSON or Binary serialized data.
|
||||||
|
/// JSON typically starts with '{', '[', '"' or whitespace.
|
||||||
|
/// Binary format starts with version byte (typically 1) followed by metadata flag.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static AcSerializerType DetectSerializerTypeFromBytes(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
if (data.IsEmpty) return AcSerializerType.Json;
|
||||||
|
|
||||||
|
var firstByte = data[0];
|
||||||
|
|
||||||
|
// JSON typically starts with: '{' (123), '[' (91), '"' (34), or whitespace (32, 9, 10, 13)
|
||||||
|
// Also numbers: '0'-'9' (48-57), '-' (45), 't' (116 for true), 'f' (102 for false), 'n' (110 for null)
|
||||||
|
return firstByte switch
|
||||||
|
{
|
||||||
|
(byte)'{' or (byte)'[' or (byte)'"' => AcSerializerType.Json,
|
||||||
|
(byte)' ' or (byte)'\t' or (byte)'\n' or (byte)'\r' => AcSerializerType.Json,
|
||||||
|
>= (byte)'0' and <= (byte)'9' => AcSerializerType.Json,
|
||||||
|
(byte)'-' or (byte)'t' or (byte)'f' or (byte)'n' => AcSerializerType.Json,
|
||||||
|
// Binary format version 1 with metadata or no-metadata header
|
||||||
|
1 when data.Length > 1 && (data[1] == 32 || data[1] == 33) => AcSerializerType.Binary,
|
||||||
|
_ => AcSerializerType.Binary // Default to Binary for unknown byte patterns
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detects serializer type from a string (checks if it looks like JSON).
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static AcSerializerType DetectSerializerTypeFromString(string? data)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(data)) return AcSerializerType.Json;
|
||||||
|
|
||||||
|
var trimmed = data.AsSpan().Trim();
|
||||||
|
if (trimmed.IsEmpty) return AcSerializerType.Json;
|
||||||
|
|
||||||
|
var firstChar = trimmed[0];
|
||||||
|
|
||||||
|
// Valid JSON starts with: '{', '[', '"', number, 't', 'f', 'n'
|
||||||
|
return firstChar switch
|
||||||
|
{
|
||||||
|
'{' or '[' or '"' => AcSerializerType.Json,
|
||||||
|
>= '0' and <= '9' => AcSerializerType.Json,
|
||||||
|
'-' or 't' or 'f' or 'n' => AcSerializerType.Json,
|
||||||
|
_ => AcSerializerType.Binary // Likely Base64 encoded binary
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fast primitive check using type code.
|
/// Fast primitive check using type code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ public static class SignalRTestHelper
|
||||||
{
|
{
|
||||||
var json = obj.ToJson();
|
var json = obj.ToJson();
|
||||||
var postMessage = new SignalPostJsonDataMessage<object>(json);
|
var postMessage = new SignalPostJsonDataMessage<object>(json);
|
||||||
return MessagePackSerializer.Serialize(postMessage, MessagePackSerializerOptions.Standard);
|
return MessagePackSerializer.Serialize(postMessage, MessagePackOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -72,15 +72,15 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
||||||
|
|
||||||
if (TryFindAndInvokeMethod(messageTag, message, tagName, out var responseData))
|
if (TryFindAndInvokeMethod(messageTag, message, tagName, out var responseData))
|
||||||
{
|
{
|
||||||
var responseDataJson = new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, responseData);
|
var responseMessage = CreateResponseMessage(messageTag, SignalResponseStatus.Success, responseData);
|
||||||
|
|
||||||
if (Logger.LogLevel <= LogLevel.Debug)
|
if (Logger.LogLevel <= LogLevel.Debug)
|
||||||
{
|
{
|
||||||
var responseDataJsonKiloBytes = System.Text.Encoding.Unicode.GetByteCount(responseDataJson.ResponseData ?? "") / 1024;
|
var responseSize = GetResponseSize(responseMessage);
|
||||||
Logger.Debug($"[{responseDataJsonKiloBytes}kb] responseData serialized to json");
|
Logger.Debug($"[{responseSize / 1024}kb] responseData serialized ({SerializerOptions.SerializerType})");
|
||||||
}
|
}
|
||||||
|
|
||||||
await ResponseToCaller(messageTag, responseDataJson, requestId);
|
await ResponseToCaller(messageTag, responseMessage, requestId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +92,33 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
||||||
Logger.Error($"Server OnReceiveMessage; {ex.Message}; {tagName}", ex);
|
Logger.Error($"Server OnReceiveMessage; {ex.Message}; {tagName}", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Error), requestId);
|
await ResponseToCaller(messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Error, null), requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a response message using the configured serializer (JSON or Binary).
|
||||||
|
/// </summary>
|
||||||
|
protected virtual ISignalRMessage CreateResponseMessage(int messageTag, SignalResponseStatus status, object? responseData)
|
||||||
|
{
|
||||||
|
if (SerializerOptions.SerializerType == AcSerializerType.Binary)
|
||||||
|
{
|
||||||
|
return new SignalResponseBinaryMessage(messageTag, status, responseData, (AcBinarySerializerOptions)SerializerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SignalResponseJsonMessage(messageTag, status, responseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the size of the response data for logging purposes.
|
||||||
|
/// </summary>
|
||||||
|
private int GetResponseSize(ISignalRMessage responseMessage)
|
||||||
|
{
|
||||||
|
return responseMessage switch
|
||||||
|
{
|
||||||
|
SignalResponseJsonMessage jsonMsg => System.Text.Encoding.Unicode.GetByteCount(jsonMsg.ResponseData ?? ""),
|
||||||
|
SignalResponseBinaryMessage binaryMsg => binaryMsg.ResponseData?.Length ?? 0,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -197,28 +223,28 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
||||||
#region Response Methods
|
#region Response Methods
|
||||||
|
|
||||||
protected virtual Task ResponseToCallerWithContent(int messageTag, object? content)
|
protected virtual Task ResponseToCallerWithContent(int messageTag, object? content)
|
||||||
=> ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, content), null);
|
=> ResponseToCaller(messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null);
|
||||||
|
|
||||||
protected virtual Task ResponseToCaller(int messageTag, ISignalRMessage message, int? requestId)
|
protected virtual Task ResponseToCaller(int messageTag, ISignalRMessage message, int? requestId)
|
||||||
=> SendMessageToClient(Clients.Caller, messageTag, message, requestId);
|
=> SendMessageToClient(Clients.Caller, messageTag, message, requestId);
|
||||||
|
|
||||||
protected virtual Task SendMessageToUserIdWithContent(string userId, int messageTag, object? content)
|
protected virtual Task SendMessageToUserIdWithContent(string userId, int messageTag, object? content)
|
||||||
=> SendMessageToUserIdInternal(userId, messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, content), null);
|
=> SendMessageToUserIdInternal(userId, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null);
|
||||||
|
|
||||||
protected virtual Task SendMessageToUserIdInternal(string userId, int messageTag, ISignalRMessage message, int? requestId)
|
protected virtual Task SendMessageToUserIdInternal(string userId, int messageTag, ISignalRMessage message, int? requestId)
|
||||||
=> SendMessageToClient(Clients.User(userId), messageTag, message, requestId);
|
=> SendMessageToClient(Clients.User(userId), messageTag, message, requestId);
|
||||||
|
|
||||||
protected virtual Task SendMessageToConnectionIdWithContent(string connectionId, int messageTag, object? content)
|
protected virtual Task SendMessageToConnectionIdWithContent(string connectionId, int messageTag, object? content)
|
||||||
=> SendMessageToConnectionIdInternal(connectionId, messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, content), null);
|
=> SendMessageToConnectionIdInternal(connectionId, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null);
|
||||||
|
|
||||||
protected virtual Task SendMessageToConnectionIdInternal(string connectionId, int messageTag, ISignalRMessage message, int? requestId)
|
protected virtual Task SendMessageToConnectionIdInternal(string connectionId, int messageTag, ISignalRMessage message, int? requestId)
|
||||||
=> SendMessageToClient(Clients.Client(connectionId), messageTag, message, requestId);
|
=> SendMessageToClient(Clients.Client(connectionId), messageTag, message, requestId);
|
||||||
|
|
||||||
protected virtual Task SendMessageToOthers(int messageTag, object? content)
|
protected virtual Task SendMessageToOthers(int messageTag, object? content)
|
||||||
=> SendMessageToClient(Clients.Others, messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, content), null);
|
=> SendMessageToClient(Clients.Others, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null);
|
||||||
|
|
||||||
protected virtual Task SendMessageToAll(int messageTag, object? content)
|
protected virtual Task SendMessageToAll(int messageTag, object? content)
|
||||||
=> SendMessageToClient(Clients.All, messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, content), null);
|
=> SendMessageToClient(Clients.All, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null);
|
||||||
|
|
||||||
protected virtual async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null)
|
protected virtual async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Http.Connections;
|
using Microsoft.AspNetCore.Http.Connections;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static AyCode.Core.Extensions.JsonUtilities;
|
||||||
|
|
||||||
namespace AyCode.Services.SignalRs
|
namespace AyCode.Services.SignalRs
|
||||||
{
|
{
|
||||||
|
|
@ -338,26 +339,33 @@ namespace AyCode.Services.SignalRs
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (await TaskHelper.WaitToAsync(() => _responseByRequestId[requestId].ResponseByRequestId != null, TransportSendTimeout, MsDelay, MsFirstDelay) &&
|
if (await TaskHelper.WaitToAsync(() => _responseByRequestId[requestId].ResponseByRequestId != null, TransportSendTimeout, MsDelay, MsFirstDelay) &&
|
||||||
_responseByRequestId.TryRemove(requestId, out var obj) && obj.ResponseByRequestId is ISignalResponseMessage<string> responseMessage)
|
_responseByRequestId.TryRemove(requestId, out var obj) && obj.ResponseByRequestId is ISignalResponseMessage responseMessage)
|
||||||
{
|
{
|
||||||
startTime = obj.RequestDateTime;
|
startTime = obj.RequestDateTime;
|
||||||
SignalRRequestModelPool.Return(obj);
|
SignalRRequestModelPool.Return(obj);
|
||||||
|
|
||||||
if (responseMessage.Status == SignalResponseStatus.Error || responseMessage.ResponseData == null)
|
if (responseMessage.Status == SignalResponseStatus.Error)
|
||||||
{
|
{
|
||||||
var errorText = $"Client SendMessageToServerAsync<TResponseData> response error; await; tag: {messageTag}; Status: {responseMessage.Status}; ConnectionState: {GetConnectionState()}; requestId: {requestId}";
|
var errorText = $"Client SendMessageToServerAsync<TResponseData> response error; await; tag: {messageTag}; Status: {responseMessage.Status}; ConnectionState: {GetConnectionState()}; requestId: {requestId}";
|
||||||
|
|
||||||
Logger.Error(errorText);
|
Logger.Error(errorText);
|
||||||
|
|
||||||
//TODO: Ideiglenes, majd a ResponseMessage-et kell visszaadni a Status miatt! - J.
|
|
||||||
return await Task.FromException<TResponse>(new Exception(errorText));
|
return await Task.FromException<TResponse>(new Exception(errorText));
|
||||||
|
|
||||||
//throw new Exception(errorText);
|
|
||||||
//return default;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseData = responseMessage.ResponseData.JsonTo<TResponse>();
|
var responseData = DeserializeResponseData<TResponse>(responseMessage);
|
||||||
Logger.Info($"Client deserialized response json. Total: {(DateTime.UtcNow.Subtract(startTime)).TotalMilliseconds} ms! requestId: {requestId}; tag: {messageTag} [{ConstHelper.NameByValue(TagsName, messageTag)}]");
|
|
||||||
|
if (responseData == null && responseMessage.Status == SignalResponseStatus.Success)
|
||||||
|
{
|
||||||
|
// Null response is valid for Success status
|
||||||
|
Logger.Info($"Client received null response. Total: {(DateTime.UtcNow.Subtract(startTime)).TotalMilliseconds} ms! requestId: {requestId}; tag: {messageTag} [{ConstHelper.NameByValue(TagsName, messageTag)}]");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var serializerType = responseMessage switch
|
||||||
|
{
|
||||||
|
SignalResponseBinaryMessage => "Binary",
|
||||||
|
_ => "JSON"
|
||||||
|
};
|
||||||
|
Logger.Info($"Client deserialized response ({serializerType}). Total: {(DateTime.UtcNow.Subtract(startTime)).TotalMilliseconds} ms! requestId: {requestId}; tag: {messageTag} [{ConstHelper.NameByValue(TagsName, messageTag)}]");
|
||||||
return responseData;
|
return responseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,6 +383,27 @@ namespace AyCode.Services.SignalRs
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializes response data from either JSON or Binary format.
|
||||||
|
/// Automatically detects the format based on the response message type.
|
||||||
|
/// </summary>
|
||||||
|
private static TResponse? DeserializeResponseData<TResponse>(ISignalResponseMessage responseMessage)
|
||||||
|
{
|
||||||
|
return responseMessage switch
|
||||||
|
{
|
||||||
|
SignalResponseBinaryMessage binaryMsg when binaryMsg.ResponseData != null
|
||||||
|
=> binaryMsg.ResponseData.BinaryTo<TResponse>(),
|
||||||
|
|
||||||
|
SignalResponseJsonMessage jsonMsg when !string.IsNullOrEmpty(jsonMsg.ResponseData)
|
||||||
|
=> jsonMsg.ResponseData.JsonTo<TResponse>(),
|
||||||
|
|
||||||
|
ISignalResponseMessage<string> stringMsg when !string.IsNullOrEmpty(stringMsg.ResponseData)
|
||||||
|
=> stringMsg.ResponseData.JsonTo<TResponse>(),
|
||||||
|
|
||||||
|
_ => default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public virtual Task SendMessageToServerAsync<TResponseData>(int messageTag, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback)
|
public virtual Task SendMessageToServerAsync<TResponseData>(int messageTag, Func<ISignalResponseMessage<TResponseData?>, Task> responseCallback)
|
||||||
=> SendMessageToServerAsync(messageTag, null, responseCallback);
|
=> SendMessageToServerAsync(messageTag, null, responseCallback);
|
||||||
|
|
||||||
|
|
@ -383,13 +412,13 @@ namespace AyCode.Services.SignalRs
|
||||||
if (messageTag == 0) Logger.Error($"SendMessageToServerAsync; messageTag == 0");
|
if (messageTag == 0) Logger.Error($"SendMessageToServerAsync; messageTag == 0");
|
||||||
|
|
||||||
var requestId = GetNextRequestId();
|
var requestId = GetNextRequestId();
|
||||||
var requestModel = SignalRRequestModelPool.Get(new Action<ISignalResponseMessage<string>>(responseMessage =>
|
var requestModel = SignalRRequestModelPool.Get(new Action<ISignalResponseMessage>(responseMessage =>
|
||||||
{
|
{
|
||||||
TResponseData? responseData = default;
|
TResponseData? responseData = default;
|
||||||
|
|
||||||
if (responseMessage.Status == SignalResponseStatus.Success)
|
if (responseMessage.Status == SignalResponseStatus.Success)
|
||||||
{
|
{
|
||||||
responseData = string.IsNullOrEmpty(responseMessage.ResponseData) ? default : responseMessage.ResponseData.JsonTo<TResponseData?>();
|
responseData = DeserializeResponseData<TResponseData>(responseMessage);
|
||||||
}
|
}
|
||||||
else Logger.Error($"Client SendMessageToServerAsync<TResponseData> response error; callback; Status: {responseMessage.Status}; ConnectionState: {GetConnectionState()}; requestId: {requestId}; {ConstHelper.NameByValue(TagsName, messageTag)}");
|
else Logger.Error($"Client SendMessageToServerAsync<TResponseData> response error; callback; Status: {responseMessage.Status}; ConnectionState: {GetConnectionState()}; requestId: {requestId}; {ConstHelper.NameByValue(TagsName, messageTag)}");
|
||||||
|
|
||||||
|
|
@ -421,7 +450,7 @@ namespace AyCode.Services.SignalRs
|
||||||
_responseByRequestId[reqId].ResponseDateTime = DateTime.UtcNow;
|
_responseByRequestId[reqId].ResponseDateTime = DateTime.UtcNow;
|
||||||
Logger.Debug($"[{_responseByRequestId[reqId].ResponseDateTime.Subtract(_responseByRequestId[reqId].RequestDateTime).TotalMilliseconds:N0}ms][{(messageBytes.Length / 1024)}kb]{logText}");
|
Logger.Debug($"[{_responseByRequestId[reqId].ResponseDateTime.Subtract(_responseByRequestId[reqId].RequestDateTime).TotalMilliseconds:N0}ms][{(messageBytes.Length / 1024)}kb]{logText}");
|
||||||
|
|
||||||
var responseMessage = DeserializeResponseMsgPack(messageBytes);
|
var responseMessage = DeserializeResponseMessage(messageBytes);
|
||||||
|
|
||||||
switch (_responseByRequestId[reqId].ResponseByRequestId)
|
switch (_responseByRequestId[reqId].ResponseByRequestId)
|
||||||
{
|
{
|
||||||
|
|
@ -429,14 +458,24 @@ namespace AyCode.Services.SignalRs
|
||||||
_responseByRequestId[reqId].ResponseByRequestId = responseMessage;
|
_responseByRequestId[reqId].ResponseByRequestId = responseMessage;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
case Action<ISignalResponseMessage<string>> messagePackCallback:
|
case Action<ISignalResponseMessage> messageCallback:
|
||||||
if (_responseByRequestId.TryRemove(reqId, out var callbackModel))
|
if (_responseByRequestId.TryRemove(reqId, out var callbackModel))
|
||||||
{
|
{
|
||||||
SignalRRequestModelPool.Return(callbackModel);
|
SignalRRequestModelPool.Return(callbackModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
messagePackCallback.Invoke(responseMessage);
|
messageCallback.Invoke(responseMessage);
|
||||||
return Task.CompletedTask; // ← Callback: NEM hívjuk meg a MessageReceived-et
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
// Legacy support for string-based callbacks
|
||||||
|
case Action<ISignalResponseMessage<string>> stringCallback when responseMessage is SignalResponseJsonMessage jsonMsg:
|
||||||
|
if (_responseByRequestId.TryRemove(reqId, out var legacyModel))
|
||||||
|
{
|
||||||
|
SignalRRequestModelPool.Return(legacyModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
stringCallback.Invoke(jsonMsg);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Logger.Error($"Client OnReceiveMessage switch; unknown message type: {_responseByRequestId[reqId].ResponseByRequestId?.GetType().Name}; {ConstHelper.NameByValue(TagsName, messageTag)}");
|
Logger.Error($"Client OnReceiveMessage switch; unknown message type: {_responseByRequestId[reqId].ResponseByRequestId?.GetType().Name}; {ConstHelper.NameByValue(TagsName, messageTag)}");
|
||||||
|
|
@ -470,7 +509,32 @@ namespace AyCode.Services.SignalRs
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual SignalResponseJsonMessage DeserializeResponseMsgPack(byte[] messageBytes)
|
/// <summary>
|
||||||
=> messageBytes.MessagePackTo<SignalResponseJsonMessage>(ContractlessStandardResolver.Options);
|
/// Deserializes a MessagePack response to the appropriate message type (JSON or Binary).
|
||||||
|
/// First tries to deserialize as Binary, then falls back to JSON if that fails.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual ISignalResponseMessage DeserializeResponseMessage(byte[] messageBytes)
|
||||||
|
{
|
||||||
|
// Try Binary format first (SignalResponseBinaryMessage)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var binaryMsg = messageBytes.MessagePackTo<SignalResponseBinaryMessage>(ContractlessStandardResolver.Options);
|
||||||
|
if (binaryMsg.ResponseData != null && binaryMsg.ResponseData.Length > 0)
|
||||||
|
{
|
||||||
|
// Verify it's actually binary data by checking the format
|
||||||
|
if (DetectSerializerTypeFromBytes(binaryMsg.ResponseData) == AcSerializerType.Binary)
|
||||||
|
{
|
||||||
|
return binaryMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Not a binary message, try JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to JSON format
|
||||||
|
return messageBytes.MessagePackTo<SignalResponseJsonMessage>(ContractlessStandardResolver.Options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,53 @@ public enum SignalResponseStatus : byte
|
||||||
Success = 5
|
Success = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signal response message with binary serialized data.
|
||||||
|
/// Used when SerializerOptions.SerializerType == Binary for better performance.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class SignalResponseBinaryMessage : ISignalResponseMessage<byte[]>
|
||||||
|
{
|
||||||
|
[Key(0)] public int MessageTag { get; set; }
|
||||||
|
|
||||||
|
[Key(1)] public SignalResponseStatus Status { get; set; }
|
||||||
|
|
||||||
|
[Key(2)] public byte[]? ResponseData { get; set; }
|
||||||
|
|
||||||
|
[IgnoreMember]
|
||||||
|
public string? ResponseDataJson => ResponseData != null ? Convert.ToBase64String(ResponseData) : null;
|
||||||
|
|
||||||
|
public SignalResponseBinaryMessage() { }
|
||||||
|
|
||||||
|
public SignalResponseBinaryMessage(int messageTag, SignalResponseStatus status)
|
||||||
|
{
|
||||||
|
Status = status;
|
||||||
|
MessageTag = messageTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalResponseBinaryMessage(int messageTag, SignalResponseStatus status, object? responseData, AcBinarySerializerOptions? options = null)
|
||||||
|
: this(messageTag, status)
|
||||||
|
{
|
||||||
|
if (responseData == null)
|
||||||
|
{
|
||||||
|
ResponseData = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If responseData is already a byte array, use it directly
|
||||||
|
if (responseData is byte[] byteData)
|
||||||
|
{
|
||||||
|
ResponseData = byteData;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize to binary
|
||||||
|
ResponseData = options != null
|
||||||
|
? responseData.ToBinary(options)
|
||||||
|
: responseData.ToBinary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public interface IAcSignalRHubClient : IAcSignalRHubBase
|
public interface IAcSignalRHubClient : IAcSignalRHubBase
|
||||||
{
|
{
|
||||||
Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId );
|
Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId );
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ public class SignalRCommunicationBenchmarks
|
||||||
[BenchmarkCategory("Server", "Deserialize")]
|
[BenchmarkCategory("Server", "Deserialize")]
|
||||||
public TestOrderItem Server_DeserializeComplexOrderItem()
|
public TestOrderItem Server_DeserializeComplexOrderItem()
|
||||||
{
|
{
|
||||||
var postMessage = MessagePackSerializer.Deserialize<SignalRPostMessageDto>(_complexOrderItemMessage, SignalRMessageFactory.StandardOptions);
|
var postMessage = MessagePackSerializer.Deserialize<SignalRPostMessageDto>(_complexOrderItemMessage, SignalRMessageFactory.ContractlessOptions);
|
||||||
return postMessage.PostDataJson!.JsonTo<TestOrderItem>()!;
|
return postMessage.PostDataJson!.JsonTo<TestOrderItem>()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,7 +116,7 @@ public class SignalRCommunicationBenchmarks
|
||||||
[BenchmarkCategory("Server", "Deserialize")]
|
[BenchmarkCategory("Server", "Deserialize")]
|
||||||
public TestOrder Server_DeserializeComplexOrder()
|
public TestOrder Server_DeserializeComplexOrder()
|
||||||
{
|
{
|
||||||
var postMessage = MessagePackSerializer.Deserialize<SignalRPostMessageDto>(_complexOrderMessage, SignalRMessageFactory.StandardOptions);
|
var postMessage = MessagePackSerializer.Deserialize<SignalRPostMessageDto>(_complexOrderMessage, SignalRMessageFactory.ContractlessOptions);
|
||||||
return postMessage.PostDataJson!.JsonTo<TestOrder>()!;
|
return postMessage.PostDataJson!.JsonTo<TestOrder>()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,7 +175,7 @@ public class SignalRCommunicationBenchmarks
|
||||||
// Client creates message
|
// Client creates message
|
||||||
var requestBytes = SignalRMessageFactory.CreateComplexObjectMessage(_data.TestOrder);
|
var requestBytes = SignalRMessageFactory.CreateComplexObjectMessage(_data.TestOrder);
|
||||||
// Server deserializes
|
// Server deserializes
|
||||||
var postMessage = MessagePackSerializer.Deserialize<SignalRPostMessageDto>(requestBytes, SignalRMessageFactory.StandardOptions);
|
var postMessage = MessagePackSerializer.Deserialize<SignalRPostMessageDto>(requestBytes, SignalRMessageFactory.ContractlessOptions);
|
||||||
var order = postMessage.PostDataJson!.JsonTo<TestOrder>()!;
|
var order = postMessage.PostDataJson!.JsonTo<TestOrder>()!;
|
||||||
// Server modifies and creates response
|
// Server modifies and creates response
|
||||||
order.OrderNumber = "PROCESSED-" + order.OrderNumber;
|
order.OrderNumber = "PROCESSED-" + order.OrderNumber;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue