Refactor SignalR param serialization to pure binary format
Replaces legacy JSON-in-Binary parameter envelopes with a length-prefixed, per-parameter binary format for all client→server calls. Introduces SerializeParametersToBinary and DeserializeParametersFromBinary for type-safe, zero-copy parameter handling. Marks IdMessage, SignalPostJsonDataMessage, and related wrappers as obsolete. Updates all client CRUD/messaging helpers and server-side dispatch to use object[] binary serialization. Adds DataSerializerType to SignalReceiveParams for response format indication. Updates tests and documentation to reflect new protocol. BREAKING CHANGE: not compatible with previous JSON-in-Binary clients/servers.
This commit is contained in:
parent
32018e906a
commit
cdd54d3196
|
|
@ -1,7 +1,5 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Services.SignalRs;
|
||||
using MessagePack;
|
||||
using MessagePack.Resolvers;
|
||||
|
||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||
|
||||
|
|
@ -36,13 +34,9 @@ public enum SendTarget
|
|||
/// </summary>
|
||||
public static class SignalRTestHelper
|
||||
{
|
||||
private static readonly MessagePackSerializerOptions MessagePackOptions = ContractlessStandardResolver.Options;
|
||||
|
||||
public static byte[] CreatePrimitiveParamsMessage(params object[] values)
|
||||
{
|
||||
var idMessage = new IdMessage(values);
|
||||
var postMessage = new SignalPostJsonDataMessage<IdMessage>(idMessage);
|
||||
return MessagePackSerializer.Serialize(postMessage, MessagePackOptions);
|
||||
return SignalRSerializationHelper.SerializeParametersToBinary(values);
|
||||
}
|
||||
|
||||
public static byte[] CreateSinglePrimitiveMessage<T>(T value) where T : notnull
|
||||
|
|
@ -50,15 +44,12 @@ public static class SignalRTestHelper
|
|||
|
||||
public static byte[] CreateComplexObjectMessage<T>(T obj)
|
||||
{
|
||||
var json = obj.ToJson();
|
||||
var postMessage = new SignalPostJsonDataMessage<object>(json);
|
||||
return MessagePackSerializer.Serialize(postMessage, MessagePackOptions);
|
||||
return SignalRSerializationHelper.SerializeParametersToBinary(new object[] { obj! });
|
||||
}
|
||||
|
||||
public static byte[] CreateEmptyMessage()
|
||||
{
|
||||
var postMessage = new SignalPostJsonDataMessage<object>();
|
||||
return MessagePackSerializer.Serialize(postMessage, MessagePackOptions);
|
||||
return SignalRSerializationHelper.SerializeParametersToBinary(Array.Empty<object>());
|
||||
}
|
||||
|
||||
public static T? GetResponseData<T>(SentMessage sentMessage)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@ public abstract class AcSignalRSendToClientService<TSignalRHub, TSignalRTags, TL
|
|||
protected virtual async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, object? content)
|
||||
{
|
||||
var responseData = SignalRSerializationHelper.CreateResponseData(content, AcBinarySerializerOptions.Default) ?? [];
|
||||
var receiveParams = new SignalReceiveParams { Status = SignalResponseStatus.Success };
|
||||
var receiveParams = new SignalReceiveParams
|
||||
{
|
||||
Status = SignalResponseStatus.Success,
|
||||
DataSerializerType = AyCode.Core.Serializers.AcSerializerType.Binary
|
||||
};
|
||||
|
||||
Logger.Info($"[{responseData.Length / 1024}kb] Server sending to client; {ConstHelper.NameByValue<TSignalRTags>(messageTag)}");
|
||||
await sendTo.OnReceiveMessage(messageTag, null, receiveParams, responseData);
|
||||
|
|
|
|||
|
|
@ -421,7 +421,8 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes parameters from the message based on method signature.
|
||||
/// Deserializes parameters from the binary message based on method signature.
|
||||
/// The message is a binary-serialized object[] from the client.
|
||||
/// Supports optional parameters with default values.
|
||||
/// </summary>
|
||||
private static object[]? DeserializeParameters(byte[]? message, AcMethodInfoModel<SignalRAttribute> methodInfoModel, string tagName, string methodName)
|
||||
|
|
@ -430,87 +431,15 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
|||
if (paramInfos is not { Length: > 0 })
|
||||
return null;
|
||||
|
||||
// Handle case where message is null/empty but all parameters have default values
|
||||
if (message is null or { Length: 0 })
|
||||
{
|
||||
// Check if all parameters have default values
|
||||
if (paramInfos.All(p => p.HasDefaultValue))
|
||||
{
|
||||
// Return default values for all parameters
|
||||
return paramInfos.Select(p => p.DefaultValue).ToArray();
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Message is null or empty but method '{methodName}' requires parameters; {tagName}");
|
||||
}
|
||||
|
||||
var msgBase = SignalRSerializationHelper.DeserializeFromBinary<SignalPostJsonMessage>(message);
|
||||
if (string.IsNullOrEmpty(msgBase?.PostDataJson))
|
||||
{
|
||||
// Check if all parameters have default values
|
||||
if (paramInfos.All(p => p.HasDefaultValue))
|
||||
{
|
||||
return paramInfos.Select(p => p.DefaultValue).ToArray();
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Failed to deserialize message for method '{methodName}'; {tagName}");
|
||||
}
|
||||
|
||||
var json = msgBase.PostDataJson;
|
||||
var paramValues = new object?[paramInfos.Length];
|
||||
|
||||
// IdMessage format: multiple parameters as JSON array
|
||||
if (json.Contains("\"Ids\""))
|
||||
{
|
||||
var idMessage = json.JsonTo<IdMessage>();
|
||||
var providedCount = idMessage?.Ids?.Count ?? 0;
|
||||
|
||||
for (var i = 0; i < paramInfos.Length; i++)
|
||||
{
|
||||
var param = paramInfos[i];
|
||||
|
||||
if (i < providedCount)
|
||||
{
|
||||
paramValues[i] = AcJsonDeserializer.Deserialize(idMessage!.Ids![i], param.ParameterType);
|
||||
}
|
||||
else if (param.HasDefaultValue)
|
||||
{
|
||||
paramValues[i] = param.DefaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Missing required parameter '{param.Name}' for method '{methodName}'; {tagName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single complex object format
|
||||
paramValues[0] = json.JsonTo(paramInfos[0].ParameterType);
|
||||
|
||||
// Fill remaining parameters with defaults
|
||||
for (var i = 1; i < paramInfos.Length; i++)
|
||||
{
|
||||
var param = paramInfos[i];
|
||||
if (param.HasDefaultValue)
|
||||
{
|
||||
paramValues[i] = param.DefaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Missing required parameter '{param.Name}' for method '{methodName}'; {tagName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paramValues!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a type should use IdMessage format.
|
||||
/// </summary>
|
||||
private static bool IsPrimitiveOrStringOrEnum(Type type)
|
||||
{
|
||||
return type == typeof(string) || type.IsEnum || type.IsValueType || type == typeof(DateTime);
|
||||
return SignalRSerializationHelper.DeserializeParametersFromBinary(message, paramInfos);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -547,7 +476,11 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
|||
protected virtual async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null)
|
||||
{
|
||||
var responseMessage = (SignalResponseDataMessage)message;
|
||||
var receiveParams = new SignalReceiveParams { Status = responseMessage.Status };
|
||||
var receiveParams = new SignalReceiveParams
|
||||
{
|
||||
Status = responseMessage.Status,
|
||||
DataSerializerType = responseMessage.DataSerializerType
|
||||
};
|
||||
var responseData = responseMessage.ResponseData ?? [];
|
||||
|
||||
var tagName = ConstHelper.NameByValue<TSignalRTags>(messageTag);
|
||||
|
|
|
|||
|
|
@ -10,14 +10,15 @@ Server-side SignalR hub infrastructure: method dispatch, session management, bro
|
|||
```
|
||||
6. OnReceiveMessage(tag, requestId, receiveParams, data)
|
||||
7. DynamicMethodRegistry.GetMethodByMessageTag(tag) ← ConcurrentDictionary lookup
|
||||
8. DeserializeParameters(data):
|
||||
├─ DeserializeFromBinary<SignalPostJsonMessage>() ← unwrap Binary envelope
|
||||
├─ IdMessage format? → parse each Ids[i] as JSON per parameter type
|
||||
└─ Complex object? → json.JsonTo(paramType) ⚠️ tech debt: JSON parse
|
||||
8. DeserializeParameters(data, methodInfoModel):
|
||||
├─ null/empty + all defaults? → default values array
|
||||
└─ DeserializeParametersFromBinary(data, paramInfos)
|
||||
Each param: [INT32 len][AcBinary bytes] → BinaryTo(targetType)
|
||||
Type-guided: target type from ParameterInfo[] (method signature)
|
||||
9. MethodInfo.InvokeMethod(instance, params) ← unwraps Task/ValueTask
|
||||
10. CreateResponseMessage(tag, Success, result) ← Binary serialize payload → byte[]
|
||||
11. SendMessageToClient(caller, tag, message, requestId):
|
||||
├─ Extract receiveParams { Status } + responseData byte[] from message
|
||||
├─ Extract receiveParams { Status, DataSerializerType } + responseData byte[] from message
|
||||
└─ caller.OnReceiveMessage(tag, requestId, receiveParams, responseData)
|
||||
(metadata + payload as separate args — no envelope serialization)
|
||||
12. If SendToOtherClientType != None:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Services.SignalRs;
|
||||
|
||||
namespace AyCode.Services.Tests.SignalRs;
|
||||
|
|
@ -8,99 +7,96 @@ namespace AyCode.Services.Tests.SignalRs;
|
|||
public class PostJsonDataMessageTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Debug_CreatePostMessage_ForInt()
|
||||
public void Debug_ParameterBinary_RoundTrip_ForInt()
|
||||
{
|
||||
var message = CreatePostMessageTest(42);
|
||||
var parameters = new object[] { 42 };
|
||||
Console.WriteLine($"Parameters: [{string.Join(", ", parameters)}]");
|
||||
|
||||
Console.WriteLine($"Message type: {message.GetType().Name}");
|
||||
|
||||
if (message is SignalPostJsonDataMessage<IdMessage> idMsg)
|
||||
{
|
||||
Console.WriteLine($"PostDataJson: {idMsg.PostDataJson}");
|
||||
Console.WriteLine($"PostData.Ids.Count: {idMsg.PostData.Ids.Count}");
|
||||
Console.WriteLine($"PostData.Ids[0]: {idMsg.PostData.Ids[0]}");
|
||||
}
|
||||
|
||||
var bytes = message.ToBinary();
|
||||
var bytes = SignalRSerializationHelper.SerializeParametersToBinary(parameters);
|
||||
Console.WriteLine($"Binary bytes: {bytes.Length}");
|
||||
|
||||
var deserialized = bytes.BinaryTo<SignalPostJsonDataMessage<IdMessage>>();
|
||||
Console.WriteLine($"Deserialized PostDataJson: {deserialized?.PostDataJson}");
|
||||
Console.WriteLine($"Deserialized PostData type: {deserialized?.PostData?.GetType().Name}");
|
||||
Console.WriteLine($"Deserialized PostData.Ids.Count: {deserialized?.PostData?.Ids.Count}");
|
||||
// Simulate server deserialization with known target types
|
||||
var paramInfos = typeof(PostJsonDataMessageTests)
|
||||
.GetMethod(nameof(DummyIntMethod))!.GetParameters();
|
||||
var deserialized = SignalRSerializationHelper.DeserializeParametersFromBinary(bytes, paramInfos);
|
||||
|
||||
Assert.IsNotNull(deserialized?.PostData);
|
||||
Assert.AreEqual(1, deserialized.PostData.Ids.Count);
|
||||
Assert.IsNotNull(deserialized);
|
||||
Assert.AreEqual(1, deserialized.Length);
|
||||
Assert.AreEqual(42, deserialized[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(42)]
|
||||
[DataRow("45")]
|
||||
[DataRow(true)]
|
||||
public void IdMessage_FullRoundTrip_AnyParameter(object testValue)
|
||||
public void ParameterBinary_FullRoundTrip_AnyParameter(object testValue)
|
||||
{
|
||||
dynamic GetValueByType(object value)
|
||||
{
|
||||
if (value is int valueInt) return valueInt;
|
||||
if (value is bool valueBool) return valueBool;
|
||||
if (value is string valueString) return valueString;
|
||||
|
||||
Assert.Fail($"Type of testValue not implemented");
|
||||
return null;
|
||||
}
|
||||
|
||||
Console.WriteLine("=== Step 1: Client creates message ===");
|
||||
var idMessage = new IdMessage(GetValueByType(testValue));
|
||||
Console.WriteLine($"IdMessage.Ids[0]: '{idMessage.Ids[0]}'");
|
||||
|
||||
var clientMessage = new SignalPostJsonDataMessage<IdMessage>(idMessage);
|
||||
Console.WriteLine($"Client PostDataJson: '{clientMessage.PostDataJson}'");
|
||||
Console.WriteLine("=== Step 1: Client creates object[] ===");
|
||||
var parameters = new[] { testValue };
|
||||
|
||||
Console.WriteLine("\n=== Step 2: Binary serialization ===");
|
||||
var bytes = clientMessage.ToBinary();
|
||||
var bytes = SignalRSerializationHelper.SerializeParametersToBinary(parameters);
|
||||
Console.WriteLine($"Binary bytes: {bytes.Length}");
|
||||
|
||||
Console.WriteLine("\n=== Step 3: Server deserializes ===");
|
||||
var serverMessage = bytes.BinaryTo<SignalPostJsonDataMessage<IdMessage>>();
|
||||
Console.WriteLine($"Server PostDataJson: '{serverMessage?.PostDataJson}'");
|
||||
Console.WriteLine($"Server PostData.Ids.Count: {serverMessage?.PostData?.Ids.Count}");
|
||||
Console.WriteLine($"Server PostData.Ids[0]: '{serverMessage?.PostData?.Ids[0]}'");
|
||||
Console.WriteLine("\n=== Step 3: Server deserializes with target type ===");
|
||||
var paramInfos = new[] { GetParamInfoForType(testValue.GetType()) };
|
||||
var serverParams = SignalRSerializationHelper.DeserializeParametersFromBinary(bytes, paramInfos);
|
||||
|
||||
Console.WriteLine("\n=== Step 4: Server deserializes parameter ===");
|
||||
var paramJson = serverMessage!.PostData.Ids[0];
|
||||
Console.WriteLine($"Parameter JSON: '{paramJson}'");
|
||||
var paramValue = AcJsonDeserializer.Deserialize(paramJson, testValue.GetType());
|
||||
Console.WriteLine($"Deserialized value: {paramValue}");
|
||||
Console.WriteLine($"Server params[0]: '{serverParams[0]}' (type: {serverParams[0]?.GetType().Name})");
|
||||
|
||||
Console.WriteLine("\n=== Step 5: Service method returns ===");
|
||||
var serviceResult = $"{paramValue}";
|
||||
Console.WriteLine("\n=== Step 4: Service method uses parameter ===");
|
||||
var serviceResult = $"{serverParams![0]}";
|
||||
Console.WriteLine($"Service result: '{serviceResult}'");
|
||||
|
||||
Console.WriteLine("\n=== Step 6: Server creates response ===");
|
||||
var response = new SignalResponseDataMessage(100, SignalResponseStatus.Success, serviceResult, AcJsonSerializerOptions.Default);
|
||||
Console.WriteLine($"Response created with Binary bytes: {response.ResponseData?.Length ?? 0}");
|
||||
Console.WriteLine("\n=== Step 5: Server creates response ===");
|
||||
var response = new SignalResponseDataMessage(100, SignalResponseStatus.Success, serviceResult, AyCode.Core.Serializers.Binaries.AcBinarySerializerOptions.Default);
|
||||
|
||||
Console.WriteLine("\n=== Step 7: Response Binary ===");
|
||||
Console.WriteLine("\n=== Step 6: Response Binary ===");
|
||||
var responseBytes = response.ToBinary();
|
||||
Console.WriteLine($"Response Binary bytes: {responseBytes.Length}");
|
||||
|
||||
Console.WriteLine("\n=== Step 8: Client deserializes response ===");
|
||||
Console.WriteLine("\n=== Step 7: Client deserializes response ===");
|
||||
var clientResponse = responseBytes.BinaryTo<SignalResponseDataMessage>();
|
||||
Console.WriteLine($"Client Response Status: {clientResponse?.Status}");
|
||||
|
||||
Console.WriteLine("\n=== Step 9: Client deserializes to string ===");
|
||||
Console.WriteLine("\n=== Step 8: Client deserializes to string ===");
|
||||
var finalResult = clientResponse?.GetResponseData<string>();
|
||||
Console.WriteLine($"Final result: '{finalResult}'");
|
||||
Assert.AreEqual(GetValueByType(testValue).ToString(), finalResult);
|
||||
Assert.AreEqual(testValue.ToString(), finalResult);
|
||||
}
|
||||
|
||||
private static ISignalRMessage CreatePostMessageTest<TPostData>(TPostData postData)
|
||||
[TestMethod]
|
||||
public void ParameterBinary_MultipleParams_RoundTrip()
|
||||
{
|
||||
var type = typeof(TPostData);
|
||||
var guid = Guid.NewGuid();
|
||||
var parameters = new object[] { 42, "hello", true, guid };
|
||||
|
||||
if (type == typeof(string) || type.IsEnum || type.IsValueType || type == typeof(DateTime))
|
||||
return new SignalPostJsonDataMessage<IdMessage>(new IdMessage(postData!));
|
||||
var bytes = SignalRSerializationHelper.SerializeParametersToBinary(parameters);
|
||||
|
||||
return new SignalPostJsonDataMessage<TPostData>(postData);
|
||||
var paramInfos = typeof(PostJsonDataMessageTests)
|
||||
.GetMethod(nameof(DummyMultiMethod))!.GetParameters();
|
||||
var deserialized = SignalRSerializationHelper.DeserializeParametersFromBinary(bytes, paramInfos);
|
||||
|
||||
Assert.IsNotNull(deserialized);
|
||||
Assert.AreEqual(4, deserialized.Length);
|
||||
Assert.AreEqual(42, deserialized[0]);
|
||||
Assert.AreEqual("hello", deserialized[1]);
|
||||
Assert.AreEqual(true, deserialized[2]);
|
||||
Assert.AreEqual(guid, deserialized[3]);
|
||||
}
|
||||
|
||||
// Dummy methods for ParameterInfo extraction
|
||||
public static void DummyIntMethod(int value) { }
|
||||
public static void DummyMultiMethod(int a, string b, bool c, Guid d) { }
|
||||
|
||||
private static System.Reflection.ParameterInfo GetParamInfoForType(Type type)
|
||||
{
|
||||
// Find a dummy method with the matching parameter type
|
||||
if (type == typeof(int)) return typeof(PostJsonDataMessageTests).GetMethod(nameof(DummyInt))!.GetParameters()[0];
|
||||
if (type == typeof(string)) return typeof(PostJsonDataMessageTests).GetMethod(nameof(DummyString))!.GetParameters()[0];
|
||||
if (type == typeof(bool)) return typeof(PostJsonDataMessageTests).GetMethod(nameof(DummyBool))!.GetParameters()[0];
|
||||
throw new NotSupportedException($"No dummy method for type {type.Name}");
|
||||
}
|
||||
|
||||
public static void DummyInt(int v) { }
|
||||
public static void DummyString(string v) { }
|
||||
public static void DummyBool(bool v) { }
|
||||
}
|
||||
|
|
@ -134,15 +134,15 @@ namespace AyCode.Services.SignalRs
|
|||
}
|
||||
|
||||
public virtual Task SendMessageToServerAsync(int messageTag)
|
||||
=> SendMessageToServerAsync(messageTag, null, GetNextRequestId());
|
||||
=> SendMessageToServerAsync(messageTag, (object[]?)null, GetNextRequestId());
|
||||
|
||||
public virtual async Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId)
|
||||
public virtual async Task SendMessageToServerAsync(int messageTag, object[]? parameters, int? requestId)
|
||||
{
|
||||
Logger.DebugConditional($"Client SendMessageToServerAsync sending; {nameof(requestId)}: {requestId}; ConnectionState: {GetConnectionState()}; {ConstHelper.NameByValue(TagsName, messageTag)}");
|
||||
|
||||
await StartConnection();
|
||||
|
||||
var msgBytes = message != null ? SignalRSerializationHelper.SerializeToBinary(message) : null;
|
||||
var msgBytes = parameters is { Length: > 0 } ? SignalRSerializationHelper.SerializeParametersToBinary(parameters) : null;
|
||||
|
||||
if (!IsConnected())
|
||||
{
|
||||
|
|
@ -157,10 +157,10 @@ namespace AyCode.Services.SignalRs
|
|||
#region CRUD
|
||||
|
||||
public virtual Task<TResponseData?> PostAsync<TResponseData>(int messageTag, object parameter)
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, new SignalPostJsonDataMessage<IdMessage>(new IdMessage(parameter)), GetNextRequestId());
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, [parameter], GetNextRequestId());
|
||||
|
||||
public virtual Task<TResponseData?> PostAsync<TResponseData>(int messageTag, object[] parameters)
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, new SignalPostJsonDataMessage<IdMessage>(new IdMessage(parameters)), GetNextRequestId());
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, parameters, GetNextRequestId());
|
||||
|
||||
public virtual Task<TResponseData?> GetByIdAsync<TResponseData>(int messageTag, object id)
|
||||
=> PostAsync<TResponseData?>(messageTag, id);
|
||||
|
|
@ -172,19 +172,19 @@ namespace AyCode.Services.SignalRs
|
|||
/// Gets data by ID with async callback response. Callback is second parameter.
|
||||
/// </summary>
|
||||
public virtual Task GetByIdAsync<TResponseData>(int messageTag, Func<SignalResponseDataMessage, Task> responseCallback, object id)
|
||||
=> SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage<IdMessage>(new IdMessage(id)), responseCallback);
|
||||
=> SendMessageToServerAsync(messageTag, [id], responseCallback);
|
||||
|
||||
/// <summary>
|
||||
/// Gets data by IDs with async callback response. Callback is second parameter.
|
||||
/// </summary>
|
||||
public virtual Task GetByIdAsync<TResponseData>(int messageTag, Func<SignalResponseDataMessage, Task> responseCallback, object[] ids)
|
||||
=> SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage<IdMessage>(new IdMessage(ids)), responseCallback);
|
||||
=> SendMessageToServerAsync(messageTag, ids, responseCallback);
|
||||
|
||||
public virtual Task<TResponseData?> GetAllAsync<TResponseData>(int messageTag)
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag);
|
||||
|
||||
public virtual Task<TResponseData?> GetAllAsync<TResponseData>(int messageTag, object[]? contextParams)
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, contextParams == null || contextParams.Length == 0 ? null : new SignalPostJsonDataMessage<IdMessage>(new IdMessage(contextParams)), GetNextRequestId());
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, contextParams is { Length: > 0 } ? contextParams : null, GetNextRequestId());
|
||||
|
||||
/// <summary>
|
||||
/// Gets all data with async callback response. Callback is second parameter.
|
||||
|
|
@ -196,7 +196,7 @@ namespace AyCode.Services.SignalRs
|
|||
/// Gets all data with context params and async callback response.
|
||||
/// </summary>
|
||||
public virtual Task GetAllAsync<TResponseData>(int messageTag, Func<SignalResponseDataMessage, Task> responseCallback, object[]? contextParams)
|
||||
=> SendMessageToServerAsync(messageTag, contextParams == null || contextParams.Length == 0 ? null : new SignalPostJsonDataMessage<IdMessage>(new IdMessage(contextParams)), responseCallback);
|
||||
=> SendMessageToServerAsync(messageTag, contextParams is { Length: > 0 } ? contextParams : null, responseCallback);
|
||||
|
||||
public virtual async IAsyncEnumerable<TResponseData?> StreamAllAsync<TResponseData>(int messageTag, object[]? contextParams = null, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
@ -208,8 +208,7 @@ namespace AyCode.Services.SignalRs
|
|||
yield break;
|
||||
}
|
||||
|
||||
var message = contextParams == null || contextParams.Length == 0 ? null : new SignalPostJsonDataMessage<IdMessage>(new IdMessage(contextParams));
|
||||
var msgBytes = message != null ? SignalRSerializationHelper.SerializeToBinary(message) : null;
|
||||
var msgBytes = contextParams is { Length: > 0 } ? SignalRSerializationHelper.SerializeParametersToBinary(contextParams) : null;
|
||||
|
||||
var stream = HubConnection.StreamAsync<byte[]>(
|
||||
"OnReceiveStreamMessage",
|
||||
|
|
@ -242,22 +241,22 @@ namespace AyCode.Services.SignalRs
|
|||
}
|
||||
|
||||
public virtual Task<TPostData?> PostDataAsync<TPostData>(int messageTag, TPostData postData) where TPostData : class
|
||||
=> SendMessageToServerAsync<TPostData>(messageTag, CreatePostMessage(postData), GetNextRequestId());
|
||||
=> SendMessageToServerAsync<TPostData>(messageTag, [postData!], GetNextRequestId());
|
||||
|
||||
public virtual Task<TResponseData?> PostDataAsync<TPostData, TResponseData>(int messageTag, TPostData postData)
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, CreatePostMessage(postData), GetNextRequestId());
|
||||
=> SendMessageToServerAsync<TResponseData>(messageTag, [postData!], GetNextRequestId());
|
||||
|
||||
/// <summary>
|
||||
/// Posts data with async callback response.
|
||||
/// </summary>
|
||||
public virtual Task PostDataAsync<TPostData>(int messageTag, TPostData postData, Func<SignalResponseDataMessage, Task> responseCallback)
|
||||
=> SendMessageToServerAsync(messageTag, CreatePostMessage(postData), responseCallback);
|
||||
=> SendMessageToServerAsync(messageTag, [postData!], responseCallback);
|
||||
|
||||
/// <summary>
|
||||
/// Posts data with typed async callback response.
|
||||
/// </summary>
|
||||
public virtual Task PostDataAsync<TPostData, TResponseData>(int messageTag, TPostData postData, Func<SignalResponseDataMessage, Task> responseCallback)
|
||||
=> SendMessageToServerAsync(messageTag, CreatePostMessage(postData), responseCallback);
|
||||
=> SendMessageToServerAsync(messageTag, [postData!], responseCallback);
|
||||
|
||||
/// <summary>
|
||||
/// Posts data and invokes callback with response. Fire-and-forget friendly for background saves.
|
||||
|
|
@ -268,7 +267,7 @@ namespace AyCode.Services.SignalRs
|
|||
var requestModel = SignalRRequestModelPool.Get(responseCallback);
|
||||
|
||||
_responseByRequestId[requestId] = requestModel;
|
||||
return SendMessageToServerAsync(messageTag, CreatePostMessage(postData), requestId);
|
||||
return SendMessageToServerAsync(messageTag, [postData!], requestId);
|
||||
}
|
||||
|
||||
public virtual async IAsyncEnumerable<TResponseData?> StreamPostDataAsync<TPostData, TResponseData>(int messageTag, TPostData postData, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
|
|
@ -281,8 +280,7 @@ namespace AyCode.Services.SignalRs
|
|||
yield break;
|
||||
}
|
||||
|
||||
var message = CreatePostMessage(postData);
|
||||
var msgBytes = SignalRSerializationHelper.SerializeToBinary(message);
|
||||
var msgBytes = SignalRSerializationHelper.SerializeParametersToBinary(new object[] { postData! });
|
||||
|
||||
var stream = HubConnection.StreamAsync<byte[]>(
|
||||
"OnReceiveStreamMessage",
|
||||
|
|
@ -314,14 +312,6 @@ namespace AyCode.Services.SignalRs
|
|||
}
|
||||
}
|
||||
|
||||
private static ISignalRMessage CreatePostMessage<TPostData>(TPostData postData)
|
||||
{
|
||||
var type = typeof(TPostData);
|
||||
if (type == typeof(string) || type.IsEnum || type.IsValueType)
|
||||
return new SignalPostJsonDataMessage<IdMessage>(new IdMessage(postData!));
|
||||
return new SignalPostJsonDataMessage<TPostData>(postData);
|
||||
}
|
||||
|
||||
public Task GetAllIntoAsync<TResponseItem>(List<TResponseItem> intoList, int messageTag, object[]? contextParams = null, Action? callback = null) where TResponseItem : IEntityGuid
|
||||
{
|
||||
return GetAllAsync<List<TResponseItem>>(messageTag, contextParams).ContinueWith(task =>
|
||||
|
|
@ -341,24 +331,24 @@ namespace AyCode.Services.SignalRs
|
|||
#endregion
|
||||
|
||||
public virtual Task<TResponse?> SendMessageToServerAsync<TResponse>(int messageTag)
|
||||
=> SendMessageToServerAsync<TResponse>(messageTag, null, GetNextRequestId());
|
||||
=> SendMessageToServerAsync<TResponse>(messageTag, (object[]?)null, GetNextRequestId());
|
||||
|
||||
public virtual Task<TResponse?> SendMessageToServerAsync<TResponse>(int messageTag, ISignalRMessage? message)
|
||||
=> SendMessageToServerAsync<TResponse>(messageTag, message, GetNextRequestId());
|
||||
public virtual Task<TResponse?> SendMessageToServerAsync<TResponse>(int messageTag, object[]? parameters)
|
||||
=> SendMessageToServerAsync<TResponse>(messageTag, parameters, GetNextRequestId());
|
||||
|
||||
/// <summary>
|
||||
/// Sends message to server with async callback response.
|
||||
/// </summary>
|
||||
public virtual async Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, Func<SignalResponseDataMessage, Task> responseCallback)
|
||||
public virtual async Task SendMessageToServerAsync(int messageTag, object[]? parameters, Func<SignalResponseDataMessage, Task> responseCallback)
|
||||
{
|
||||
var requestId = GetNextRequestId();
|
||||
var requestModel = SignalRRequestModelPool.Get(responseCallback);
|
||||
|
||||
_responseByRequestId[requestId] = requestModel;
|
||||
await SendMessageToServerAsync(messageTag, message, requestId);
|
||||
await SendMessageToServerAsync(messageTag, parameters, requestId);
|
||||
}
|
||||
|
||||
protected virtual async Task<TResponse?> SendMessageToServerAsync<TResponse>(int messageTag, ISignalRMessage? message, int requestId)
|
||||
protected virtual async Task<TResponse?> SendMessageToServerAsync<TResponse>(int messageTag, object[]? parameters, int requestId)
|
||||
{
|
||||
Logger.DebugConditional($"Client SendMessageToServerAsync<TResult>; {nameof(requestId)}: {requestId}; {ConstHelper.NameByValue(TagsName, messageTag)}");
|
||||
|
||||
|
|
@ -366,7 +356,7 @@ namespace AyCode.Services.SignalRs
|
|||
var requestModel = SignalRRequestModelPool.Get();
|
||||
|
||||
_responseByRequestId[requestId] = requestModel;
|
||||
await SendMessageToServerAsync(messageTag, message, requestId);
|
||||
await SendMessageToServerAsync(messageTag, parameters, requestId);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -441,7 +431,7 @@ namespace AyCode.Services.SignalRs
|
|||
var responseMessage = new SignalResponseDataMessage
|
||||
{
|
||||
Status = receiveParams.Status,
|
||||
DataSerializerType = AcSerializerType.Binary,
|
||||
DataSerializerType = receiveParams.DataSerializerType,
|
||||
ResponseData = data
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace AyCode.Services.SignalRs;
|
|||
/// Message container for serialized parameter IDs.
|
||||
/// Optimized for common primitive types to avoid full JSON overhead.
|
||||
/// </summary>
|
||||
[Obsolete("Use direct object[] binary serialization instead of IdMessage JSON wrapper.")]
|
||||
public class IdMessage
|
||||
{
|
||||
public List<string> Ids { get; private set; }
|
||||
|
|
@ -61,6 +62,7 @@ public class IdMessage
|
|||
/// <summary>
|
||||
/// Message containing JSON-serialized post data.
|
||||
/// </summary>
|
||||
[Obsolete("Use direct object[] binary serialization instead of JSON-in-Binary wrapper.")]
|
||||
public class SignalPostJsonMessage
|
||||
{
|
||||
public string PostDataJson { get; set; } = "";
|
||||
|
|
@ -72,6 +74,7 @@ public class SignalPostJsonMessage
|
|||
/// <summary>
|
||||
/// Generic message containing JSON-serialized post data with typed access.
|
||||
/// </summary>
|
||||
[Obsolete("Use direct object[] binary serialization instead of JSON-in-Binary wrapper.")]
|
||||
public class SignalPostJsonDataMessage<TPostDataType> : SignalPostJsonMessage, ISignalPostMessage<TPostDataType>
|
||||
{
|
||||
[JsonIgnore]
|
||||
|
|
@ -98,11 +101,13 @@ public class SignalPostJsonDataMessage<TPostDataType> : SignalPostJsonMessage, I
|
|||
/// <summary>
|
||||
/// Simple message containing post data.
|
||||
/// </summary>
|
||||
[Obsolete("Use direct object[] binary serialization instead of message wrappers.")]
|
||||
public class SignalPostMessage<TPostData>(TPostData postData) : ISignalPostMessage<TPostData>
|
||||
{
|
||||
public TPostData? PostData { get; set; } = postData;
|
||||
}
|
||||
|
||||
[Obsolete("Use direct object[] binary serialization instead of message wrappers.")]
|
||||
public interface ISignalPostMessage<TPostData> : ISignalRMessage
|
||||
{
|
||||
TPostData? PostData { get; }
|
||||
|
|
@ -436,5 +441,5 @@ public sealed class SignalResponseDataMessage : ISignalResponseMessage, IDisposa
|
|||
|
||||
public interface IAcSignalRHubClient : IAcSignalRHubBase
|
||||
{
|
||||
Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId);
|
||||
Task SendMessageToServerAsync(int messageTag, object[]? parameters, int? requestId);
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using AyCode.Core.Serializers;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
|
||||
namespace AyCode.Services.SignalRs;
|
||||
|
|
@ -16,4 +17,5 @@ public interface ISignalParams { }
|
|||
public class SignalReceiveParams : ISignalParams
|
||||
{
|
||||
public SignalResponseStatus Status { get; set; }
|
||||
public AcSerializerType DataSerializerType { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ public static class SignalRSerializationHelper
|
|||
/// 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)
|
||||
{
|
||||
|
|
@ -42,6 +43,7 @@ public static class SignalRSerializationHelper
|
|||
/// <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)
|
||||
{
|
||||
|
|
@ -96,6 +98,103 @@ public static class SignalRSerializationHelper
|
|||
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
|
||||
|
|
|
|||
|
|
@ -58,9 +58,10 @@ public abstract class MyProjectTags : AcSignalRTags
|
|||
|
||||
**Usage:**
|
||||
```csharp
|
||||
var orders = await client.PostAsync<List<Order>>(MyTags.GetOrders, companyId);
|
||||
var orders = await client.PostAsync<List<Order>>(MyTags.GetOrders, companyId); // single param
|
||||
var result = await client.PostAsync<T>(MyTags.Query, [companyId, filter]); // object[] params
|
||||
await client.PostDataAsync(MyTags.SaveOrder, order);
|
||||
await client.PostDataAsync(MyTags.SaveOrder, order, async response => { ... }); // async callback
|
||||
await client.PostDataAsync(MyTags.SaveOrder, order, async response => { ... }); // async callback
|
||||
```
|
||||
|
||||
CRUD helpers (`PostAsync`, `GetByIdAsync`, `GetAllAsync`, `PostDataAsync`) are generic transport, not tied to DataSource.
|
||||
|
|
@ -80,26 +81,24 @@ Custom `IHubProtocol` (`"acbinary"`), replaces JSON. Zero-copy via `BufferWriter
|
|||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| `Status` | SignalResponseStatus | Success/Error |
|
||||
| `DataSerializerType` | AcSerializerType | Binary or JsonGZip — tells client how to deserialize response data |
|
||||
|
||||
`byte[] data` (separate hub argument, protocol fast-path, zero-copy).
|
||||
|
||||
`SignalResponseDataMessage` remains as **internal DTO** for callback routing — constructed in-memory from `receiveParams` + `data`, never serialized as envelope on wire.
|
||||
|
||||
Binary (default): `AcBinarySerializer.ToBinary(data)`. JSON fallback: `ToJson` → `GzipHelper.Compress`.
|
||||
`SignalResponseDataMessage` remains as **internal DTO** for callback routing — constructed in-memory from `receiveParams` + `data`, never serialized as envelope on wire. `GetResponseData<T>()` dispatches on `DataSerializerType`: Binary → `BinaryTo<T>()`, JsonGZip → decompress → `JsonTo<T>()`.
|
||||
|
||||
## Request/Response Flow
|
||||
|
||||
### Client → Server
|
||||
|
||||
```
|
||||
1. PostAsync<T>(tag, postData) / PostDataAsync(tag, data, callback)
|
||||
2. CreatePostMessage(postData):
|
||||
├─ Primitives/strings/enums/value types → IdMessage
|
||||
└─ Complex → SignalPostJsonDataMessage<T> ⚠️ JSON-in-Binary tech debt
|
||||
3. SerializeToBinary(message)
|
||||
4. SignalReceiveParams { Status = Success }
|
||||
5. HubConnection.SendAsync("OnReceiveMessage", tag, requestId, receiveParams, bytes)
|
||||
6. AcBinaryHubProtocol frames on wire (byte[] via fast-path, receiveParams via AcBinary)
|
||||
1. PostAsync<T>(tag, param) / PostAsync<T>(tag, params[]) / PostDataAsync(tag, data, callback)
|
||||
2. SerializeParametersToBinary(object[] parameters):
|
||||
[VarUInt count] [for each: INT32 length + AcBinary bytes]
|
||||
Each parameter individually binary-serialized with length prefix.
|
||||
3. SignalReceiveParams { Status = Success }
|
||||
4. HubConnection.SendAsync("OnReceiveMessage", tag, requestId, receiveParams, bytes)
|
||||
5. AcBinaryHubProtocol frames on wire (byte[] via fast-path, receiveParams via AcBinary)
|
||||
```
|
||||
|
||||
### Server → Client
|
||||
|
|
@ -107,10 +106,11 @@ Binary (default): `AcBinarySerializer.ToBinary(data)`. JSON fallback: `ToJson`
|
|||
```
|
||||
OnReceiveMessage(tag, requestId, receiveParams, data)
|
||||
├─ Construct SignalResponseDataMessage in-memory (no envelope deser):
|
||||
│ └─ { Status = receiveParams.Status, DataSerializerType = Binary, ResponseData = data }
|
||||
│ └─ { Status, DataSerializerType, ResponseData } from receiveParams + data
|
||||
├─ Matching requestId in pending dict:
|
||||
│ ├─ Route: null→sync wait, Action→invoke, Func<Task>→await
|
||||
│ └─ GetResponseData<T>(): Binary→BinaryTo<T>(), JSON→Decompress→Deserialize
|
||||
│ └─ GetResponseData<T>(): dispatches on DataSerializerType
|
||||
│ Binary→BinaryTo<T>(), JsonGZip→Decompress→JsonTo<T>()
|
||||
└─ No match (broadcast):
|
||||
└─ abstract MessageReceived(tag, receiveParams, data).Forget()
|
||||
```
|
||||
|
|
@ -136,9 +136,20 @@ Request pooling: `SignalRRequestModel` via `SignalRRequestModelPool` (ObjectPool
|
|||
|
||||
`AcSignalRClientBase.EnableBinaryDiagnostics = true` — hex dump, header parsing, property enumeration.
|
||||
|
||||
## Tech Debt
|
||||
## Parameter Serialization
|
||||
|
||||
**JSON-in-Binary:** client→server wraps params in JSON inside binary envelope (`SignalPostJsonDataMessage`). Do NOT fix as side effect — requires coordinated cross-project changes.
|
||||
Client→server parameters use length-prefixed per-parameter binary format via `SignalRSerializationHelper`:
|
||||
|
||||
```
|
||||
SerializeParametersToBinary(object[]):
|
||||
[VarUInt count] [for each: INT32 length + AcBinary bytes]
|
||||
|
||||
DeserializeParametersFromBinary(byte[], ParameterInfo[]):
|
||||
Server reads each segment and deserializes with known target type from method signature.
|
||||
Trailing parameters with defaults are auto-filled.
|
||||
```
|
||||
|
||||
This enables type-guided deserialization — each parameter is individually serialized/deserialized with its concrete type, avoiding the `object[]` → dictionary problem of untyped binary deserialization.
|
||||
|
||||
## Source Files
|
||||
|
||||
|
|
|
|||
|
|
@ -70,14 +70,15 @@ For full architecture see `AyCode.Services/docs/SIGNALR.md`.
|
|||
|
||||
| Term | Definition |
|
||||
|---|---|
|
||||
| **OnReceiveMessage** | The single SignalR method for all communication. Signature: `(int messageTag, int? requestId, SignalReceiveParams receiveParams, byte[] data)`. Metadata and payload are separate hub arguments — `byte[]` uses protocol zero-copy fast-path. |
|
||||
| **SignalReceiveParams** | Lightweight metadata sent alongside message payload as separate hub argument. Contains `Status` (SignalResponseStatus). Implements `ISignalParams`. AcBinary serialized. |
|
||||
| **OnReceiveMessage** | The single SignalR method for all communication. Signature: `(int messageTag, int? requestId, SignalReceiveParams receiveParams, byte[] data)`. Metadata and payload are separate hub arguments — `byte[]` uses protocol zero-copy fast-path. Client→server: `data` is length-prefixed parameter binary. Server→client: `data` is response payload (format indicated by `receiveParams.DataSerializerType`). |
|
||||
| **SignalReceiveParams** | Lightweight metadata sent alongside message payload as separate hub argument. Contains `Status` (SignalResponseStatus) and `DataSerializerType` (AcSerializerType — Binary or JsonGZip, tells client how to deserialize response data). Implements `ISignalParams`. AcBinary serialized. |
|
||||
| **Message Tag** | Integer identifier mapping to a method via `[SignalR(tag)]` or `[SignalRSendToClient(tag)]` attributes. |
|
||||
| **DynamicMethodRegistry** | Resolves message tags to `MethodInfo` at runtime. Static `ConcurrentDictionary` cache with lazy scan on miss. |
|
||||
| **SignalRCrudTags** | Sealed class bundling 5 independent tag integers (getAllTag, getItemTag, addTag, updateTag, removeTag) for entity CRUD. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`. |
|
||||
| **AcBinaryHubProtocol** | Custom `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"`. Uses `BufferWriterBinaryOutput` for zero-copy writes to the SignalR pipe. |
|
||||
| **SignalResponseDataMessage** | Internal DTO for callback routing (not serialized on wire). Constructed in-memory from `receiveParams` + `data`. Supports Binary or JSON+GZip via `GetResponseData<T>()`. |
|
||||
| **SignalPostJsonDataMessage** | ⚠️ TECH DEBT — request params serialized to JSON inside Binary envelope. Planned for pure Binary replacement. |
|
||||
| **SignalPostJsonDataMessage** | ⚠️ OBSOLETE — replaced by `SerializeParametersToBinary(object[])`. Legacy: serialized params to JSON inside Binary envelope. |
|
||||
| **SerializeParametersToBinary** | Length-prefixed per-parameter binary format: `[VarUInt count][INT32 len + AcBinary bytes per param]`. Server deserializes each with target type from method signature via `DeserializeParametersFromBinary`. In `SignalRSerializationHelper`. |
|
||||
| **AcSignalRDataSource** | Generic real-time `IList<T>` with change tracking, CRUD via SignalRCrudTags, binary merge, rollback, sync state. |
|
||||
| **TrackingItem** | Wraps a modified DataSource item with `TrackingState` (Add/Update/Remove) + `OriginalValue` for rollback. |
|
||||
| **SendToClientType** | Enum controlling broadcast scope: None, Others, Caller, All. |
|
||||
|
|
|
|||
Loading…
Reference in New Issue