From 6fb3d0d848e11db2203abd188a42e17a83a78567 Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 6 Nov 2024 14:36:03 +0100 Subject: [PATCH] Refactoring Login, SignalR, etc... --- .../AyCode.Blazor.Components.csproj | 11 + .../Services/AcWebSignalRHubBase.cs | 196 ++++++++++++++++++ .../Services/ExtensionMethods.cs | 18 ++ .../Services/Logins/AcWebAuthService.cs | 7 + .../AyCode.Blazor.Controllers.csproj | 1 - .../AyCode.Blazor.Models.Server.csproj | 7 + .../Models/DynamicMethodCallModel.cs | 35 ++++ .../Models/MethodInfoModel.cs | 24 +++ 8 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 AyCode.Blazor.Components/Services/AcWebSignalRHubBase.cs create mode 100644 AyCode.Blazor.Components/Services/ExtensionMethods.cs create mode 100644 AyCode.Blazor.Components/Services/Logins/AcWebAuthService.cs create mode 100644 AyCode.Blazor.Models.Server/Models/DynamicMethodCallModel.cs create mode 100644 AyCode.Blazor.Models.Server/Models/MethodInfoModel.cs diff --git a/AyCode.Blazor.Components/AyCode.Blazor.Components.csproj b/AyCode.Blazor.Components/AyCode.Blazor.Components.csproj index 8891585..831da5d 100644 --- a/AyCode.Blazor.Components/AyCode.Blazor.Components.csproj +++ b/AyCode.Blazor.Components/AyCode.Blazor.Components.csproj @@ -14,10 +14,13 @@ + + + @@ -36,17 +39,25 @@ ..\..\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Services.dll + + ..\..\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Services.Server.dll + ..\..\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Utils.dll + + C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.10\Microsoft.AspNetCore.SignalR.Core.dll + + + diff --git a/AyCode.Blazor.Components/Services/AcWebSignalRHubBase.cs b/AyCode.Blazor.Components/Services/AcWebSignalRHubBase.cs new file mode 100644 index 0000000..cb1803b --- /dev/null +++ b/AyCode.Blazor.Components/Services/AcWebSignalRHubBase.cs @@ -0,0 +1,196 @@ +using System.Linq.Expressions; +using System.Security.Claims; +using AyCode.Blazor.Models.Server.Models; +using AyCode.Core; +using AyCode.Core.Extensions; +using AyCode.Core.Helpers; +using AyCode.Core.Loggers; +using AyCode.Services.Server.SignalRs; +using AyCode.Services.SignalRs; +using MessagePack; +using MessagePack.Resolvers; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Configuration; + +namespace AyCode.Blazor.Components.Services; + +public abstract class AcWebSignalRHubBase(IConfiguration configuration, TLogger logger) + : Hub, IAcSignalRHubServer where TSignalRTags : AcSignalRTags where TLogger : AcLoggerBase +{ + protected readonly List> DynamicMethodCallModels = []; + //protected readonly TIAM.Core.Loggers.Logger> Logger = new(logWriters.ToArray()); + protected TLogger Logger = logger; + protected IConfiguration Configuration = configuration; + + //private readonly ServiceProviderAPIController _serviceProviderApiController; + //private readonly TransferDataAPIController _transferDataApiController; + + //_serviceProviderApiController = serviceProviderApiController; + //_transferDataApiController = transferDataApiController; + + // https://docs.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-3.1#strongly-typed-hubs + public override async Task OnConnectedAsync() + { + Logger.Debug($"Server OnConnectedAsync; ConnectionId: {Context.ConnectionId}; UserIdentifier: {Context.UserIdentifier}"); + + LogContextUserNameAndId(); + + await base.OnConnectedAsync(); + + //Clients.Caller.ConnectionId = Context.ConnectionId; + //Clients.Caller.UserIdentifier = Context.UserIdentifier; + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + var logText = $"Server OnDisconnectedAsync; ConnectionId: {Context.ConnectionId}; UserIdentifier: {Context.UserIdentifier};"; + + if (exception == null) Logger.Debug(logText); + else Logger.Error(logText, exception); + + LogContextUserNameAndId(); + + await base.OnDisconnectedAsync(exception); + } + + public virtual Task OnReceiveMessage(int messageTag, byte[]? message, int? requestId) + { + return ProcessOnReceiveMessage(messageTag, message, requestId, null); + } + + protected async Task ProcessOnReceiveMessage(int messageTag, byte[]? message, int? requestId, Func? notFoundCallback) + { + var tagName = ConstHelper.NameByValue(messageTag); + var logText = $"Server OnReceiveMessage; {nameof(requestId)}: {requestId}; ConnectionId: {Context.ConnectionId}; {tagName}"; + + if (message is { Length: 0 }) Logger.Warning($"message.Length == 0! {logText}"); + else Logger.Info($"[{message?.Length:N0}b] {logText}"); + + try + { + if (AcDomain.IsDeveloperVersion) LogContextUserNameAndId(); + + foreach (var methodsByDeclaringObject in DynamicMethodCallModels) + { + if (!methodsByDeclaringObject.MethodsByMessageTag.TryGetValue(messageTag, out var methodInfoModel)) continue; + + object[]? paramValues = null; + + logText = $"Found dynamic method for the tag! method: {methodsByDeclaringObject.InstanceObject.GetType().Name}.{methodInfoModel.MethodInfo.Name}"; + + if (methodInfoModel.ParamInfos is { Length: > 0 }) + { + Logger.Debug($"{logText}({string.Join(", ", methodInfoModel.ParamInfos.Select(x => x.Name))}); {tagName}"); + + paramValues = new object[methodInfoModel.ParamInfos.Length]; + + var firstParamType = methodInfoModel.ParamInfos[0].ParameterType; + if (methodInfoModel.ParamInfos.Length > 1 || firstParamType == typeof(string) || firstParamType.IsEnum || firstParamType.IsValueType || firstParamType == typeof(DateTime)) + { + var msg = message!.MessagePackTo>(); + + for (var i = 0; i < msg.PostData.Ids.Count; i++) + { + //var obj = (string)msg.PostData.Ids[i]; + //if (msg.PostData.Ids[i] is Guid id) + //{ + // if (id.IsNullOrEmpty()) throw new NullReferenceException($"PostData.Id.IsNullOrEmpty(); Ids: {msg.PostData.Ids}"); + // paramValues[i] = id; + //} + //else if (Guid.TryParse(obj, out id)) + //{ + // if (id.IsNullOrEmpty()) throw new NullReferenceException($"PostData.Id.IsNullOrEmpty(); Ids: {msg.PostData.Ids}"); + // paramValues[i] = id; + //} + //else if (Enum.TryParse(methodInfoModel.ParameterType, obj, out var enumObj)) + //{ + // paramValues[i] = enumObj; + //} + //else paramValues[i] = Convert.ChangeType(obj, methodInfoModel.ParameterType); + + var obj = msg.PostData.Ids[i]; + //var config = new MapperConfiguration(cfg => + //{ + // cfg.CreateMap(obj.GetType(), methodInfoModel.ParameterType); + //}); + + //var mapper = new Mapper(config); + //paramValues[i] = mapper.Map(obj, methodInfoModel.ParameterType); + + //paramValues[i] = obj; + + var a = Array.CreateInstance(methodInfoModel.ParamInfos[i].ParameterType, 1); + + if (methodInfoModel.ParamInfos[i].ParameterType == typeof(Expression)) + { + //var serializer = new ExpressionSerializer(new JsonSerializer()); + //paramValues[i] = serializer.DeserializeText((string)(obj.JsonTo(a.GetType()) as Array)?.GetValue(0)!); + } + else paramValues[i] = (obj.JsonTo(a.GetType()) as Array)?.GetValue(0)!; + + } + } + else paramValues[0] = message!.MessagePackTo>(MessagePackSerializerOptions.Standard).PostDataJson.JsonTo(firstParamType)!; + } + else Logger.Debug($"{logText}(); {tagName}"); + + var responseDataJson = new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, methodInfoModel.MethodInfo.InvokeMethod(methodsByDeclaringObject.InstanceObject, paramValues)); + var responseDataJsonKiloBytes = System.Text.Encoding.Unicode.GetByteCount(responseDataJson.ResponseData!) / 1024; + + //File.WriteAllText(Path.Combine("h:", $"{requestId}.json"), responseDataJson.ResponseData); + + Logger.Info($"[{responseDataJsonKiloBytes}kb] responseData serialized to json"); + await ResponseToCaller(messageTag, responseDataJson, 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, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Error), requestId); + } + + protected async Task ResponseToCaller(int messageTag, ISignalRMessage message, int? requestId) + => await SendMessageToClient(Clients.Caller, messageTag, message, requestId); + + public async Task SendMessageToUserId(string userId, int messageTag, ISignalRMessage message, int? requestId) + => await SendMessageToClient(Clients.User(userId), messageTag, message, requestId); + + public async Task SendMessageToConnectionId(string connectionId, int messageTag, ISignalRMessage message, int? requestId) + => await SendMessageToClient(Clients.Client(Context.ConnectionId), messageTag, message, requestId); + + protected async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null) + { + var responseDataMessagePack = message.ToMessagePack(ContractlessStandardResolver.Options); + Logger.Info($"[{(responseDataMessagePack.Length/1024)}kb] Server sending responseDataMessagePack to client; {nameof(requestId)}: {requestId}; ConnectionId: {Context.ConnectionId}; {ConstHelper.NameByValue(messageTag)}"); + + await sendTo.OnReceiveMessage(messageTag, responseDataMessagePack, requestId); + } + + public async Task SendMessageToGroup(string groupId, int messageTag, string message) + { + //await Clients.Group(groupId).Post("", messageTag, message); + } + + //[Conditional("DEBUG")] + private void LogContextUserNameAndId() + { + string? userName = null; + var userId = Guid.Empty; + + if (Context.User != null) + { + userName = Context.User.Identity?.Name; + Guid.TryParse((string?)Context.User.FindFirstValue(ClaimTypes.NameIdentifier), out userId); + } + + if (AcDomain.IsDeveloperVersion) Logger.WarningConditional($"SignalR.Context; userName: {userName}; userId: {userId}"); + else Logger.Debug($"SignalR.Context; userName: {userName}; userId: {userId}"); + } +} \ No newline at end of file diff --git a/AyCode.Blazor.Components/Services/ExtensionMethods.cs b/AyCode.Blazor.Components/Services/ExtensionMethods.cs new file mode 100644 index 0000000..baf4d1e --- /dev/null +++ b/AyCode.Blazor.Components/Services/ExtensionMethods.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace AyCode.Blazor.Components.Services; + +public static class ExtensionMethods +{ + public static object? InvokeMethod(this MethodInfo methodInfo, object obj, params object[]? parameters) + { + if (methodInfo.GetCustomAttribute(typeof(AsyncStateMachineAttribute)) is AsyncStateMachineAttribute isAsyncTask) + { + dynamic awaitable = methodInfo.Invoke(obj, parameters)!; + return awaitable.GetAwaiter().GetResult(); + } + + return methodInfo.Invoke(obj, parameters); + } +} \ No newline at end of file diff --git a/AyCode.Blazor.Components/Services/Logins/AcWebAuthService.cs b/AyCode.Blazor.Components/Services/Logins/AcWebAuthService.cs new file mode 100644 index 0000000..1f052cf --- /dev/null +++ b/AyCode.Blazor.Components/Services/Logins/AcWebAuthService.cs @@ -0,0 +1,7 @@ +namespace AyCode.Blazor.Components.Services.Logins; + +public class AcWebAuthService +{ + public virtual void Logout() + {} +} \ No newline at end of file diff --git a/AyCode.Blazor.Controllers/AyCode.Blazor.Controllers.csproj b/AyCode.Blazor.Controllers/AyCode.Blazor.Controllers.csproj index 4284043..e5f88ff 100644 --- a/AyCode.Blazor.Controllers/AyCode.Blazor.Controllers.csproj +++ b/AyCode.Blazor.Controllers/AyCode.Blazor.Controllers.csproj @@ -58,7 +58,6 @@ - diff --git a/AyCode.Blazor.Models.Server/AyCode.Blazor.Models.Server.csproj b/AyCode.Blazor.Models.Server/AyCode.Blazor.Models.Server.csproj index ecc9c3a..5734d80 100644 --- a/AyCode.Blazor.Models.Server/AyCode.Blazor.Models.Server.csproj +++ b/AyCode.Blazor.Models.Server/AyCode.Blazor.Models.Server.csproj @@ -31,6 +31,9 @@ ..\..\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Models.Server.dll + + ..\..\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Services.dll + ..\..\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Services.Server.dll @@ -39,4 +42,8 @@ + + + + diff --git a/AyCode.Blazor.Models.Server/Models/DynamicMethodCallModel.cs b/AyCode.Blazor.Models.Server/Models/DynamicMethodCallModel.cs new file mode 100644 index 0000000..58ff192 --- /dev/null +++ b/AyCode.Blazor.Models.Server/Models/DynamicMethodCallModel.cs @@ -0,0 +1,35 @@ +using System.Collections.Concurrent; +using System.Reflection; +using AyCode.Services.SignalRs; + +namespace AyCode.Blazor.Models.Server.Models; + +public class DynamicMethodCallModel where TAttribute : TagAttribute +{ + public object InstanceObject { get; init; } + public ConcurrentDictionary> MethodsByMessageTag { get; init; } = new(); + + + public DynamicMethodCallModel(Type instanceObjectType) : this(instanceObjectType, null!) + { + } + + public DynamicMethodCallModel(Type instanceObjectType, params object[] constructorParams) : this(Activator.CreateInstance(instanceObjectType, constructorParams)!) + { + } + + public DynamicMethodCallModel(object instanceObject) + { + InstanceObject = instanceObject; + + foreach (var methodInfo in instanceObject.GetType().GetMethods()) + { + if (methodInfo.GetCustomAttribute(typeof(TAttribute)) is not TAttribute attribute) continue; + + if (MethodsByMessageTag.ContainsKey(attribute.MessageTag)) + throw new Exception($"Multiple SignaRMessageTag! messageTag: {attribute.MessageTag}; methodName: {methodInfo.Name}"); + + MethodsByMessageTag[attribute.MessageTag] = new MethodInfoModel(attribute, methodInfo!); + } + } +} \ No newline at end of file diff --git a/AyCode.Blazor.Models.Server/Models/MethodInfoModel.cs b/AyCode.Blazor.Models.Server/Models/MethodInfoModel.cs new file mode 100644 index 0000000..75bd3ec --- /dev/null +++ b/AyCode.Blazor.Models.Server/Models/MethodInfoModel.cs @@ -0,0 +1,24 @@ +using System.Reflection; +using AyCode.Services.SignalRs; + +namespace AyCode.Blazor.Models.Server.Models; + +public class MethodInfoModel where TAttribute : TagAttribute +{ + public ParameterInfo[]? ParamInfos { get; init; } = null; + public TAttribute Attribute { get; init; } + public MethodInfo MethodInfo { get; init; } + + public MethodInfoModel(TAttribute attribute, MethodInfo methodInfo) + { + Attribute = attribute; + MethodInfo = methodInfo; + + var parameters = methodInfo.GetParameters(); + + //if (parameters.Length > 1) + // throw new Exception("MethodInfoModel; parameters.Length > 1"); + + ParamInfos = parameters; + } +} \ No newline at end of file