AyCode.Core/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs

271 lines
12 KiB
C#

using System.Buffers;
using System.Security.Claims;
using AyCode.Core;
using AyCode.Core.Extensions;
using AyCode.Core.Helpers;
using AyCode.Core.Loggers;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Models.Server.DynamicMethods;
using AyCode.Services.SignalRs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;
namespace AyCode.Services.Server.SignalRs;
public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration configuration, TLogger logger)
: Hub<IAcSignalRHubItemServer>, IAcSignalRHubServer where TSignalRTags : AcSignalRTags where TLogger : AcLoggerBase
{
protected readonly List<AcDynamicMethodCallModel<SignalRAttribute>> DynamicMethodCallModels = [];
protected TLogger Logger = logger;
protected IConfiguration Configuration = configuration;
protected AcSerializerOptions SerializerOptions = new AcBinarySerializerOptions();
#region Connection Lifecycle
public override async Task OnConnectedAsync()
{
Logger.Debug($"Server OnConnectedAsync; ConnectionId: {GetConnectionId()}; UserIdentifier: {GetUserIdentifier()}");
LogContextUserNameAndId();
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var connectionId = GetConnectionId();
var userIdentifier = GetUserIdentifier();
if (exception == null)
Logger.Debug($"Server OnDisconnectedAsync; ConnectionId: {connectionId}; UserIdentifier: {userIdentifier}");
else
Logger.Error($"Server OnDisconnectedAsync; ConnectionId: {connectionId}; UserIdentifier: {userIdentifier}", exception);
LogContextUserNameAndId();
await base.OnDisconnectedAsync(exception);
}
#endregion
#region Message Processing
public virtual Task OnReceiveMessage(int messageTag, byte[]? messageBytes, int? requestId)
{
return ProcessOnReceiveMessage(messageTag, messageBytes, requestId, null);
}
protected virtual async Task ProcessOnReceiveMessage(int messageTag, byte[]? message, int? requestId, Func<string, Task>? notFoundCallback)
{
var tagName = ConstHelper.NameByValue<TSignalRTags>(messageTag);
if (message is { Length: 0 })
{
Logger.Warning($"message.Length == 0! Server OnReceiveMessage; requestId: {requestId}; ConnectionId: {GetConnectionId()}; {tagName}");
}
else
{
Logger.Debug($"[{message?.Length:N0}b] Server OnReceiveMessage; requestId: {requestId}; ConnectionId: {GetConnectionId()}; {tagName}");
}
try
{
if (AcDomain.IsDeveloperVersion) LogContextUserNameAndId();
if (TryFindAndInvokeMethod(messageTag, message, tagName, out var responseData))
{
var responseMessage = CreateResponseMessage(messageTag, SignalResponseStatus.Success, responseData);
if (Logger.LogLevel <= LogLevel.Debug)
{
var responseSize = GetResponseSize(responseMessage);
Logger.Debug($"[{responseSize / 1024}kb] responseData serialized ({SerializerOptions.SerializerType})");
}
await ResponseToCaller(messageTag, responseMessage, requestId);
return;
}
Logger.Debug($"Not found dynamic method for the tag! {tagName}");
notFoundCallback?.Invoke(tagName);
}
catch (Exception ex)
{
Logger.Error($"Server OnReceiveMessage; {ex.Message}; {tagName}", ex);
}
await ResponseToCaller(messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Error, null), requestId);
}
/// <summary>
/// Creates a response message using the configured serializer.
/// </summary>
protected virtual ISignalRMessage CreateResponseMessage(int messageTag, SignalResponseStatus status, object? responseData)
{
return new SignalResponseDataMessage(messageTag, status, responseData, SerializerOptions);
}
/// <summary>
/// Gets the size of the response data for logging purposes.
/// </summary>
private static int GetResponseSize(ISignalRMessage responseMessage)
{
return responseMessage is SignalResponseDataMessage dataMsg ? dataMsg.ResponseData?.Length ?? 0 : 0;
}
/// <summary>
/// Finds and invokes the method registered for the given message tag.
/// </summary>
private bool TryFindAndInvokeMethod(int messageTag, byte[]? message, string tagName, out object? responseData)
{
responseData = null;
foreach (var methodsByDeclaringObject in DynamicMethodCallModels)
{
if (!methodsByDeclaringObject.MethodsByMessageTag.TryGetValue(messageTag, out var methodInfoModel))
continue;
var methodName = $"{methodsByDeclaringObject.InstanceObject.GetType().Name}.{methodInfoModel.MethodInfo.Name}";
var paramValues = DeserializeParameters(message, methodInfoModel, tagName, methodName);
if (paramValues == null)
Logger.Debug($"Found dynamic method for the tag! method: {methodName}(); {tagName}");
else
Logger.Debug($"Found dynamic method for the tag! method: {methodName}({string.Join(", ", methodInfoModel.ParamInfos.Select(x => x.Name))}); {tagName}");
responseData = methodInfoModel.MethodInfo.InvokeMethod(methodsByDeclaringObject.InstanceObject, paramValues);
if (methodInfoModel.Attribute.SendToOtherClientType != SendToClientType.None)
SendMessageToOthers(methodInfoModel.Attribute.SendToOtherClientTag, responseData).Forget();
return true;
}
return false;
}
/// <summary>
/// Deserializes parameters from the message based on method signature.
/// Uses Binary serialization for message wrapper.
/// </summary>
private static object[]? DeserializeParameters(byte[]? message, AcMethodInfoModel<SignalRAttribute> methodInfoModel, string tagName, string methodName)
{
if (methodInfoModel.ParamInfos is not { Length: > 0 })
return null;
if (message is null or { Length: 0 })
throw new ArgumentException($"Message is null or empty but method '{methodName}' requires {methodInfoModel.ParamInfos.Length} parameter(s); {tagName}");
var paramValues = new object[methodInfoModel.ParamInfos.Length];
var firstParamType = methodInfoModel.ParamInfos[0].ParameterType;
// First, try to deserialize as SignalPostJsonMessage to get raw PostDataJson
var msgBase = SignalRSerializationHelper.DeserializeFromBinary<SignalPostJsonMessage>(message);
if (msgBase?.PostDataJson == null || string.IsNullOrEmpty(msgBase.PostDataJson))
{
throw new ArgumentException($"Failed to deserialize message for method '{methodName}'; {tagName}");
}
var json = msgBase.PostDataJson;
// Check if it's an IdMessage format (contains "Ids" property)
if (json.Contains("\"Ids\""))
{
// Parse as IdMessage - each Id is a JSON string for a parameter
var idMessage = json.JsonTo<IdMessage>();
if (idMessage?.Ids != null && idMessage.Ids.Count > 0)
{
for (var i = 0; i < idMessage.Ids.Count && i < methodInfoModel.ParamInfos.Length; i++)
{
var paramType = methodInfoModel.ParamInfos[i].ParameterType;
paramValues[i] = AcJsonDeserializer.Deserialize(idMessage.Ids[i], paramType)!;
}
return paramValues;
}
}
// Single complex object - deserialize directly from PostDataJson
paramValues[0] = json.JsonTo(firstParamType)!;
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);
}
#endregion
#region Response Methods
protected virtual Task ResponseToCallerWithContent(int messageTag, object? content)
=> ResponseToCaller(messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null);
protected virtual Task ResponseToCaller(int messageTag, ISignalRMessage message, int? requestId)
=> SendMessageToClient(Clients.Caller, messageTag, message, requestId);
protected virtual Task SendMessageToUserIdWithContent(string userId, int messageTag, object? content)
=> SendMessageToUserIdInternal(userId, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null);
protected virtual Task SendMessageToUserIdInternal(string userId, int messageTag, ISignalRMessage message, int? requestId)
=> SendMessageToClient(Clients.User(userId), messageTag, message, requestId);
protected virtual Task SendMessageToConnectionIdWithContent(string connectionId, int messageTag, object? content)
=> SendMessageToConnectionIdInternal(connectionId, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null);
protected virtual Task SendMessageToConnectionIdInternal(string connectionId, int messageTag, ISignalRMessage message, int? requestId)
=> SendMessageToClient(Clients.Client(connectionId), messageTag, message, requestId);
protected virtual Task SendMessageToOthers(int messageTag, object? content)
=> SendMessageToClient(Clients.Others, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null);
protected virtual Task SendMessageToAll(int messageTag, object? content)
=> SendMessageToClient(Clients.All, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null);
/// <summary>
/// Sends message to client using Binary serialization.
/// </summary>
protected virtual async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null)
{
var responseBytes = SignalRSerializationHelper.SerializeToBinary(message);
var tagName = ConstHelper.NameByValue<TSignalRTags>(messageTag);
Logger.Debug($"[{responseBytes.Length / 1024}kb] Server sending message to client; requestId: {requestId}; Aborted: {IsConnectionAborted()}; ConnectionId: {GetConnectionId()}; {tagName}");
await sendTo.OnReceiveMessage(messageTag, responseBytes, requestId);
Logger.Debug($"Server sent message to client; requestId: {requestId}; ConnectionId: {GetConnectionId()}; {tagName}");
}
#endregion
#region Context Accessor Methods
protected virtual string GetConnectionId() => Context.ConnectionId;
protected virtual bool IsConnectionAborted() => Context.ConnectionAborted.IsCancellationRequested;
protected virtual string? GetUserIdentifier() => Context.UserIdentifier;
protected virtual ClaimsPrincipal? GetUser() => Context.User;
#endregion
#region Logging
protected virtual void LogContextUserNameAndId()
{
var user = GetUser();
if (user == null) return;
var userName = user.Identity?.Name;
Guid.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier), out var userId);
if (AcDomain.IsDeveloperVersion)
Logger.WarningConditional($"SignalR.Context; userName: {userName}; userId: {userId}");
else
Logger.Debug($"SignalR.Context; userName: {userName}; userId: {userId}");
}
#endregion
}