From e0398074b81697dfbe1f95a3a7996029bfd7a88e Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Wed, 9 Oct 2024 11:40:55 +0900 Subject: [PATCH 01/27] WIP: Rework method binder --- .../Internal.Shared/GrpcMethodHelper.cs | 11 - src/MagicOnion.Internal/GrpcMethodHelper.cs | 11 - .../Binder/IMagicOnionGrpcMethod.cs | 14 + .../Binder/IMagicOnionGrpcMethodBinder.cs | 19 + .../Binder/IMagicOnionGrpcMethodProvider.cs | 28 + .../DynamicMagicOnionMethodProvider.cs | 137 +++++ .../Internal/MagicOnionGrpcMethodBinder.cs | 482 ++++++++++++++++++ .../MagicOnionGrpcServiceMethodProvider.cs | 62 +++ .../Binder/{ => Legacy}/MagicOnionService.cs | 0 .../{ => Legacy}/MagicOnionServiceBinder.cs | 10 +- .../MagicOnionServiceMethodProvider.cs | 0 .../Binder/MagicOnionClientStreamingMethod.cs | 50 ++ .../Binder/MagicOnionDuplexStreamingMethod.cs | 54 ++ .../Binder/MagicOnionServerStreamingMethod.cs | 54 ++ ...icOnionServiceEndpointConventionBuilder.cs | 7 + .../MagicOnionStreamingHubConnectMethod.cs | 22 + .../Binder/MagicOnionStreamingHubMethod.cs | 44 ++ .../Binder/MagicOnionUnaryMethod.cs | 48 ++ .../Diagnostics/MagicOnionServerLog.cs | 9 + ...agicOnionEndpointRouteBuilderExtensions.cs | 7 + .../MagicOnionServicesExtensions.cs | 10 +- .../Hubs/HubGroupRepository.cs | 2 +- .../Hubs/Internal/StreamingHubRegistry.cs | 121 +++++ src/MagicOnion.Server/Hubs/StreamingHub.cs | 32 +- .../Internal/IServiceBase.cs | 9 + .../Internal/IStreamingHubBase.cs | 8 + .../MagicOnion.Server.csproj | 2 +- src/MagicOnion.Server/MagicOnionEngine.cs | 4 +- .../MagicOnionServiceDefinition.cs | 4 +- src/MagicOnion.Server/MethodHandler.cs | 88 +++- src/MagicOnion.Server/Service.cs | 14 +- .../ServiceContext.Streaming.cs | 4 +- src/MagicOnion.Server/ServiceContext.cs | 8 + ...HandCraftedMagicOnionMethodProviderTest.cs | 179 +++++++ .../MagicOnionApplicationFactory.cs | 14 +- 35 files changed, 1493 insertions(+), 75 deletions(-) create mode 100644 src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs create mode 100644 src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodBinder.cs create mode 100644 src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs create mode 100644 src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs create mode 100644 src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs create mode 100644 src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs rename src/MagicOnion.Server/Binder/{ => Legacy}/MagicOnionService.cs (100%) rename src/MagicOnion.Server/Binder/{ => Legacy}/MagicOnionServiceBinder.cs (96%) rename src/MagicOnion.Server/Binder/{ => Legacy}/MagicOnionServiceMethodProvider.cs (100%) create mode 100644 src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs create mode 100644 src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs create mode 100644 src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs create mode 100644 src/MagicOnion.Server/Binder/MagicOnionServiceEndpointConventionBuilder.cs create mode 100644 src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs create mode 100644 src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs create mode 100644 src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs create mode 100644 src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs create mode 100644 src/MagicOnion.Server/Internal/IServiceBase.cs create mode 100644 src/MagicOnion.Server/Internal/IStreamingHubBase.cs create mode 100644 tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/GrpcMethodHelper.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/GrpcMethodHelper.cs index c4369eb6d..0d59cdc55 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/GrpcMethodHelper.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/GrpcMethodHelper.cs @@ -23,11 +23,6 @@ public static T FromRaw(TRaw obj) public static Method, TRawResponse> CreateMethod(MethodType methodType, string serviceName, string name, IMagicOnionSerializer messageSerializer) where TRawResponse : class - { - return CreateMethod(methodType, serviceName, name, null, messageSerializer); - } - public static Method, TRawResponse> CreateMethod(MethodType methodType, string serviceName, string name, MethodInfo? methodInfo, IMagicOnionSerializer messageSerializer) - where TRawResponse : class { // WORKAROUND: Prior to MagicOnion 5.0, the request type for the parameter-less method was byte[]. // DynamicClient sends byte[], but GeneratedClient sends Nil, which is incompatible, @@ -49,12 +44,6 @@ public static Method, TRawResponse> CreateMethod CreateMethod(MethodType methodType, string serviceName, string name, IMagicOnionSerializer messageSerializer) where TRawRequest : class where TRawResponse : class - { - return CreateMethod(methodType, serviceName, name, null, messageSerializer); - } - public static Method CreateMethod(MethodType methodType, string serviceName, string name, MethodInfo? methodInfo, IMagicOnionSerializer messageSerializer) - where TRawRequest : class - where TRawResponse : class { var isMethodRequestTypeBoxed = typeof(TRequest).IsValueType; var isMethodResponseTypeBoxed = typeof(TResponse).IsValueType; diff --git a/src/MagicOnion.Internal/GrpcMethodHelper.cs b/src/MagicOnion.Internal/GrpcMethodHelper.cs index c4369eb6d..0d59cdc55 100644 --- a/src/MagicOnion.Internal/GrpcMethodHelper.cs +++ b/src/MagicOnion.Internal/GrpcMethodHelper.cs @@ -23,11 +23,6 @@ public static T FromRaw(TRaw obj) public static Method, TRawResponse> CreateMethod(MethodType methodType, string serviceName, string name, IMagicOnionSerializer messageSerializer) where TRawResponse : class - { - return CreateMethod(methodType, serviceName, name, null, messageSerializer); - } - public static Method, TRawResponse> CreateMethod(MethodType methodType, string serviceName, string name, MethodInfo? methodInfo, IMagicOnionSerializer messageSerializer) - where TRawResponse : class { // WORKAROUND: Prior to MagicOnion 5.0, the request type for the parameter-less method was byte[]. // DynamicClient sends byte[], but GeneratedClient sends Nil, which is incompatible, @@ -49,12 +44,6 @@ public static Method, TRawResponse> CreateMethod CreateMethod(MethodType methodType, string serviceName, string name, IMagicOnionSerializer messageSerializer) where TRawRequest : class where TRawResponse : class - { - return CreateMethod(methodType, serviceName, name, null, messageSerializer); - } - public static Method CreateMethod(MethodType methodType, string serviceName, string name, MethodInfo? methodInfo, IMagicOnionSerializer messageSerializer) - where TRawRequest : class - where TRawResponse : class { var isMethodRequestTypeBoxed = typeof(TRequest).IsValueType; var isMethodResponseTypeBoxed = typeof(TResponse).IsValueType; diff --git a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs new file mode 100644 index 000000000..085a12f30 --- /dev/null +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs @@ -0,0 +1,14 @@ +using System.Reflection; + +namespace MagicOnion.Server.Binder; + +public interface IMagicOnionGrpcMethod; + +public interface IMagicOnionGrpcMethod : IMagicOnionGrpcMethod + where TService : class +{ + string ServiceName { get; } + string MethodName { get; } + MethodInfo MethodInfo { get; } + void Bind(IMagicOnionGrpcMethodBinder binder); +} diff --git a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodBinder.cs b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodBinder.cs new file mode 100644 index 000000000..62c9fb7b3 --- /dev/null +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodBinder.cs @@ -0,0 +1,19 @@ +namespace MagicOnion.Server.Binder; + +public interface IMagicOnionGrpcMethodBinder + where TService : class +{ + void BindUnary(IMagicOnionUnaryMethod method) + where TRawRequest : class + where TRawResponse : class; + void BindClientStreaming(MagicOnionClientStreamingMethod method) + where TRawRequest : class + where TRawResponse : class; + void BindServerStreaming(MagicOnionServerStreamingMethod method) + where TRawRequest : class + where TRawResponse : class; + void BindDuplexStreaming(MagicOnionDuplexStreamingMethod method) + where TRawRequest : class + where TRawResponse : class; + void BindStreamingHub(MagicOnionStreamingHubConnectMethod method); +} diff --git a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs new file mode 100644 index 000000000..2abbbad01 --- /dev/null +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace MagicOnion.Server.Binder; + +public class MagicOnionGrpcServiceRegistrationContext(IEndpointRouteBuilder builder) +{ + public MagicOnionServiceEndpointConventionBuilder Register() + where T : class, IServiceMarker + { + return new MagicOnionServiceEndpointConventionBuilder(builder.MapGrpcService()); + } + + public MagicOnionServiceEndpointConventionBuilder Register(Type t) + { + return new MagicOnionServiceEndpointConventionBuilder((GrpcServiceEndpointConventionBuilder)typeof(GrpcEndpointRouteBuilderExtensions) + .GetMethod(nameof(GrpcEndpointRouteBuilderExtensions.MapGrpcService))! + .MakeGenericMethod(t) + .Invoke(null, [builder])!); + } +} + +public interface IMagicOnionGrpcMethodProvider +{ + void OnRegisterGrpcServices(MagicOnionGrpcServiceRegistrationContext context); + IEnumerable GetGrpcMethods() where TService : class; + IEnumerable GetStreamingHubMethods() where TService : class; +} diff --git a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs new file mode 100644 index 000000000..388785b2c --- /dev/null +++ b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs @@ -0,0 +1,137 @@ +using System.Linq.Expressions; +using System.Reflection; +using MagicOnion.Internal; + +namespace MagicOnion.Server.Binder.Internal; + +internal class DynamicMagicOnionMethodProvider : IMagicOnionGrpcMethodProvider +{ + readonly MagicOnionServiceDefinition definition; + + public DynamicMagicOnionMethodProvider(MagicOnionServiceDefinition definition) + { + this.definition = definition; + } + + public void OnRegisterGrpcServices(MagicOnionGrpcServiceRegistrationContext context) + { + foreach (var serviceType in this.definition.TargetTypes.Distinct()) + { + context.Register(serviceType); + } + } + + public IEnumerable GetGrpcMethods() where TService : class + { + var typeServiceImplementation = typeof(TService); + var typeServiceInterface = typeServiceImplementation.GetInterfaces() + .First(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IService<>)) + .GenericTypeArguments[0]; + + // StreamingHub + if (typeof(TService).IsAssignableTo(typeof(IStreamingHubMarker))) + { + yield return new MagicOnionStreamingHubConnectMethod(typeServiceInterface.Name); + yield break; + } + + // Unary, ClientStreaming, ServerStreaming, DuplexStreaming + var interfaceMap = typeServiceImplementation.GetInterfaceMapWithParents(typeServiceInterface); + for (var i = 0; i < interfaceMap.TargetMethods.Length; i++) + { + var methodInfo = interfaceMap.TargetMethods[i]; + var methodName = interfaceMap.InterfaceMethods[i].Name; + + if (methodInfo.IsSpecialName && (methodInfo.Name.StartsWith("set_") || methodInfo.Name.StartsWith("get_"))) continue; + if (methodInfo.GetCustomAttribute(false) != null) continue; // ignore + if (methodName is "Equals" or "GetHashCode" or "GetType" or "ToString" or "WithOptions" or "WithHeaders" or "WithDeadline" or "WithCancellationToken" or "WithHost") continue; + + var targetMethod = methodInfo; + var methodParameters = targetMethod.GetParameters(); + var typeRequest = methodParameters is { Length: > 1 } + ? typeof(DynamicArgumentTuple<,>).MakeGenericType(methodParameters[0].ParameterType, methodParameters[1].ParameterType) + : methodParameters is { Length: 1 } + ? methodParameters[0].ParameterType + : typeof(MessagePack.Nil); + var typeRawRequest = typeRequest.IsValueType + ? typeof(Box<>).MakeGenericType(typeRequest) + : typeRequest; + + Type typeMethod; + if (targetMethod.ReturnType == typeof(UnaryResult)) + { + // UnaryResult: The method has no return value. + typeMethod = typeof(MagicOnionUnaryMethod<,,>).MakeGenericType(typeServiceImplementation, typeRequest, typeRawRequest); + } + else + { + // UnaryResult + var typeResponse = targetMethod.ReturnType.GetGenericArguments()[0]; + var typeRawResponse = typeResponse.IsValueType + ? typeof(Box<>).MakeGenericType(typeResponse) + : typeResponse; + typeMethod = typeof(MagicOnionUnaryMethod<,,,,>).MakeGenericType(typeServiceImplementation, typeRequest, typeResponse, typeRawRequest, typeRawResponse); + } + + var exprParamInstance = Expression.Parameter(typeServiceImplementation); + var exprParamRequest = Expression.Parameter(typeRequest); + var exprParamServiceContext = Expression.Parameter(typeof(ServiceContext)); + var exprArguments = methodParameters.Length == 1 + ? [exprParamRequest] + : methodParameters + .Select((x, i) => Expression.Field(exprParamRequest, "Item" + (i + 1))) + .Cast() + .ToArray(); + + var exprCall = Expression.Call(exprParamInstance, targetMethod, exprArguments); + var invoker = Expression.Lambda(exprCall, [exprParamInstance, exprParamRequest, exprParamServiceContext]).Compile(); + + var serviceMethod = Activator.CreateInstance(typeMethod, [typeServiceInterface.Name, targetMethod.Name, invoker])!; + yield return (IMagicOnionGrpcMethod)serviceMethod; + } + } + + public IEnumerable GetStreamingHubMethods() where TService : class + { + if (!typeof(TService).IsAssignableTo(typeof(IStreamingHubMarker))) + { + yield break; + } + + var typeServiceImplementation = typeof(TService); + var typeServiceInterface = typeServiceImplementation.GetInterfaces() + .First(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IService<>)) + .GenericTypeArguments[0]; + + var interfaceMap = typeServiceImplementation.GetInterfaceMapWithParents(typeServiceInterface); + for (var i = 0; i < interfaceMap.TargetMethods.Length; i++) + { + var methodInfo = interfaceMap.TargetMethods[i]; + var methodName = interfaceMap.InterfaceMethods[i].Name; + + if (methodInfo.IsSpecialName && (methodInfo.Name.StartsWith("set_") || methodInfo.Name.StartsWith("get_"))) continue; + if (methodInfo.GetCustomAttribute(false) != null) continue; // ignore + if (methodName is "Equals" or "GetHashCode" or "GetType" or "ToString" or "WithOptions" or "WithHeaders" or "WithDeadline" or "WithCancellationToken" or "WithHost") continue; + + var methodParameters = methodInfo.GetParameters(); + var typeRequest = methodParameters is { Length: > 1 } + ? typeof(DynamicArgumentTuple<,>).MakeGenericType(methodParameters[0].ParameterType, methodParameters[1].ParameterType) + : methodParameters is { Length: 1 } + ? methodParameters[0].ParameterType + : typeof(MessagePack.Nil); + var typeResponse = methodInfo.ReturnType; + + Type hubMethodType; + if (typeResponse == typeof(ValueTask) || typeResponse == typeof(Task) || typeResponse == typeof(void)) + { + hubMethodType = typeof(MagicOnionStreamingHubMethod<,>).MakeGenericType([typeServiceImplementation, typeRequest]); + } + else + { + hubMethodType = typeof(MagicOnionStreamingHubMethod<,,>).MakeGenericType([typeServiceImplementation, typeRequest, typeResponse]); + } + + yield return (IMagicOnionStreamingHubMethod)Activator.CreateInstance(hubMethodType, [typeServiceInterface.Name, methodInfo.Name, methodInfo])!; + } + } +} diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs new file mode 100644 index 000000000..c59ae9d6b --- /dev/null +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs @@ -0,0 +1,482 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using Grpc.AspNetCore.Server.Model; +using Grpc.Core; +using MagicOnion.Internal; +using MagicOnion.Serialization; +using MagicOnion.Server.Diagnostics; +using MagicOnion.Server.Filters; +using MagicOnion.Server.Filters.Internal; +using MagicOnion.Server.Hubs.Internal; +using MagicOnion.Server.Internal; +using MessagePack; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace MagicOnion.Server.Binder.Internal; + +internal class MagicOnionGrpcMethodBinder : IMagicOnionGrpcMethodBinder + where TService : class +{ + readonly ServiceMethodProviderContext providerContext; + readonly IMagicOnionSerializerProvider messageSerializerProvider; + readonly IList globalFilters; + readonly IServiceProvider serviceProvider; + readonly ILogger logger; + + readonly bool enableCurrentContext; + readonly bool isReturnExceptionStackTraceInErrorDetail; + + public MagicOnionGrpcMethodBinder(ServiceMethodProviderContext context, MagicOnionOptions options, IServiceProvider serviceProvider, ILogger> logger) + { + this.providerContext = context; + this.messageSerializerProvider = options.MessageSerializer; + this.globalFilters = options.GlobalFilters; + this.serviceProvider = serviceProvider; + this.logger = logger; + this.enableCurrentContext = options.EnableCurrentContext; + this.isReturnExceptionStackTraceInErrorDetail = options.IsReturnExceptionStackTraceInErrorDetail; + } + + public void BindUnary(IMagicOnionUnaryMethod method) + where TRawRequest : class + where TRawResponse : class + { + var messageSerializer = messageSerializerProvider.Create(MethodType.Unary, default); + var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.Unary, method.ServiceName, method.MethodName, messageSerializer); + var attrs = GetMetadataFromHandler(method.MethodInfo); + + providerContext.AddUnaryMethod(grpcMethod, attrs, BuildUnaryMethodPipeline(method, messageSerializer, attrs)); + } + + public void BindClientStreaming(MagicOnionClientStreamingMethod method) + where TRawRequest : class + where TRawResponse : class + { + var messageSerializer = messageSerializerProvider.Create(MethodType.ClientStreaming, default); + var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ClientStreaming, method.ServiceName, method.MethodName, messageSerializer); + var attrs = GetMetadataFromHandler(method.MethodInfo); + + providerContext.AddClientStreamingMethod(grpcMethod, attrs, BuildClientStreamingMethodPipeline(method, messageSerializer, attrs)); + } + + public void BindServerStreaming(MagicOnionServerStreamingMethod method) + where TRawRequest : class + where TRawResponse : class + { + var messageSerializer = messageSerializerProvider.Create(MethodType.ServerStreaming, default); + var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ServerStreaming, method.ServiceName, method.MethodName, messageSerializer); + var attrs = GetMetadataFromHandler(method.MethodInfo); + + providerContext.AddServerStreamingMethod(grpcMethod, attrs, BuildServerStreamingMethodPipeline(method, messageSerializer, attrs)); + } + + public void BindDuplexStreaming(MagicOnionDuplexStreamingMethod method) + where TRawRequest : class + where TRawResponse : class + { + var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, default); + var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.DuplexStreaming, method.ServiceName, method.MethodName, messageSerializer); + var attrs = GetMetadataFromHandler(method.MethodInfo); + + providerContext.AddDuplexStreamingMethod(grpcMethod, attrs, BuildDuplexStreamingMethodPipeline(method, messageSerializer, attrs)); + } + + public void BindStreamingHub(MagicOnionStreamingHubConnectMethod method) + { + var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, default); + // StreamingHub uses the special marshallers for streaming messages serialization. + // TODO: Currently, MagicOnion expects TRawRequest/TRawResponse to be raw-byte array (`StreamingHubPayload`). + var grpcMethod = new Method( + MethodType.DuplexStreaming, + method.ServiceName, + method.MethodName, + MagicOnionMarshallers.StreamingHubMarshaller, + MagicOnionMarshallers.StreamingHubMarshaller + ); + var attrs = GetMetadataFromHandler(method.MethodInfo); + + var duplexMethod = new MagicOnionDuplexStreamingMethod( + method.ServiceName, + method.MethodName, + static (instance, context) => + { + context.CallContext.GetHttpContext().Features.Set(context.ServiceProvider.GetRequiredService>()); + return ((IStreamingHubBase)instance).Connect(); + }); + providerContext.AddDuplexStreamingMethod(grpcMethod, attrs, BuildDuplexStreamingMethodPipeline(duplexMethod, messageSerializer, attrs)); + } + + IList GetMetadataFromHandler(MethodInfo methodInfo) + { + // NOTE: We need to collect Attributes for Endpoint metadata. ([Authorize], [AllowAnonymous] ...) + // https://github.com/grpc/grpc-dotnet/blob/7ef184f3c4cd62fbc3cde55e4bb3e16b58258ca1/src/Grpc.AspNetCore.Server/Model/Internal/ProviderServiceBinder.cs#L89-L98 + var metadata = new List(); + metadata.AddRange(methodInfo.DeclaringType!.GetCustomAttributes(inherit: true)); + metadata.AddRange(methodInfo.GetCustomAttributes(inherit: true)); + + metadata.Add(new HttpMethodMetadata(["POST"], acceptCorsPreflight: true)); + return metadata; + } + + void InitializeServiceProperties(object instance, ServiceContext serviceContext) + { + var service = ((IServiceBase)instance); + service.Context = serviceContext; + service.Metrics = serviceProvider.GetRequiredService(); + } + + ClientStreamingServerMethod BuildClientStreamingMethodPipeline( + MagicOnionClientStreamingMethod method, + IMagicOnionSerializer messageSerializer, + IList metadata + ) + where TRawRequest : class + where TRawResponse : class + { + var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); + var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); + var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext)); + + return InvokeAsync; + + async Task InvokeAsync(TService instance, IAsyncStreamReader rawRequestStream, ServerCallContext context) + { + var isCompletedSuccessfully = false; + var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); + + var requestStream = new MagicOnionAsyncStreamReader(rawRequestStream); + var serviceContext = new StreamingServiceContext( + instance, + typeof(TService), + method.ServiceName, + method.MethodInfo, + attributeLookup, + MethodType.ClientStreaming, + context, + messageSerializer, + logger, + default!, + context.GetHttpContext().RequestServices, + requestStream, + default + ); + + InitializeServiceProperties(instance, serviceContext); + + TResponse response; + try + { + using (rawRequestStream as IDisposable) + { + MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(Nil)); + if (enableCurrentContext) + { + ServiceContext.currentServiceContext.Value = serviceContext; + } + await wrappedBody(serviceContext); + response = serviceContext.Result is TResponse r ? r : default!; + isCompletedSuccessfully = true; + } + } + catch (ReturnStatusException ex) + { + context.Status = ex.ToStatus(); + response = default!; + } + catch (Exception ex) + { + if (TryResolveStatus(ex, out var status)) + { + context.Status = status.Value; + MagicOnionServerLog.Error(logger, ex, context); + response = default!; + } + else + { + throw; + } + } + finally + { + MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); + } + + return GrpcMethodHelper.ToRaw(response); + } + } + + ServerStreamingServerMethod BuildServerStreamingMethodPipeline( + MagicOnionServerStreamingMethod method, + IMagicOnionSerializer messageSerializer, + IList metadata + ) + where TRawRequest : class + where TRawResponse : class + { + var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); + var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); + var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, (TRequest)serviceContext.Request!, serviceContext)); + + return InvokeAsync; + + async Task InvokeAsync(TService instance, TRawRequest rawRequest, IServerStreamWriter rawResponseStream, ServerCallContext context) + { + var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); + var isCompletedSuccessfully = false; + + var request = GrpcMethodHelper.FromRaw(rawRequest); + var responseStream = new MagicOnionServerStreamWriter(rawResponseStream); + var serviceContext = new StreamingServiceContext( + instance, + typeof(TService), + method.ServiceName, + method.MethodInfo, + attributeLookup, + MethodType.ServerStreaming, + context, + messageSerializer, + logger, + default!, + context.GetHttpContext().RequestServices, + default, + responseStream + ); + + serviceContext.SetRawRequest(request); + + InitializeServiceProperties(instance, serviceContext); + + try + { + MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(Nil)); + if (enableCurrentContext) + { + ServiceContext.currentServiceContext.Value = serviceContext; + } + await wrappedBody(serviceContext); + isCompletedSuccessfully = true; + } + catch (ReturnStatusException ex) + { + context.Status = ex.ToStatus(); + } + catch (Exception ex) + { + if (TryResolveStatus(ex, out var status)) + { + context.Status = status.Value; + MagicOnionServerLog.Error(logger, ex, context); + } + else + { + throw; + } + } + finally + { + MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); + } + } + } + + DuplexStreamingServerMethod BuildDuplexStreamingMethodPipeline( + MagicOnionDuplexStreamingMethod method, + IMagicOnionSerializer messageSerializer, + IList metadata + ) + where TRawRequest : class + where TRawResponse : class + { + var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); + var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); + var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext)); + + return InvokeAsync; + + async Task InvokeAsync(TService instance, IAsyncStreamReader rawRequestStream, IServerStreamWriter rawResponseStream, ServerCallContext context) + { + var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); + var isCompletedSuccessfully = false; + + var requestStream = new MagicOnionAsyncStreamReader(rawRequestStream); + var responseStream = new MagicOnionServerStreamWriter(rawResponseStream); + var serviceContext = new StreamingServiceContext( + instance, + typeof(TService), + method.ServiceName, + method.MethodInfo, + attributeLookup, + MethodType.DuplexStreaming, + context, + messageSerializer, + logger, + default!, + context.GetHttpContext().RequestServices, + requestStream, + responseStream + ); + + InitializeServiceProperties(instance, serviceContext); + + try + { + MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(Nil)); + if (enableCurrentContext) + { + ServiceContext.currentServiceContext.Value = serviceContext; + } + + using (rawRequestStream as IDisposable) + { + await wrappedBody(serviceContext); + } + + isCompletedSuccessfully = true; + } + catch (ReturnStatusException ex) + { + context.Status = ex.ToStatus(); + } + catch (Exception ex) + { + if (TryResolveStatus(ex, out var status)) + { + context.Status = status.Value; + MagicOnionServerLog.Error(logger, ex, context); + } + else + { + throw; + } + } + finally + { + MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); + } + } + } + + UnaryServerMethod BuildUnaryMethodPipeline( + IMagicOnionUnaryMethod method, + IMagicOnionSerializer messageSerializer, + IList metadata + ) + where TRawRequest : class + where TRawResponse : class + { + var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); + var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); + var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, (TRequest)serviceContext.Request!, serviceContext)); + + return InvokeAsync; + + async Task InvokeAsync(TService instance, TRawRequest requestRaw, ServerCallContext context) + { + var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); + var isCompletedSuccessfully = false; + + var serviceContext = new ServiceContext(instance, typeof(TService), method.ServiceName, method.MethodInfo, attributeLookup, MethodType.Unary, context, messageSerializer, logger, default!, context.GetHttpContext().RequestServices); + var request = GrpcMethodHelper.FromRaw(requestRaw); + + serviceContext.SetRawRequest(request); + + InitializeServiceProperties(instance, serviceContext); + + TResponse response = default!; + try + { + MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(TRequest)); + + if (enableCurrentContext) + { + ServiceContext.currentServiceContext.Value = serviceContext; + } + + await wrappedBody(serviceContext); + + isCompletedSuccessfully = true; + + if (serviceContext.Result is not null) + { + response = (TResponse)serviceContext.Result; + } + + if (response is RawBytesBox rawBytesResponse) + { + return Unsafe.As(ref rawBytesResponse); // NOTE: To disguise an object as a `TRawResponse`, `TRawResponse` must be `class`. + } + } + catch (ReturnStatusException ex) + { + context.Status = ex.ToStatus(); + response = default!; + + // WORKAROUND: Grpc.AspNetCore.Server throws a `Cancelled` status exception when it receives `null` response. + // To return the status code correctly, we need to rethrow the exception here. + // https://github.com/grpc/grpc-dotnet/blob/d4ee8babcd90666fc0727163a06527ab9fd7366a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs#L50-L56 + var rpcException = new RpcException(ex.ToStatus()); + if (ex.StackTrace is not null) + { + ExceptionDispatchInfo.SetRemoteStackTrace(rpcException, ex.StackTrace); + } + throw rpcException; + } + catch (Exception ex) + { + if (TryResolveStatus(ex, out var status)) + { + context.Status = status.Value; + MagicOnionServerLog.Error(logger, ex, context); + response = default!; + } + else + { + throw; + } + } + finally + { + MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); + } + + return GrpcMethodHelper.ToRaw(response); + } + } + + bool TryResolveStatus(Exception ex, [NotNullWhen(true)] out Status? status) + { + if (isReturnExceptionStackTraceInErrorDetail) + { + // Trim data. + var msg = ex.ToString(); + var lineSplit = msg.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + var sb = new System.Text.StringBuilder(); + for (int i = 0; i < lineSplit.Length; i++) + { + if (!(lineSplit[i].Contains("System.Runtime.CompilerServices") + || lineSplit[i].Contains("直前に例外がスローされた場所からのスタック トレースの終わり") + || lineSplit[i].Contains("End of stack trace from the previous location where the exception was thrown") + )) + { + sb.AppendLine(lineSplit[i]); + } + if (sb.Length >= 5000) + { + sb.AppendLine("----Omit Message(message size is too long)----"); + break; + } + } + var str = sb.ToString(); + + status = new Status(StatusCode.Unknown, str); + return true; + } + + status = default; + return false; + } +} diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs new file mode 100644 index 000000000..f82fa06b5 --- /dev/null +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs @@ -0,0 +1,62 @@ +using Grpc.AspNetCore.Server.Model; +using MagicOnion.Server.Diagnostics; +using MagicOnion.Server.Hubs.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace MagicOnion.Server.Binder.Internal; + +internal class MagicOnionGrpcServiceMethodProvider : IServiceMethodProvider + where TService : class +{ + readonly IMagicOnionGrpcMethodProvider[] methodProviders; + readonly MagicOnionOptions options; + readonly IServiceProvider serviceProvider; + readonly ILoggerFactory loggerFactory; + readonly ILogger logger; + + public MagicOnionGrpcServiceMethodProvider(IEnumerable methodProviders, IOptions options, IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ILogger> logger) + { + this.methodProviders = methodProviders.ToArray(); + this.options = options.Value; + this.serviceProvider = serviceProvider; + this.loggerFactory = loggerFactory; + this.logger = logger; + } + + public void OnServiceMethodDiscovery(ServiceMethodProviderContext context) + { + if (!typeof(TService).IsAssignableTo(typeof(IServiceMarker))) return; + + var methodBinderLogger = loggerFactory.CreateLogger>(); + var binder = new MagicOnionGrpcMethodBinder(context, options, serviceProvider, methodBinderLogger); + + var registered = false; + foreach (var methodProvider in methodProviders.OrderBy(x => x is DynamicMagicOnionMethodProvider ? 1 : 0)) // DynamicMagicOnionMethodProvider is always last. + { + foreach (var method in methodProvider.GetGrpcMethods()) + { + ((IMagicOnionGrpcMethod)method).Bind(binder); + registered = true; + } + + if (typeof(TService).IsAssignableTo(typeof(IStreamingHubMarker))) + { + var registry = serviceProvider.GetRequiredService>(); + registry.RegisterMethods(methodProvider.GetStreamingHubMethods()); + } + + if (registered) + { + MagicOnionServerLog.ServiceMethodDiscovered(logger, typeof(TService).Name, methodProvider.GetType().FullName!); + return; + } + } + + if (!registered) + { + MagicOnionServerLog.ServiceMethodNotDiscovered(logger, typeof(TService).Name); + } + } +} diff --git a/src/MagicOnion.Server/Binder/MagicOnionService.cs b/src/MagicOnion.Server/Binder/Legacy/MagicOnionService.cs similarity index 100% rename from src/MagicOnion.Server/Binder/MagicOnionService.cs rename to src/MagicOnion.Server/Binder/Legacy/MagicOnionService.cs diff --git a/src/MagicOnion.Server/Binder/MagicOnionServiceBinder.cs b/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceBinder.cs similarity index 96% rename from src/MagicOnion.Server/Binder/MagicOnionServiceBinder.cs rename to src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceBinder.cs index 3b2e29b5b..e893152dc 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionServiceBinder.cs +++ b/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceBinder.cs @@ -66,7 +66,7 @@ public void BindUnary(MagicOnion where TRawRequest : class where TRawResponse : class { - var method = GrpcMethodHelper.CreateMethod(MethodType.Unary, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MethodInfo, ctx.MethodHandler.MessageSerializer); + var method = GrpcMethodHelper.CreateMethod(MethodType.Unary, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MessageSerializer); UnaryServerMethod invoker = async (_, request, context) => { var response = await serverMethod(GrpcMethodHelper.FromRaw(request), context); @@ -88,7 +88,7 @@ public void BindUnaryParameterless(MethodType.Unary, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MethodInfo, ctx.MethodHandler.MessageSerializer); + var method = GrpcMethodHelper.CreateMethod(MethodType.Unary, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MessageSerializer); UnaryServerMethod, TRawResponse> invoker = async (_, request, context) => { var response = await serverMethod(GrpcMethodHelper.FromRaw, Nil>(request), context); @@ -131,7 +131,7 @@ public void BindDuplexStreaming( where TRawRequest : class where TRawResponse : class { - var method = GrpcMethodHelper.CreateMethod(MethodType.DuplexStreaming, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MethodInfo, ctx.MethodHandler.MessageSerializer); + var method = GrpcMethodHelper.CreateMethod(MethodType.DuplexStreaming, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MessageSerializer); DuplexStreamingServerMethod invoker = async (_, request, response, context) => await serverMethod( new MagicOnionAsyncStreamReader(request), new MagicOnionServerStreamWriter(response), @@ -145,7 +145,7 @@ public void BindServerStreaming( where TRawRequest : class where TRawResponse : class { - var method = GrpcMethodHelper.CreateMethod(MethodType.ServerStreaming, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MethodInfo, ctx.MethodHandler.MessageSerializer); + var method = GrpcMethodHelper.CreateMethod(MethodType.ServerStreaming, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MessageSerializer); ServerStreamingServerMethod invoker = async (_, request, response, context) => await serverMethod( GrpcMethodHelper.FromRaw(request), new MagicOnionServerStreamWriter(response), @@ -159,7 +159,7 @@ public void BindClientStreaming( where TRawRequest : class where TRawResponse : class { - var method = GrpcMethodHelper.CreateMethod(MethodType.ClientStreaming, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MethodInfo, ctx.MethodHandler.MessageSerializer); + var method = GrpcMethodHelper.CreateMethod(MethodType.ClientStreaming, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MessageSerializer); ClientStreamingServerMethod invoker = async (_, request, context) => GrpcMethodHelper.ToRaw((await serverMethod( new MagicOnionAsyncStreamReader(request), context diff --git a/src/MagicOnion.Server/Binder/MagicOnionServiceMethodProvider.cs b/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceMethodProvider.cs similarity index 100% rename from src/MagicOnion.Server/Binder/MagicOnionServiceMethodProvider.cs rename to src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceMethodProvider.cs diff --git a/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs new file mode 100644 index 000000000..69129b417 --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs @@ -0,0 +1,50 @@ +using System.Reflection; + +namespace MagicOnion.Server.Binder; + +public class MagicOnionClientStreamingMethod : IMagicOnionGrpcMethod + where TService : class + where TRawRequest : class + where TRawResponse : class +{ + + readonly Func>> invoker; + + public string ServiceName { get; } + public string MethodName { get; } + + public MethodInfo MethodInfo { get; } + + public MagicOnionClientStreamingMethod(string serviceName, string methodName, Func> invoker) + { + ServiceName = serviceName; + MethodName = methodName; + MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + + this.invoker = (service, context) => ValueTask.FromResult(invoker(service, context)); + } + + public MagicOnionClientStreamingMethod(string serviceName, string methodName, Func>> invoker) + { + ServiceName = serviceName; + MethodName = methodName; + MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + + this.invoker = (service, context) => new ValueTask>(invoker(service, context)); + } + + public MagicOnionClientStreamingMethod(string serviceName, string methodName, Func>> invoker) + { + ServiceName = serviceName; + MethodName = methodName; + MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + + this.invoker = invoker; + } + + public void Bind(IMagicOnionGrpcMethodBinder binder) + => binder.BindClientStreaming(this); + + public ValueTask InvokeAsync(TService service, ServiceContext context) + => MethodHandlerResultHelper.SerializeValueTaskClientStreamingResult(invoker(service, context), context); +} diff --git a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs new file mode 100644 index 000000000..7e214f147 --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs @@ -0,0 +1,54 @@ +using System.Reflection; + +namespace MagicOnion.Server.Binder; + +public class MagicOnionDuplexStreamingMethod : IMagicOnionGrpcMethod + where TService : class + where TRawRequest : class + where TRawResponse : class +{ + + readonly Func invoker; + + public string ServiceName { get; } + public string MethodName { get; } + + public MethodInfo MethodInfo { get; } + + public MagicOnionDuplexStreamingMethod(string serviceName, string methodName, Func> invoker) + { + ServiceName = serviceName; + MethodName = methodName; + MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + + this.invoker = (service, context) => + { + invoker(service, context); + return default; + }; + } + + public MagicOnionDuplexStreamingMethod(string serviceName, string methodName, Func>> invoker) + { + ServiceName = serviceName; + MethodName = methodName; + MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + + this.invoker = (service, context) => new ValueTask(invoker(service, context)); + } + + public MagicOnionDuplexStreamingMethod(string serviceName, string methodName, Func>> invoker) + { + ServiceName = serviceName; + MethodName = methodName; + MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + + this.invoker = async (service, context) => await invoker(service, context); + } + + public void Bind(IMagicOnionGrpcMethodBinder binder) + => binder.BindDuplexStreaming(this); + + public ValueTask InvokeAsync(TService service, ServiceContext context) + => invoker(service, context); +} diff --git a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs new file mode 100644 index 000000000..eef669022 --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs @@ -0,0 +1,54 @@ +using System.Reflection; + +namespace MagicOnion.Server.Binder; + +public class MagicOnionServerStreamingMethod : IMagicOnionGrpcMethod + where TService : class + where TRawRequest : class + where TRawResponse : class +{ + + readonly Func invoker; + + public string ServiceName { get; } + public string MethodName { get; } + + public MethodInfo MethodInfo { get; } + + public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func> invoker) + { + ServiceName = serviceName; + MethodName = methodName; + MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + + this.invoker = (service, request, context) => + { + invoker(service, request, context); + return default; + }; + } + + public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func>> invoker) + { + ServiceName = serviceName; + MethodName = methodName; + MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + + this.invoker = (service, request, context) => new ValueTask(invoker(service, request, context)); + } + + public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func>> invoker) + { + ServiceName = serviceName; + MethodName = methodName; + MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + + this.invoker = async (service, request, context) => await invoker(service, request, context); + } + + public void Bind(IMagicOnionGrpcMethodBinder binder) + => binder.BindServerStreaming(this); + + public ValueTask InvokeAsync(TService service, TRequest request, ServiceContext context) + => invoker(service, request, context); +} diff --git a/src/MagicOnion.Server/Binder/MagicOnionServiceEndpointConventionBuilder.cs b/src/MagicOnion.Server/Binder/MagicOnionServiceEndpointConventionBuilder.cs new file mode 100644 index 000000000..0bde69062 --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionServiceEndpointConventionBuilder.cs @@ -0,0 +1,7 @@ +namespace Microsoft.AspNetCore.Builder; + +public class MagicOnionServiceEndpointConventionBuilder(GrpcServiceEndpointConventionBuilder inner) : IEndpointConventionBuilder +{ + public void Add(Action convention) + => inner.Add(convention); +} diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs new file mode 100644 index 000000000..6f48daa25 --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using MagicOnion.Server.Internal; + +namespace MagicOnion.Server.Binder; + +public class MagicOnionStreamingHubConnectMethod : IMagicOnionGrpcMethod where TService : class +{ + public string ServiceName { get; } + public string MethodName { get; } + + public MethodInfo MethodInfo { get; } + + public MagicOnionStreamingHubConnectMethod(string serviceName) + { + ServiceName = serviceName; + MethodName = nameof(IStreamingHubBase.Connect); + MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + } + + public void Bind(IMagicOnionGrpcMethodBinder binder) + => binder.BindStreamingHub(this); +} diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs new file mode 100644 index 000000000..266551382 --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs @@ -0,0 +1,44 @@ +using System.Reflection; + +namespace MagicOnion.Server.Binder; + +public interface IMagicOnionStreamingHubMethod +{ + string ServiceName { get; } + string MethodName { get; } + + [Obsolete] + MethodInfo Method { get; } +} + +// TODO: +public class MagicOnionStreamingHubMethod(string serviceName, string methodName, MethodInfo methodInfo) : IMagicOnionStreamingHubMethod +{ + public string ServiceName => serviceName; + public string MethodName => methodName; + public MethodInfo Method => methodInfo; + + public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func> invoker) : this(serviceName, methodName, invoker.Method) + { + } + + public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func> invoker) : this(serviceName, methodName, invoker.Method) + { + } +} + +public class MagicOnionStreamingHubMethod(string serviceName, string methodName, MethodInfo methodInfo) : IMagicOnionStreamingHubMethod +{ + public string ServiceName => serviceName; + public string MethodName => methodName; + public MethodInfo Method => methodInfo; + + public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func invoker) : this(serviceName, methodName, invoker.Method) + { + } + + public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func invoker) : this(serviceName, methodName, invoker.Method) + { + } +} + diff --git a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs new file mode 100644 index 000000000..323214e02 --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs @@ -0,0 +1,48 @@ +using System.Reflection; +using MagicOnion.Internal; + +namespace MagicOnion.Server.Binder; + +public interface IMagicOnionUnaryMethod : IMagicOnionGrpcMethod + where TService : class + where TRawRequest : class + where TRawResponse : class +{ + ValueTask InvokeAsync(TService service, TRequest request, ServiceContext context); +} + +public abstract class MagicOnionUnaryMethodBase(string serviceName, string methodName) + : IMagicOnionUnaryMethod + where TService : class + where TRawRequest : class + where TRawResponse : class +{ + public string ServiceName => serviceName; + public string MethodName => methodName; + + public MethodInfo MethodInfo { get; } = typeof(TService).GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + + public void Bind(IMagicOnionGrpcMethodBinder binder) + => binder.BindUnary(this); + + public abstract ValueTask InvokeAsync(TService service, TRequest request, ServiceContext context); +} + +public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func> invoker) + : MagicOnionUnaryMethodBase(serviceName, methodName) + where TService : class + where TRawRequest : class + where TRawResponse : class +{ + public override ValueTask InvokeAsync(TService service, TRequest request, ServiceContext context) + => MethodHandlerResultHelper.SetUnaryResult(invoker(service, request, context), context); +} + +public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func invoker) + : MagicOnionUnaryMethodBase>(serviceName, methodName) + where TService : class + where TRawRequest : class +{ + public override ValueTask InvokeAsync(TService service, TRequest request, ServiceContext context) + => MethodHandlerResultHelper.SetUnaryResultNonGeneric(invoker(service, request, context), context); +} diff --git a/src/MagicOnion.Server/Diagnostics/MagicOnionServerLog.cs b/src/MagicOnion.Server/Diagnostics/MagicOnionServerLog.cs index 03153a525..29e4d05c0 100644 --- a/src/MagicOnion.Server/Diagnostics/MagicOnionServerLog.cs +++ b/src/MagicOnion.Server/Diagnostics/MagicOnionServerLog.cs @@ -79,6 +79,15 @@ static string MethodTypeToString(MethodType type) => [LoggerMessage(EventId = 13, Level = LogLevel.Debug, EventName = nameof(SendHeartbeat), Message = nameof(SendHeartbeat) + " method:{method}")] public static partial void SendHeartbeat(ILogger logger, string method); + [LoggerMessage(EventId = 14, Level = LogLevel.Trace, EventName = nameof(AddStreamingHubMethod), Message = "Added StreamingHub method '{methodName}' to StreamingHub '{hubName}'. Method Id: {methodId}")] + public static partial void AddStreamingHubMethod(ILogger logger, string hubName, string methodName, int methodId); + + [LoggerMessage(EventId = 15, Level = LogLevel.Trace, EventName = nameof(ServiceMethodDiscovered), Message = "Discovered gRPC and StreamingHub methods for '{serviceName}' by '{methodProviderName}'")] + public static partial void ServiceMethodDiscovered(ILogger logger, string serviceName, string methodProviderName); + + [LoggerMessage(EventId = 16, Level = LogLevel.Trace, EventName = nameof(ServiceMethodNotDiscovered), Message = "Could not found gRPC and StreamingHub methods for '{serviceName}'")] + public static partial void ServiceMethodNotDiscovered(ILogger logger, string serviceName); + [LoggerMessage(EventId = 90, Level = LogLevel.Error, EventName = nameof(ErrorOnServiceMethod), Message = "A service handler throws an exception occurred in {method}")] public static partial void ErrorOnServiceMethod(ILogger logger, Exception ex, string method); diff --git a/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs b/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs index a637c7ef6..afa9907e5 100644 --- a/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs +++ b/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs @@ -1,5 +1,6 @@ using MagicOnion.Server.Binder; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Builder; @@ -7,6 +8,12 @@ public static class MagicOnionEndpointRouteBuilderExtensions { public static GrpcServiceEndpointConventionBuilder MapMagicOnionService(this IEndpointRouteBuilder builder) { + var context = new MagicOnionGrpcServiceRegistrationContext(builder); + foreach (var methodProvider in builder.ServiceProvider.GetRequiredService>()) + { + methodProvider.OnRegisterGrpcServices(context); + } + return builder.MapGrpcService(); } } diff --git a/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs index f64a5247d..755141ab0 100644 --- a/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs +++ b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs @@ -5,8 +5,10 @@ using Grpc.AspNetCore.Server.Model; using MagicOnion.Server; using MagicOnion.Server.Binder; +using MagicOnion.Server.Binder.Internal; using MagicOnion.Server.Diagnostics; using MagicOnion.Server.Hubs; +using MagicOnion.Server.Hubs.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -48,8 +50,11 @@ internal static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollecti services.AddMetrics(); // MagicOnion: Core services - services.TryAddSingleton(); - services.TryAddSingleton, MagicOnionServiceMethodProvider>(); + services.TryAddSingleton(typeof(StreamingHubRegistry<>)); + services.AddSingleton(typeof(IServiceMethodProvider<>), typeof(MagicOnionGrpcServiceMethodProvider<>)); + services.AddSingleton(); + services.TryAddSingleton(); // Legacy + services.TryAddSingleton, MagicOnionServiceMethodProvider>(); // Legacy // MagicOnion: Metrics services.TryAddSingleton(); @@ -70,6 +75,7 @@ internal static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollecti services.TryAddSingleton(); services.TryAddSingleton(); + return new MagicOnionServerBuilder(services); } } diff --git a/src/MagicOnion.Server/Hubs/HubGroupRepository.cs b/src/MagicOnion.Server/Hubs/HubGroupRepository.cs index 935d4307a..a533e9e79 100644 --- a/src/MagicOnion.Server/Hubs/HubGroupRepository.cs +++ b/src/MagicOnion.Server/Hubs/HubGroupRepository.cs @@ -21,7 +21,7 @@ internal HubGroupRepository(T remoteClient, StreamingServiceContext diff --git a/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs b/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs new file mode 100644 index 000000000..59c176a8b --- /dev/null +++ b/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs @@ -0,0 +1,121 @@ +using System.Diagnostics.CodeAnalysis; +using Cysharp.Runtime.Multicast; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; +using MagicOnion.Server.Binder; +using MagicOnion.Server.Diagnostics; +using MagicOnion.Server.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace MagicOnion.Server.Hubs.Internal; + +internal interface IStreamingHubFeature +{ + MagicOnionManagedGroupProvider GroupProvider { get; } + IStreamingHubHeartbeatManager HeartbeatManager { get; } + UniqueHashDictionary Handlers { get; } + + bool TryGetMethod(int methodId, [NotNullWhen(true)] out StreamingHubHandler? handler); +} + +internal class StreamingHubRegistry : IStreamingHubFeature +{ + readonly MagicOnionOptions options; + readonly IServiceProvider serviceProvider; + readonly MagicOnionManagedGroupProvider groupProvider; + readonly IStreamingHubHeartbeatManager heartbeatManager; + readonly ILogger logger; + + UniqueHashDictionary? methodsById; + + public MagicOnionManagedGroupProvider GroupProvider => groupProvider; + public IStreamingHubHeartbeatManager HeartbeatManager => heartbeatManager; + public UniqueHashDictionary Handlers => methodsById!; + + public StreamingHubRegistry(IOptions options, IServiceProvider serviceProvider, ILogger> logger) + { + this.options = options.Value; + this.serviceProvider = serviceProvider; + this.groupProvider = new MagicOnionManagedGroupProvider(CreateGroupProvider(serviceProvider)); + this.heartbeatManager = CreateHeartbeatManager(options.Value, typeof(TService), serviceProvider); + this.logger = logger; + } + + public void RegisterMethods(IEnumerable methods) + { + var streamingHubHandlerOptions = new StreamingHubHandlerOptions(options); + var methodAndIdPairs = methods + .Select(x => new StreamingHubHandler(x.Method.DeclaringType!, x.Method, streamingHubHandlerOptions, serviceProvider)) + .Select(x => (x.MethodId, x)) + .ToArray(); + + methodsById = new UniqueHashDictionary(methodAndIdPairs); + + foreach (var (_, method) in methodAndIdPairs) + { + MagicOnionServerLog.AddStreamingHubMethod(logger, method.HubName, method.MethodInfo.Name, method.MethodId); + } + } + + public bool TryGetMethod(int methodId, [NotNullWhen(true)] out StreamingHubHandler? handler) + { + return methodsById!.TryGetValue(methodId, out handler); + } + + static IMulticastGroupProvider CreateGroupProvider(IServiceProvider serviceProvider) + { + // Group Provider + var attr = typeof(TService).GetCustomAttribute(true); + if (attr != null) + { + return (IMulticastGroupProvider)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, attr.FactoryType); + } + else + { + return serviceProvider.GetRequiredService(); + } + } + + static IStreamingHubHeartbeatManager CreateHeartbeatManager(MagicOnionOptions options, Type classType, IServiceProvider serviceProvider) + { + var heartbeatEnable = options.EnableStreamingHubHeartbeat; + var heartbeatInterval = options.StreamingHubHeartbeatInterval; + var heartbeatTimeout = options.StreamingHubHeartbeatTimeout; + var heartbeatMetadataProvider = default(IStreamingHubHeartbeatMetadataProvider); + if (classType.GetCustomAttribute(inherit: true) is { } heartbeatAttr) + { + heartbeatEnable = heartbeatAttr.Enable; + if (heartbeatAttr.Timeout != 0) + { + heartbeatTimeout = TimeSpan.FromMilliseconds(heartbeatAttr.Timeout); + } + if (heartbeatAttr.Interval != 0) + { + heartbeatInterval = TimeSpan.FromMilliseconds(heartbeatAttr.Interval); + } + if (heartbeatAttr.MetadataProvider != null) + { + heartbeatMetadataProvider = (IStreamingHubHeartbeatMetadataProvider)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, heartbeatAttr.MetadataProvider); + } + } + + IStreamingHubHeartbeatManager heartbeatManager; + if (!heartbeatEnable || heartbeatInterval is null) + { + heartbeatManager = NopStreamingHubHeartbeatManager.Instance; + } + else + { + heartbeatManager = new StreamingHubHeartbeatManager( + heartbeatInterval.Value, + heartbeatTimeout ?? Timeout.InfiniteTimeSpan, + heartbeatMetadataProvider ?? serviceProvider.GetService(), + options.TimeProvider ?? TimeProvider.System, + serviceProvider.GetRequiredService>() + ); + } + + return heartbeatManager; + } +} diff --git a/src/MagicOnion.Server/Hubs/StreamingHub.cs b/src/MagicOnion.Server/Hubs/StreamingHub.cs index ef94212ea..92a5c7044 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHub.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHub.cs @@ -7,6 +7,7 @@ using MagicOnion.Internal.Buffers; using MagicOnion.Server.Diagnostics; using MagicOnion.Server.Features; +using MagicOnion.Server.Hubs.Internal; using MagicOnion.Server.Internal; using MessagePack; using Microsoft.AspNetCore.Connections; @@ -16,12 +17,14 @@ namespace MagicOnion.Server.Hubs; -public abstract class StreamingHubBase : ServiceBase, IStreamingHub +public abstract class StreamingHubBase : ServiceBase, IStreamingHub, IStreamingHubBase where THubInterface : IStreamingHub { + IStreamingHubFeature streamingHubFeature = default!; IRemoteClientResultPendingTaskRegistry remoteClientResultPendingTasks = default!; StreamingHubHeartbeatHandle heartbeatHandle = default!; TimeProvider timeProvider = default!; + bool isReturnExceptionStackTraceInErrorDetail = false; protected static readonly Task NilTask = Task.FromResult(Nil.Default); protected static readonly ValueTask CompletedTask = new ValueTask(); @@ -78,28 +81,31 @@ protected virtual ValueTask OnDisconnected() return CompletedTask; } + Task> IStreamingHubBase.Connect() + => Connect(); + + [Obsolete] internal async Task> Connect() { - Metrics.StreamingHubConnectionIncrement(Context.Metrics, Context.MethodHandler.ServiceName); + Metrics.StreamingHubConnectionIncrement(Context.Metrics, Context.ServiceName); var streamingContext = GetDuplexStreamingContext(); var serviceProvider = streamingContext.ServiceContext.ServiceProvider; var features = this.Context.CallContext.GetHttpContext().Features; + streamingHubFeature = features.Get()!; // TODO: GetRequiredFeature var magicOnionOptions = serviceProvider.GetRequiredService>().Value; timeProvider = magicOnionOptions.TimeProvider ?? TimeProvider.System; + isReturnExceptionStackTraceInErrorDetail = magicOnionOptions.IsReturnExceptionStackTraceInErrorDetail; var remoteProxyFactory = serviceProvider.GetRequiredService(); var remoteSerializer = serviceProvider.GetRequiredService(); this.remoteClientResultPendingTasks = new RemoteClientResultPendingTaskRegistry(magicOnionOptions.ClientResultsDefaultTimeout, timeProvider); this.Client = remoteProxyFactory.CreateDirect(new MagicOnionRemoteReceiverWriter(StreamingServiceContext), remoteSerializer, remoteClientResultPendingTasks); - var handlerRepository = serviceProvider.GetRequiredService(); - var groupProvider = handlerRepository.GetGroupProvider(Context.MethodHandler); - this.Group = new HubGroupRepository(Client, StreamingServiceContext, groupProvider); + this.Group = new HubGroupRepository(Client, StreamingServiceContext, streamingHubFeature.GroupProvider); - var heartbeatManager = handlerRepository.GetHeartbeatManager(Context.MethodHandler); - heartbeatHandle = heartbeatManager.Register(StreamingServiceContext); + heartbeatHandle = streamingHubFeature.HeartbeatManager.Register(StreamingServiceContext); features.Set(new MagicOnionHeartbeatFeature(heartbeatHandle)); try @@ -132,7 +138,7 @@ internal async Task().GetHandlers(Context.MethodHandler); + var handlers = streamingHubFeature.Handlers; // Starts a loop that consumes the request queue. var consumeRequestsTask = ConsumeRequestQueueAsync(ct); @@ -275,7 +281,7 @@ async ValueTask ProcessRequestAsync(UniqueHashDictionary ha var methodStartingTimestamp = timeProvider.GetTimestamp(); var isErrorOrInterrupted = false; - MagicOnionServerLog.BeginInvokeHubMethod(Context.MethodHandler.Logger, context, context.Request, handler.RequestType); + MagicOnionServerLog.BeginInvokeHubMethod(Context.Logger, context, context.Request, handler.RequestType); try { await handler.MethodBody.Invoke(context); @@ -290,18 +296,18 @@ async ValueTask ProcessRequestAsync(UniqueHashDictionary ha catch (Exception ex) { isErrorOrInterrupted = true; - MagicOnionServerLog.Error(Context.MethodHandler.Logger, ex, context); + MagicOnionServerLog.Error(Context.Logger, ex, context); Metrics.StreamingHubException(Context.Metrics, handler, ex); if (hasResponse) { - await context.WriteErrorMessage((int)StatusCode.Internal, $"An error occurred while processing handler '{handler.ToString()}'.", ex, Context.MethodHandler.IsReturnExceptionStackTraceInErrorDetail); + await context.WriteErrorMessage((int)StatusCode.Internal, $"An error occurred while processing handler '{handler.ToString()}'.", ex, isReturnExceptionStackTraceInErrorDetail); } } finally { var methodEndingTimestamp = timeProvider.GetTimestamp(); - MagicOnionServerLog.EndInvokeHubMethod(Context.MethodHandler.Logger, context, context.ResponseSize, context.ResponseType, timeProvider.GetElapsedTime(methodStartingTimestamp, methodEndingTimestamp).TotalMilliseconds, isErrorOrInterrupted); + MagicOnionServerLog.EndInvokeHubMethod(Context.Logger, context, context.ResponseSize, context.ResponseType, timeProvider.GetElapsedTime(methodStartingTimestamp, methodEndingTimestamp).TotalMilliseconds, isErrorOrInterrupted); Metrics.StreamingHubMethodCompleted(Context.Metrics, handler, methodStartingTimestamp, methodEndingTimestamp, isErrorOrInterrupted); StreamingHubContextPool.Shared.Return(context); diff --git a/src/MagicOnion.Server/Internal/IServiceBase.cs b/src/MagicOnion.Server/Internal/IServiceBase.cs new file mode 100644 index 000000000..5a04d5b8b --- /dev/null +++ b/src/MagicOnion.Server/Internal/IServiceBase.cs @@ -0,0 +1,9 @@ +using MagicOnion.Server.Diagnostics; + +namespace MagicOnion.Server.Internal; + +internal interface IServiceBase +{ + ServiceContext Context { get; set; } + MagicOnionMetrics Metrics { get; set; } +} diff --git a/src/MagicOnion.Server/Internal/IStreamingHubBase.cs b/src/MagicOnion.Server/Internal/IStreamingHubBase.cs new file mode 100644 index 000000000..bd896eab4 --- /dev/null +++ b/src/MagicOnion.Server/Internal/IStreamingHubBase.cs @@ -0,0 +1,8 @@ +using MagicOnion.Internal; + +namespace MagicOnion.Server.Internal; + +internal interface IStreamingHubBase +{ + Task> Connect(); +} diff --git a/src/MagicOnion.Server/MagicOnion.Server.csproj b/src/MagicOnion.Server/MagicOnion.Server.csproj index 69343c4bb..82aa4cda4 100644 --- a/src/MagicOnion.Server/MagicOnion.Server.csproj +++ b/src/MagicOnion.Server/MagicOnion.Server.csproj @@ -1,4 +1,4 @@ - + net6.0;net8.0 diff --git a/src/MagicOnion.Server/MagicOnionEngine.cs b/src/MagicOnion.Server/MagicOnionEngine.cs index 75255ff5f..710af45f1 100644 --- a/src/MagicOnion.Server/MagicOnionEngine.cs +++ b/src/MagicOnion.Server/MagicOnionEngine.cs @@ -189,6 +189,7 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP } else { + continue; // create handler var handler = new MethodHandler(classType, methodInfo, methodName, methodHandlerOptions, serviceProvider, loggerMethodHandler, isStreamingHub: false); if (!handlers.Add(handler)) @@ -198,6 +199,7 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP } } + continue; if (isStreamingHub) { var connectHandler = new MethodHandler(classType, classType.GetMethod("Connect", BindingFlags.NonPublic | BindingFlags.Instance)!, "Connect", methodHandlerOptions, serviceProvider, loggerMethodHandler, isStreamingHub: true); @@ -233,7 +235,7 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP streamingHubHandlerRepository.Freeze(); - var result = new MagicOnionServiceDefinition(handlers.ToArray(), streamingHubHandlers.ToArray()); + var result = new MagicOnionServiceDefinition(handlers.ToArray(), streamingHubHandlers.ToArray(), targetTypes.ToArray()); sw.Stop(); MagicOnionServerLog.EndBuildServiceDefinition(loggerMagicOnionEngine, sw.Elapsed.TotalMilliseconds); diff --git a/src/MagicOnion.Server/MagicOnionServiceDefinition.cs b/src/MagicOnion.Server/MagicOnionServiceDefinition.cs index f3d9bc81e..8a4fa1ef7 100644 --- a/src/MagicOnion.Server/MagicOnionServiceDefinition.cs +++ b/src/MagicOnion.Server/MagicOnionServiceDefinition.cs @@ -6,10 +6,12 @@ public class MagicOnionServiceDefinition { public IReadOnlyList MethodHandlers { get; } public IReadOnlyList StreamingHubHandlers { get; } + public IReadOnlyList TargetTypes { get; } - public MagicOnionServiceDefinition(IReadOnlyList handlers, IReadOnlyList streamingHubHandlers) + public MagicOnionServiceDefinition(IReadOnlyList handlers, IReadOnlyList streamingHubHandlers, IReadOnlyList targetTypes) { this.MethodHandlers = handlers; this.StreamingHubHandlers = streamingHubHandlers; + this.TargetTypes = targetTypes; } } diff --git a/src/MagicOnion.Server/MethodHandler.cs b/src/MagicOnion.Server/MethodHandler.cs index 0dc458df3..05b7c77eb 100644 --- a/src/MagicOnion.Server/MethodHandler.cs +++ b/src/MagicOnion.Server/MethodHandler.cs @@ -230,7 +230,7 @@ void BindHandlerTyped(IMagicOnio async ValueTask UnaryServerMethod(TRequest request, ServerCallContext context) { var isErrorOrInterrupted = false; - var serviceContext = new ServiceContext(ServiceType, MethodInfo, AttributeLookup, this.MethodType, context, messageSerializer, Logger, this, context.GetHttpContext().RequestServices); + var serviceContext = new ServiceContext(null!, ServiceType, ServiceName, MethodInfo, AttributeLookup, this.MethodType, context, messageSerializer, Logger, this, context.GetHttpContext().RequestServices); serviceContext.SetRawRequest(request); object? response = default(TResponse?); @@ -312,7 +312,9 @@ void BindHandlerTyped(IMagicOnio { var isErrorOrInterrupted = false; var serviceContext = new StreamingServiceContext( + default!, ServiceType, + ServiceName, MethodInfo, AttributeLookup, this.MethodType, @@ -371,7 +373,9 @@ async ValueTask ServerStreamingServerMethod(TRequest reques { var isErrorOrInterrupted = false; var serviceContext = new StreamingServiceContext( + default!, ServiceType, + ServiceName, MethodInfo, AttributeLookup, this.MethodType, @@ -424,7 +428,9 @@ async ValueTask DuplexStreamingServerMethod(IAsyncStreamRea { var isErrorOrInterrupted = false; var serviceContext = new StreamingServiceContext( + default!, ServiceType, + ServiceName, MethodInfo, AttributeLookup, this.MethodType, @@ -531,38 +537,60 @@ public MethodHandlerOptions(MagicOnionOptions options) internal class MethodHandlerResultHelper { - static readonly ValueTask CopmletedValueTask = new ValueTask(); static readonly object BoxedNil = Nil.Default; public static ValueTask NewEmptyValueTask(T result) - { - // ignore result. - return CopmletedValueTask; - } + => default; - public static async ValueTask TaskToEmptyValueTask(Task result) - { - // wait and ignore result. - await result; - } + public static ValueTask TaskToEmptyValueTask(Task result) + => new(result); - public static async ValueTask SetUnaryResultNonGeneric(UnaryResult result, ServiceContext context) + public static ValueTask SetUnaryResultNonGeneric(UnaryResult result, ServiceContext context) { if (result.hasRawValue) { - if (result.rawTaskValue != null) + if (result.rawTaskValue is { IsCompletedSuccessfully: true }) { - await result.rawTaskValue.ConfigureAwait(false); + return Await(result.rawTaskValue, context); } context.Result = BoxedNil; } + + return default; + + static async ValueTask Await(Task task, ServiceContext context) + { + await task.ConfigureAwait(false); + context.Result = BoxedNil; + } } - public static async ValueTask SetUnaryResult(UnaryResult result, ServiceContext context) + public static ValueTask SetUnaryResult(UnaryResult result, ServiceContext context) { if (result.hasRawValue) { - context.Result = (result.rawTaskValue != null) ? await result.rawTaskValue.ConfigureAwait(false) : result.rawValue; + if (result.rawTaskValue is { } task) + { + if (task.IsCompletedSuccessfully) + { + context.Result = task.Result; + } + else + { + return Await(task, context); + } + } + else + { + context.Result = result.rawValue; + } + } + + return default; + + static async ValueTask Await(Task task, ServiceContext context) + { + context.Result = await task.ConfigureAwait(false); } } @@ -576,21 +604,31 @@ public static async ValueTask SetTaskUnaryResult(Task> taskRes } public static ValueTask SerializeClientStreamingResult(ClientStreamingResult result, ServiceContext context) + => SerializeValueTaskClientStreamingResult(new ValueTask>(result), context); + + public static ValueTask SerializeTaskClientStreamingResult(Task> taskResult, ServiceContext context) + => SerializeValueTaskClientStreamingResult(new ValueTask>(taskResult), context); + + public static ValueTask SerializeValueTaskClientStreamingResult(ValueTask> taskResult, ServiceContext context) { - if (result.hasRawValue) + if (taskResult.IsCompletedSuccessfully) { - context.Result = result.rawValue; + if (taskResult.Result.hasRawValue) + { + context.Result = taskResult.Result.rawValue; + return default; + } } - return default(ValueTask); - } + return Await(taskResult, context); - public static async ValueTask SerializeTaskClientStreamingResult(Task> taskResult, ServiceContext context) - { - var result = await taskResult.ConfigureAwait(false); - if (result.hasRawValue) + static async ValueTask Await(ValueTask> taskResult, ServiceContext context) { - context.Result = result.rawValue; + var result = await taskResult.ConfigureAwait(false); + if (result.hasRawValue) + { + context.Result = result.rawValue; + } } } } diff --git a/src/MagicOnion.Server/Service.cs b/src/MagicOnion.Server/Service.cs index f16bde103..89a0a11c1 100644 --- a/src/MagicOnion.Server/Service.cs +++ b/src/MagicOnion.Server/Service.cs @@ -1,10 +1,11 @@ using Grpc.Core; using MagicOnion.Server.Diagnostics; +using MagicOnion.Server.Internal; using MessagePack; namespace MagicOnion.Server; -public abstract class ServiceBase : IService +public abstract class ServiceBase : IService, IServiceBase where TServiceInterface : IServiceMarker { // NOTE: Properties `Context` and `Metrics` are set by an internal setter during instance activation of the service. @@ -12,6 +13,17 @@ public abstract class ServiceBase : IService this.Context; + set => this.Context = value; + } + MagicOnionMetrics IServiceBase.Metrics + { + get => this.Metrics; + set => this.Metrics = value; + } + public ServiceBase() { this.Context = default!; diff --git a/src/MagicOnion.Server/ServiceContext.Streaming.cs b/src/MagicOnion.Server/ServiceContext.Streaming.cs index facce8626..714e5527f 100644 --- a/src/MagicOnion.Server/ServiceContext.Streaming.cs +++ b/src/MagicOnion.Server/ServiceContext.Streaming.cs @@ -39,7 +39,9 @@ internal class StreamingServiceContext : ServiceContext, IS public bool IsDisconnected { get; private set; } public StreamingServiceContext( + object instance, Type serviceType, + string serviceName, MethodInfo methodInfo, ILookup attributeLookup, MethodType methodType, @@ -50,7 +52,7 @@ public StreamingServiceContext( IServiceProvider serviceProvider, IAsyncStreamReader? requestStream, IServerStreamWriter? responseStream - ) : base(serviceType, methodInfo, attributeLookup, methodType, context, messageSerializer, logger, methodHandler, serviceProvider) + ) : base(instance, serviceType, serviceName, methodInfo, attributeLookup, methodType, context, messageSerializer, logger, methodHandler, serviceProvider) { RequestStream = requestStream; ResponseStream = responseStream; diff --git a/src/MagicOnion.Server/ServiceContext.cs b/src/MagicOnion.Server/ServiceContext.cs index cc2f231d2..afdeb6d34 100644 --- a/src/MagicOnion.Server/ServiceContext.cs +++ b/src/MagicOnion.Server/ServiceContext.cs @@ -65,6 +65,9 @@ public ConcurrentDictionary Items public Type ServiceType { get; } + public string ServiceName { get; } + public string MethodName => MethodInfo.Name; + public MethodInfo MethodInfo { get; } /// Cached Attributes both service and method. @@ -79,6 +82,7 @@ public ConcurrentDictionary Items public IServiceProvider ServiceProvider { get; } + internal object Instance { get; } internal object? Request => request; internal object? Result { get; set; } internal ILogger Logger { get; } @@ -86,7 +90,9 @@ public ConcurrentDictionary Items internal MetricsContext Metrics { get; } public ServiceContext( + object instance, Type serviceType, + string serviceName, MethodInfo methodInfo, ILookup attributeLookup, MethodType methodType, @@ -98,7 +104,9 @@ IServiceProvider serviceProvider ) { this.ContextId = Guid.NewGuid(); + this.Instance = instance; this.ServiceType = serviceType; + this.ServiceName = serviceName; this.MethodInfo = methodInfo; this.AttributeLookup = attributeLookup; this.MethodType = methodType; diff --git a/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs b/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs new file mode 100644 index 000000000..66b86ce3c --- /dev/null +++ b/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs @@ -0,0 +1,179 @@ +using Grpc.Net.Client; +using MagicOnion.Client; +using MagicOnion.Internal; +using MagicOnion.Server.Binder; +using MagicOnion.Server.Hubs; + +namespace MagicOnion.Server.Tests; + +public class HandCraftedMagicOnionMethodProviderTest(HandCraftedMagicOnionMethodProviderTest.ApplicationFactory factory) : IClassFixture +{ + public class ApplicationFactory : MagicOnionApplicationFactory + { + protected override IEnumerable GetServiceImplementationTypes() + { + yield return typeof(HandCraftedMagicOnionMethodProviderTest_GreeterService); + yield return typeof(HandCraftedMagicOnionMethodProviderTest_GreeterService2); + yield return typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub); + } + } + + [Fact] + public async Task Services() + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var client1 = MagicOnionClient.Create(GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient })); + var client2 = MagicOnionClient.Create(GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient })); + + // Act + var result1 = await client1.HelloAsync("Alice", 18); + var result2 = await client2.GoodByeAsync("Alice", 18); + + // Assert + Assert.Equal("Hello Alice (18) !", result1); + Assert.Equal("Goodbye Alice (18) !", result2); + } + + [Fact] + public async Task Unary_Parameter_Many_Return_RefType() + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var client = MagicOnionClient.Create(GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient })); + + // Act + var result = await client.HelloAsync("Alice", 18); + + // Assert + Assert.Equal("Hello Alice (18) !", result); + } + + [Fact] + public async Task Unary_Parameter_Zero_NoReturnValue() + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var client = MagicOnionClient.Create(GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient })); + + // Act & Assert + await client.PingAsync(); + } +} + + +public interface IHandCraftedMagicOnionMethodProviderTest_GreeterService : IService +{ + UnaryResult HelloAsync(string name, int age); + UnaryResult PingAsync(); +} + +public interface IHandCraftedMagicOnionMethodProviderTest_GreeterService2 : IService +{ + UnaryResult GoodByeAsync(string name, int age); + UnaryResult PingAsync(); +} + +class HandCraftedMagicOnionMethodProviderTest_GreeterService : ServiceBase, IHandCraftedMagicOnionMethodProviderTest_GreeterService +{ + [HandCraftedMagicOnionMethodProviderTest_MyFilter] + public UnaryResult HelloAsync(string name, int age) => UnaryResult.FromResult($"Hello {name} ({age}) !"); + public UnaryResult PingAsync() => default; +} + +class HandCraftedMagicOnionMethodProviderTest_GreeterService2 : ServiceBase, IHandCraftedMagicOnionMethodProviderTest_GreeterService2 +{ + [HandCraftedMagicOnionMethodProviderTest_MyFilter] + public UnaryResult GoodByeAsync(string name, int age) => UnaryResult.FromResult($"Goodbye {name} ({age}) !"); + public UnaryResult PingAsync() => default; +} + +public interface IHandCraftedMagicOnionMethodProviderTest_GreeterHub : IStreamingHub +{ + Task JoinAsync(string name, string channel); + ValueTask SendMessageAsync(string message); + ValueTask> GetMembersAsync(); +} + +public interface IHandCraftedMagicOnionMethodProviderTest_GreeterHubReceiver +{ + void OnMessage(string message); +} + +class HandCraftedMagicOnionMethodProviderTest_GreeterHub : StreamingHubBase, IHandCraftedMagicOnionMethodProviderTest_GreeterHub +{ + public Task JoinAsync(string name, string channel) + { + throw new NotImplementedException(); + } + + public ValueTask SendMessageAsync(string message) + { + throw new NotImplementedException(); + } + + public ValueTask> GetMembersAsync() + { + throw new NotImplementedException(); + } +} + +class HandCraftedMagicOnionMethodProviderTest_MyFilter : MagicOnionFilterAttribute +{ + public override ValueTask Invoke(ServiceContext context, Func next) + { + return next(context); + } +} + +internal class HandCraftedMagicOnionMethodProviderTest_GeneratedMagicOnionMethodProvider : IMagicOnionGrpcMethodProvider +{ + public void OnRegisterGrpcServices(MagicOnionGrpcServiceRegistrationContext context) + { + context.Register(); + context.Register(); + context.Register(); + } + + public IEnumerable GetGrpcMethods() where TService : class + { + if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterService)) + { + yield return new MagicOnionUnaryMethod, string, Box>, string>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService.HelloAsync), static (instance, request, context) => instance.HelloAsync(request.Item1, request.Item2)); + yield return new MagicOnionUnaryMethod>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService.PingAsync), static (instance, request, context) => instance.PingAsync()); + } + if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterService2)) + { + yield return new MagicOnionUnaryMethod, string, Box>, string>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2.GoodByeAsync), static (instance, request, context) => instance.GoodByeAsync(request.Item1, request.Item2)); + yield return new MagicOnionUnaryMethod>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2.PingAsync), static (instance, request, context) => instance.PingAsync()); + } + + if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub)) + { + yield return new MagicOnionStreamingHubConnectMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub)); + } + } + + public IEnumerable GetStreamingHubMethods() where TService : class + { + if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub)) + { + //yield return new MagicOnionStreamingHubMethod>( + // nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync), static (instance, request) => instance.JoinAsync(request.Item1, request.Item2)); + //yield return new MagicOnionStreamingHubMethod( + // nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.SendMessageAsync), static (instance, request) => instance.SendMessageAsync(request)); + //yield return new MagicOnionStreamingHubMethod>( + // nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync), static (instance, request) => instance.GetMembersAsync()); + yield return new MagicOnionStreamingHubMethod>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync), typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub).GetMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync))!); + yield return new MagicOnionStreamingHubMethod( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.SendMessageAsync), typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub).GetMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.SendMessageAsync))!); + yield return new MagicOnionStreamingHubMethod>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync), typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub).GetMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync))!); + } + } +} diff --git a/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs index bdfda7046..f9d8f2c39 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs @@ -7,7 +7,15 @@ namespace MagicOnion.Server.Tests; #pragma warning disable CS1998 -public class MagicOnionApplicationFactory : WebApplicationFactory +public class MagicOnionApplicationFactory : MagicOnionApplicationFactory +{ + protected override IEnumerable GetServiceImplementationTypes() + { + yield return typeof(TServiceImplementation); + } +} + +public abstract class MagicOnionApplicationFactory : WebApplicationFactory { public const string ItemsKey = "MagicOnionApplicationFactory.Items"; public ConcurrentDictionary Items => Services.GetRequiredKeyedService>(ItemsKey); @@ -26,10 +34,12 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder.ConfigureServices(services => { services.AddKeyedSingleton>(ItemsKey); - services.AddMagicOnion(new[] { typeof(TServiceImplementation) }); + services.AddMagicOnion(GetServiceImplementationTypes()); }); } + protected abstract IEnumerable GetServiceImplementationTypes(); + public WebApplicationFactory WithMagicOnionOptions(Action configure) { return this.WithWebHostBuilder(x => From f9451207e470b92bf82adcda433b2ea141c73173 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Fri, 11 Oct 2024 14:41:58 +0900 Subject: [PATCH 02/27] wip --- .../DynamicMagicOnionMethodProvider.cs | 68 ++++++-- .../Internal/MagicOnionGrpcMethodBinder.cs | 2 +- .../MagicOnionGrpcServiceMethodProvider.cs | 2 +- .../Legacy/MagicOnionServiceMethodProvider.cs | 1 + .../Binder/MagicOnionStreamingHubMethod.cs | 106 ++++++++++-- .../Binder/MagicOnionUnaryMethod.cs | 16 +- .../Hubs/Internal/StreamingHubRegistry.cs | 2 +- .../Hubs/StreamingHubHandler.cs | 157 +----------------- .../Hubs/StreamingHubHandlerRepository.cs | 1 + src/MagicOnion.Server/MagicOnionEngine.cs | 153 ----------------- src/MagicOnion.Server/MethodHandler.cs | 1 + .../FakeStreamingServiceContext.cs | 1 - ...HandCraftedMagicOnionMethodProviderTest.cs | 27 +-- .../StreamingHubHandlerTest.cs | 97 ++++++++--- 14 files changed, 249 insertions(+), 385 deletions(-) diff --git a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs index 388785b2c..c494042aa 100644 --- a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs @@ -1,6 +1,7 @@ using System.Linq.Expressions; using System.Reflection; using MagicOnion.Internal; +using MagicOnion.Server.Hubs; namespace MagicOnion.Server.Binder.Internal; @@ -48,11 +49,7 @@ public IEnumerable GetGrpcMethods() where TServ var targetMethod = methodInfo; var methodParameters = targetMethod.GetParameters(); - var typeRequest = methodParameters is { Length: > 1 } - ? typeof(DynamicArgumentTuple<,>).MakeGenericType(methodParameters[0].ParameterType, methodParameters[1].ParameterType) - : methodParameters is { Length: 1 } - ? methodParameters[0].ParameterType - : typeof(MessagePack.Nil); + var typeRequest = CreateRequestType(methodParameters); var typeRawRequest = typeRequest.IsValueType ? typeof(Box<>).MakeGenericType(typeRequest) : typeRequest; @@ -73,9 +70,10 @@ public IEnumerable GetGrpcMethods() where TServ typeMethod = typeof(MagicOnionUnaryMethod<,,,,>).MakeGenericType(typeServiceImplementation, typeRequest, typeResponse, typeRawRequest, typeRawResponse); } + // (instance, context, request) => instance.Foo(request.Item1, request.Item2...); var exprParamInstance = Expression.Parameter(typeServiceImplementation); - var exprParamRequest = Expression.Parameter(typeRequest); var exprParamServiceContext = Expression.Parameter(typeof(ServiceContext)); + var exprParamRequest = Expression.Parameter(typeRequest); var exprArguments = methodParameters.Length == 1 ? [exprParamRequest] : methodParameters @@ -84,7 +82,7 @@ public IEnumerable GetGrpcMethods() where TServ .ToArray(); var exprCall = Expression.Call(exprParamInstance, targetMethod, exprArguments); - var invoker = Expression.Lambda(exprCall, [exprParamInstance, exprParamRequest, exprParamServiceContext]).Compile(); + var invoker = Expression.Lambda(exprCall, [exprParamInstance, exprParamServiceContext, exprParamRequest]).Compile(); var serviceMethod = Activator.CreateInstance(typeMethod, [typeServiceInterface.Name, targetMethod.Name, invoker])!; yield return (IMagicOnionGrpcMethod)serviceMethod; @@ -114,11 +112,7 @@ public IEnumerable GetStreamingHubMethods 1 } - ? typeof(DynamicArgumentTuple<,>).MakeGenericType(methodParameters[0].ParameterType, methodParameters[1].ParameterType) - : methodParameters is { Length: 1 } - ? methodParameters[0].ParameterType - : typeof(MessagePack.Nil); + var typeRequest = CreateRequestType(methodParameters); var typeResponse = methodInfo.ReturnType; Type hubMethodType; @@ -126,12 +120,58 @@ public IEnumerable GetStreamingHubMethods).MakeGenericType([typeServiceImplementation, typeRequest]); } + else if (typeResponse.IsConstructedGenericType && typeResponse.GetGenericTypeDefinition() is {} typeResponseOpen && (typeResponseOpen == typeof(Task<>) || typeResponseOpen == typeof(ValueTask<>))) + { + hubMethodType = typeof(MagicOnionStreamingHubMethod<,,>).MakeGenericType([typeServiceImplementation, typeRequest, typeResponse.GetGenericArguments()[0]]); + } else { - hubMethodType = typeof(MagicOnionStreamingHubMethod<,,>).MakeGenericType([typeServiceImplementation, typeRequest, typeResponse]); + throw new InvalidOperationException("Unsupported method return type. The return type of StreamingHub method must be one of 'void', 'Task', 'ValueTask', 'Task' or 'ValueTask'."); } - yield return (IMagicOnionStreamingHubMethod)Activator.CreateInstance(hubMethodType, [typeServiceInterface.Name, methodInfo.Name, methodInfo])!; + // Invoker + // var invokeHubMethodFunc = (service, context, request) => service.Foo(request); + // or + // var invokeHubMethodFunc = (service, context, request) => service.Foo(request.Item1, request.Item2 ...); + var exprParamService = Expression.Parameter(typeof(TService), "service"); + var exprParamContext = Expression.Parameter(typeof(StreamingHubContext), "context"); + var exprParamRequest = Expression.Parameter(typeRequest, "request"); + var exprArguments = methodParameters.Length == 1 + ? [exprParamRequest] + : methodParameters + .Select((x, i) => Expression.Field(exprParamRequest, "Item" + (i + 1))) + .Cast() + .ToArray(); + + var exprCallHubMethod = Expression.Call(exprParamService, methodInfo, exprArguments); + var invoker = Expression.Lambda(exprCallHubMethod, [exprParamService, exprParamContext, exprParamRequest]).Compile(); + + var hubMethod = (IMagicOnionStreamingHubMethod)Activator.CreateInstance(hubMethodType, [typeServiceInterface.Name, methodInfo.Name, invoker])!; + yield return hubMethod; } } + + static Type CreateRequestType(ParameterInfo[] parameters) + { + return parameters.Length switch + { + 0 => typeof(MessagePack.Nil), + 1 => parameters[0].ParameterType, + 2 => typeof(DynamicArgumentTuple<,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType), + 3 => typeof(DynamicArgumentTuple<,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType), + 4 => typeof(DynamicArgumentTuple<,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType), + 5 => typeof(DynamicArgumentTuple<,,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType, parameters[4].ParameterType), + 6 => typeof(DynamicArgumentTuple<,,,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType, parameters[4].ParameterType, parameters[5].ParameterType), + 7 => typeof(DynamicArgumentTuple<,,,,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType, parameters[4].ParameterType, parameters[5].ParameterType, parameters[6].ParameterType), + 8 => typeof(DynamicArgumentTuple<,,,,,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType, parameters[4].ParameterType, parameters[5].ParameterType, parameters[6].ParameterType, parameters[7].ParameterType), + 9 => typeof(DynamicArgumentTuple<,,,,,,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType, parameters[4].ParameterType, parameters[5].ParameterType, parameters[6].ParameterType, parameters[7].ParameterType, parameters[8].ParameterType), + 10 => typeof(DynamicArgumentTuple<,,,,,,,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType, parameters[4].ParameterType, parameters[5].ParameterType, parameters[6].ParameterType, parameters[7].ParameterType, parameters[8].ParameterType, parameters[9].ParameterType), + 11 => typeof(DynamicArgumentTuple<,,,,,,,,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType, parameters[4].ParameterType, parameters[5].ParameterType, parameters[6].ParameterType, parameters[7].ParameterType, parameters[8].ParameterType, parameters[9].ParameterType, parameters[10].ParameterType), + 12 => typeof(DynamicArgumentTuple<,,,,,,,,,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType, parameters[4].ParameterType, parameters[5].ParameterType, parameters[6].ParameterType, parameters[7].ParameterType, parameters[8].ParameterType, parameters[9].ParameterType, parameters[10].ParameterType, parameters[11].ParameterType), + 13 => typeof(DynamicArgumentTuple<,,,,,,,,,,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType, parameters[4].ParameterType, parameters[5].ParameterType, parameters[6].ParameterType, parameters[7].ParameterType, parameters[8].ParameterType, parameters[9].ParameterType, parameters[10].ParameterType, parameters[11].ParameterType, parameters[12].ParameterType), + 14 => typeof(DynamicArgumentTuple<,,,,,,,,,,,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType, parameters[4].ParameterType, parameters[5].ParameterType, parameters[6].ParameterType, parameters[7].ParameterType, parameters[8].ParameterType, parameters[9].ParameterType, parameters[10].ParameterType, parameters[11].ParameterType, parameters[12].ParameterType, parameters[13].ParameterType), + 15 => typeof(DynamicArgumentTuple<,,,,,,,,,,,,,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, parameters[2].ParameterType, parameters[3].ParameterType, parameters[4].ParameterType, parameters[5].ParameterType, parameters[6].ParameterType, parameters[7].ParameterType, parameters[8].ParameterType, parameters[9].ParameterType, parameters[10].ParameterType, parameters[11].ParameterType, parameters[12].ParameterType, parameters[13].ParameterType, parameters[14].ParameterType), + _ => throw new NotSupportedException("The method must have no more than 16 parameters."), + }; + } } diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs index c59ae9d6b..9d858fb6f 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs @@ -370,7 +370,7 @@ IList metadata { var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); - var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, (TRequest)serviceContext.Request!, serviceContext)); + var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext, (TRequest)serviceContext.Request!)); return InvokeAsync; diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs index f82fa06b5..12b93aa72 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs @@ -16,7 +16,7 @@ internal class MagicOnionGrpcServiceMethodProvider : IServiceMethodPro readonly ILoggerFactory loggerFactory; readonly ILogger logger; - public MagicOnionGrpcServiceMethodProvider(IEnumerable methodProviders, IOptions options, IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ILogger> logger) + public MagicOnionGrpcServiceMethodProvider(IEnumerable methodProviders, IOptions options, IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ILogger> logger) { this.methodProviders = methodProviders.ToArray(); this.options = options.Value; diff --git a/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceMethodProvider.cs b/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceMethodProvider.cs index 67387d518..48f51e7c5 100644 --- a/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceMethodProvider.cs @@ -2,6 +2,7 @@ namespace MagicOnion.Server.Binder; +[Obsolete] internal class MagicOnionServiceMethodProvider : IServiceMethodProvider where TService : class { diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs index 266551382..a0da04a11 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs @@ -1,4 +1,7 @@ +using System.Buffers; +using System.Diagnostics; using System.Reflection; +using MagicOnion.Server.Hubs; namespace MagicOnion.Server.Binder; @@ -6,39 +9,114 @@ public interface IMagicOnionStreamingHubMethod { string ServiceName { get; } string MethodName { get; } + MethodInfo MethodInfo { get; } - [Obsolete] - MethodInfo Method { get; } + ValueTask InvokeAsync(StreamingHubContext context); } // TODO: -public class MagicOnionStreamingHubMethod(string serviceName, string methodName, MethodInfo methodInfo) : IMagicOnionStreamingHubMethod +public class MagicOnionStreamingHubMethod : IMagicOnionStreamingHubMethod { - public string ServiceName => serviceName; - public string MethodName => methodName; - public MethodInfo Method => methodInfo; + public string ServiceName { get; } + public string MethodName { get; } + public MethodInfo MethodInfo { get; } - public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func> invoker) : this(serviceName, methodName, invoker.Method) + readonly Func> invoker; + + // for Dynamic + public MagicOnionStreamingHubMethod(string serviceName, string methodName, Delegate invoker) + { + Debug.Assert(invoker is Func> or Func>); + + this.ServiceName = serviceName; + this.MethodName = methodName; + this.MethodInfo = typeof(TService).GetMethod(MethodName) ?? throw new InvalidOperationException(); + + if (invoker is Func> invokerTask) + { + this.invoker = InvokeTask; + ValueTask InvokeTask(TService instance, StreamingHubContext context, TRequest request) + => new(invokerTask(instance, context, request)); + } + else + { + this.invoker = (Func>)invoker; + } + } + + public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func> invoker) : this(serviceName, methodName, (Delegate)invoker) { } - public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func> invoker) : this(serviceName, methodName, invoker.Method) + public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func> invoker) : this(serviceName, methodName, (Delegate)invoker) { } + + public ValueTask InvokeAsync(StreamingHubContext context) + { + var seq = new ReadOnlySequence(context.Request); + TRequest request = context.ServiceContext.MessageSerializer.Deserialize(seq); + var result = invoker((TService)context.HubInstance, context, request); + return context.WriteResponseMessage(result); + } } -public class MagicOnionStreamingHubMethod(string serviceName, string methodName, MethodInfo methodInfo) : IMagicOnionStreamingHubMethod +public class MagicOnionStreamingHubMethod : IMagicOnionStreamingHubMethod { - public string ServiceName => serviceName; - public string MethodName => methodName; - public MethodInfo Method => methodInfo; + public string ServiceName { get; } + public string MethodName { get; } + public MethodInfo MethodInfo { get; } + + readonly Func invoker; + + // for Dynamic + public MagicOnionStreamingHubMethod(string serviceName, string methodName, Delegate invoker) + { + Debug.Assert(invoker is Func or Func or Action); + + this.ServiceName = serviceName; + this.MethodName = methodName; + this.MethodInfo = typeof(TService).GetMethod(MethodName) ?? throw new InvalidOperationException(); + + if (invoker is Func invokerTask) + { + this.invoker = InvokeTask; + ValueTask InvokeTask(TService instance, StreamingHubContext context, TRequest request) + => new(invokerTask(instance, context, request)); + } + else if (invoker is Action invokerVoid) + { + this.invoker = InvokeVoid; + ValueTask InvokeVoid(TService instance, StreamingHubContext context, TRequest request) + { + invokerVoid(instance, context, request); + return default; + } + } + else + { + this.invoker = (Func)invoker; + } + } + + public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func invoker) : this(serviceName, methodName, (Delegate)invoker) + { + } + + public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func invoker) : this(serviceName, methodName, (Delegate)invoker) + { + } - public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func invoker) : this(serviceName, methodName, invoker.Method) + public MagicOnionStreamingHubMethod(string serviceName, string methodName, Action invoker) : this(serviceName, methodName, (Delegate)invoker) { } - public MagicOnionStreamingHubMethod(string serviceName, string methodName, Func invoker) : this(serviceName, methodName, invoker.Method) + public ValueTask InvokeAsync(StreamingHubContext context) { + var seq = new ReadOnlySequence(context.Request); + TRequest request = context.ServiceContext.MessageSerializer.Deserialize(seq); + var response = invoker((TService)context.HubInstance, context, request); + return context.WriteResponseMessageNil(response); } } diff --git a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs index 323214e02..e8f913e66 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs @@ -8,7 +8,7 @@ public interface IMagicOnionUnaryMethod(string serviceName, string methodName) @@ -25,24 +25,24 @@ public abstract class MagicOnionUnaryMethodBase binder) => binder.BindUnary(this); - public abstract ValueTask InvokeAsync(TService service, TRequest request, ServiceContext context); + public abstract ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request); } -public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func> invoker) +public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func> invoker) : MagicOnionUnaryMethodBase(serviceName, methodName) where TService : class where TRawRequest : class where TRawResponse : class { - public override ValueTask InvokeAsync(TService service, TRequest request, ServiceContext context) - => MethodHandlerResultHelper.SetUnaryResult(invoker(service, request, context), context); + public override ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) + => MethodHandlerResultHelper.SetUnaryResult(invoker(service, context, request), context); } -public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func invoker) +public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func invoker) : MagicOnionUnaryMethodBase>(serviceName, methodName) where TService : class where TRawRequest : class { - public override ValueTask InvokeAsync(TService service, TRequest request, ServiceContext context) - => MethodHandlerResultHelper.SetUnaryResultNonGeneric(invoker(service, request, context), context); + public override ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) + => MethodHandlerResultHelper.SetUnaryResultNonGeneric(invoker(service, context, request), context); } diff --git a/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs b/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs index 59c176a8b..8452a42ae 100644 --- a/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs +++ b/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs @@ -46,7 +46,7 @@ public void RegisterMethods(IEnumerable methods) { var streamingHubHandlerOptions = new StreamingHubHandlerOptions(options); var methodAndIdPairs = methods - .Select(x => new StreamingHubHandler(x.Method.DeclaringType!, x.Method, streamingHubHandlerOptions, serviceProvider)) + .Select(x => new StreamingHubHandler(typeof(TService), x, streamingHubHandlerOptions, serviceProvider)) .Select(x => (x.MethodId, x)) .ToArray(); diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs index c2d646961..9d58b3ec0 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs @@ -8,6 +8,7 @@ using MagicOnion.Server.Filters.Internal; using MagicOnion.Server.Internal; using MagicOnion.Serialization; +using MagicOnion.Server.Binder; namespace MagicOnion.Server.Hubs; @@ -27,44 +28,16 @@ public class StreamingHubHandler : IEquatable internal Type RequestType => metadata.RequestType; internal Func MethodBody { get; } - public StreamingHubHandler(Type classType, MethodInfo methodInfo, StreamingHubHandlerOptions handlerOptions, IServiceProvider serviceProvider) + public StreamingHubHandler(Type implementationType, IMagicOnionStreamingHubMethod hubMethod, StreamingHubHandlerOptions handlerOptions, IServiceProvider serviceProvider) { - this.metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(classType, methodInfo); + this.metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(implementationType, hubMethod.MethodInfo); this.toStringCache = HubName + "/" + MethodInfo.Name; this.getHashCodeCache = HashCode.Combine(HubName, MethodInfo.Name); - var messageSerializer = handlerOptions.MessageSerializer.Create(MethodType.DuplexStreaming, methodInfo); - var parameters = metadata.Parameters; try { - // var invokeHubMethodFunc = (context, request) => ((HubType)context.HubInstance).Foo(request); - // or - // var invokeHubMethodFunc = (context, request) => ((HubType)context.HubInstance).Foo(request.Item1, request.Item2 ...); - var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - var contextArg = Expression.Parameter(typeof(StreamingHubContext), "context"); - var requestArg = Expression.Parameter(RequestType, "request"); - var getInstanceCast = Expression.Convert(Expression.Property(contextArg, typeof(StreamingHubContext).GetProperty(nameof(StreamingHubContext.HubInstance), flags)!), HubType); - Expression[] arguments = new Expression[parameters.Count]; - if (parameters.Count == 1) - { - arguments[0] = requestArg; - } - else - { - for (int i = 0; i < parameters.Count; i++) - { - arguments[i] = Expression.Field(requestArg, "Item" + (i + 1)); - } - } - var callHubMethod = Expression.Call(getInstanceCast, methodInfo, arguments); - var invokeHubMethodFunc = Expression.Lambda(callHubMethod, contextArg, requestArg).Compile(); - - // Create a StreamingHub method invoker and a wrapped-invoke method. - Type invokerType = StreamingHubMethodInvoker.CreateInvokerTypeFromMetadata(metadata); - StreamingHubMethodInvoker invoker = (StreamingHubMethodInvoker)Activator.CreateInstance(invokerType, messageSerializer, invokeHubMethodFunc)!; - - var filters = FilterHelper.GetFilters(handlerOptions.GlobalStreamingHubFilters, classType, methodInfo); - this.MethodBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, invoker.InvokeAsync); + var filters = FilterHelper.GetFilters(handlerOptions.GlobalStreamingHubFilters, implementationType, hubMethod.MethodInfo); + this.MethodBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, hubMethod.InvokeAsync); } catch (Exception ex) { @@ -98,123 +71,3 @@ public StreamingHubHandlerOptions(MagicOnionOptions options) } } -internal abstract class StreamingHubMethodInvoker -{ - protected IMagicOnionSerializer MessageSerializer { get; } - - protected StreamingHubMethodInvoker(IMagicOnionSerializer messageSerializer) - { - MessageSerializer = messageSerializer; - } - - public abstract ValueTask InvokeAsync(StreamingHubContext context); - - public static Type CreateInvokerTypeFromMetadata(in StreamingHubMethodHandlerMetadata metadata) - { - var isVoid = metadata.InterfaceMethod.ReturnType == typeof(void); - var isTaskOrTaskOfT = metadata.InterfaceMethod.ReturnType == typeof(Task) || - (metadata.InterfaceMethod.ReturnType is { IsGenericType: true } t && t.BaseType == typeof(Task)); - return isVoid - ? typeof(StreamingHubMethodInvokerVoid<>).MakeGenericType(metadata.RequestType) - : isTaskOrTaskOfT - ? (metadata.ResponseType is null - ? typeof(StreamingHubMethodInvokerTask<>).MakeGenericType(metadata.RequestType) - : typeof(StreamingHubMethodInvokerTask<,>).MakeGenericType(metadata.RequestType, metadata.ResponseType) - ) - : (metadata.ResponseType is null - ? typeof(StreamingHubMethodInvokerValueTask<>).MakeGenericType(metadata.RequestType) - : typeof(StreamingHubMethodInvokerValueTask<,>).MakeGenericType(metadata.RequestType, metadata.ResponseType) - ); - } - - sealed class StreamingHubMethodInvokerVoid : StreamingHubMethodInvoker - { - readonly Action hubMethodFunc; - - public StreamingHubMethodInvokerVoid(IMagicOnionSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) - { - this.hubMethodFunc = (Action)hubMethodFunc; - } - - public override ValueTask InvokeAsync(StreamingHubContext context) - { - var seq = new ReadOnlySequence(context.Request); - TRequest request = MessageSerializer.Deserialize(seq); - hubMethodFunc(context, request); - return context.WriteResponseMessageNil(default); - } - } - - sealed class StreamingHubMethodInvokerTask : StreamingHubMethodInvoker - { - readonly Func> hubMethodFunc; - - public StreamingHubMethodInvokerTask(IMagicOnionSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) - { - this.hubMethodFunc = (Func>)hubMethodFunc; - } - - public override ValueTask InvokeAsync(StreamingHubContext context) - { - var seq = new ReadOnlySequence(context.Request); - TRequest request = MessageSerializer.Deserialize(seq); - Task response = hubMethodFunc(context, request); - return context.WriteResponseMessage(new ValueTask(response)); - } - } - - sealed class StreamingHubMethodInvokerTask : StreamingHubMethodInvoker - { - readonly Func hubMethodFunc; - - public StreamingHubMethodInvokerTask(IMagicOnionSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) - { - this.hubMethodFunc = (Func)hubMethodFunc; - } - - public override ValueTask InvokeAsync(StreamingHubContext context) - { - var seq = new ReadOnlySequence(context.Request); - TRequest request = MessageSerializer.Deserialize(seq); - Task response = hubMethodFunc(context, request); - return context.WriteResponseMessageNil(new ValueTask(response)); - } - } - - sealed class StreamingHubMethodInvokerValueTask : StreamingHubMethodInvoker - { - readonly Func> hubMethodFunc; - - public StreamingHubMethodInvokerValueTask(IMagicOnionSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) - { - this.hubMethodFunc = (Func>)hubMethodFunc; - } - - public override ValueTask InvokeAsync(StreamingHubContext context) - { - var seq = new ReadOnlySequence(context.Request); - TRequest request = MessageSerializer.Deserialize(seq); - ValueTask response = hubMethodFunc(context, request); - return context.WriteResponseMessage(response); - } - } - - sealed class StreamingHubMethodInvokerValueTask : StreamingHubMethodInvoker - { - readonly Func hubMethodFunc; - - public StreamingHubMethodInvokerValueTask(IMagicOnionSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) - { - this.hubMethodFunc = (Func)hubMethodFunc; - } - - public override ValueTask InvokeAsync(StreamingHubContext context) - { - var seq = new ReadOnlySequence(context.Request); - TRequest request = MessageSerializer.Deserialize(seq); - ValueTask response = hubMethodFunc(context, request); - return context.WriteResponseMessageNil(response); - } - } - -} diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs b/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs index 863c7cba0..48b4e6aad 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs @@ -7,6 +7,7 @@ namespace MagicOnion.Server.Hubs; // Global cache of Streaming Handler +[Obsolete] internal class StreamingHubHandlerRepository { bool frozen; diff --git a/src/MagicOnion.Server/MagicOnionEngine.cs b/src/MagicOnion.Server/MagicOnionEngine.cs index 710af45f1..a1cb76603 100644 --- a/src/MagicOnion.Server/MagicOnionEngine.cs +++ b/src/MagicOnion.Server/MagicOnionEngine.cs @@ -116,125 +116,13 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP var handlers = new HashSet(); var streamingHubHandlers = new List(); - var methodHandlerOptions = new MethodHandlerOptions(options); - var streamingHubHandlerOptions = new StreamingHubHandlerOptions(options); - var loggerFactory = serviceProvider.GetRequiredService(); var loggerMagicOnionEngine = loggerFactory.CreateLogger(LoggerNameMagicOnionEngine); - var loggerMethodHandler = loggerFactory.CreateLogger(LoggerNameMethodHandler); - - var streamingHubHandlerRepository = serviceProvider.GetRequiredService(); MagicOnionServerLog.BeginBuildServiceDefinition(loggerMagicOnionEngine); var sw = Stopwatch.StartNew(); - try - { - foreach (var classType in targetTypes) - { - VerifyServiceType(classType); - - var className = classType.Name; - var isStreamingHub = typeof(IStreamingHubMarker).IsAssignableFrom(classType); - HashSet? tempStreamingHubHandlers = null; - if (isStreamingHub) - { - tempStreamingHubHandlers = new HashSet(); - } - - var inheritInterface = classType.GetInterfaces() - .First(x => x.IsGenericType && x.GetGenericTypeDefinition() == (isStreamingHub ? typeof(IStreamingHub<,>) : typeof(IService<>))) - .GenericTypeArguments[0]; - - if (!inheritInterface.IsAssignableFrom(classType)) - { - throw new NotImplementedException($"Type '{classType.FullName}' has no implementation of interface '{inheritInterface.FullName}'."); - } - - var interfaceMap = classType.GetInterfaceMapWithParents(inheritInterface); - - for (int i = 0; i < interfaceMap.TargetMethods.Length; ++i) - { - var methodInfo = interfaceMap.TargetMethods[i]; - var methodName = interfaceMap.InterfaceMethods[i].Name; - - if (methodInfo.IsSpecialName && (methodInfo.Name.StartsWith("set_") || methodInfo.Name.StartsWith("get_"))) continue; - if (methodInfo.GetCustomAttribute(false) != null) continue; // ignore - - // ignore default methods - if (methodName == "Equals" - || methodName == "GetHashCode" - || methodName == "GetType" - || methodName == "ToString" - || methodName == "WithOptions" - || methodName == "WithHeaders" - || methodName == "WithDeadline" - || methodName == "WithCancellationToken" - || methodName == "WithHost" - ) - { - continue; - } - - // register for StreamingHub - if (isStreamingHub && methodName != "Connect") - { - var streamingHandler = new StreamingHubHandler(classType, methodInfo, streamingHubHandlerOptions, serviceProvider); - if (!tempStreamingHubHandlers!.Add(streamingHandler)) - { - throw new InvalidOperationException($"Method does not allow overload, {className}.{methodName}"); - } - continue; - } - else - { - continue; - // create handler - var handler = new MethodHandler(classType, methodInfo, methodName, methodHandlerOptions, serviceProvider, loggerMethodHandler, isStreamingHub: false); - if (!handlers.Add(handler)) - { - throw new InvalidOperationException($"Method does not allow overload, {className}.{methodName}"); - } - } - } - - continue; - if (isStreamingHub) - { - var connectHandler = new MethodHandler(classType, classType.GetMethod("Connect", BindingFlags.NonPublic | BindingFlags.Instance)!, "Connect", methodHandlerOptions, serviceProvider, loggerMethodHandler, isStreamingHub: true); - if (!handlers.Add(connectHandler)) - { - throw new InvalidOperationException($"Method does not allow overload, {className}.Connect"); - } - - streamingHubHandlers.AddRange(tempStreamingHubHandlers!); - streamingHubHandlerRepository.RegisterHandler(connectHandler, tempStreamingHubHandlers!.ToArray()); - - // Group Provider - IMulticastGroupProvider groupProvider; - var attr = classType.GetCustomAttribute(true); - if (attr != null) - { - groupProvider = (IMulticastGroupProvider)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, attr.FactoryType); - } - else - { - groupProvider = serviceProvider.GetRequiredService(); - } - - streamingHubHandlerRepository.RegisterGroupProvider(connectHandler, groupProvider); - streamingHubHandlerRepository.RegisterHeartbeatManager(connectHandler, CreateHeartbeatManager(options, classType, serviceProvider)); - } - } - } - catch (AggregateException agex) - { - ExceptionDispatchInfo.Capture(agex.InnerExceptions[0]).Throw(); - } - - streamingHubHandlerRepository.Freeze(); - var result = new MagicOnionServiceDefinition(handlers.ToArray(), streamingHubHandlers.ToArray(), targetTypes.ToArray()); sw.Stop(); @@ -243,47 +131,6 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP return result; } - static IStreamingHubHeartbeatManager CreateHeartbeatManager(MagicOnionOptions options, Type classType, IServiceProvider serviceProvider) - { - var heartbeatEnable = options.EnableStreamingHubHeartbeat; - var heartbeatInterval = options.StreamingHubHeartbeatInterval; - var heartbeatTimeout = options.StreamingHubHeartbeatTimeout; - var heartbeatMetadataProvider = default(IStreamingHubHeartbeatMetadataProvider); - if (classType.GetCustomAttribute(inherit: true) is { } heartbeatAttr) - { - heartbeatEnable = heartbeatAttr.Enable; - if (heartbeatAttr.Timeout != 0) - { - heartbeatTimeout = TimeSpan.FromMilliseconds(heartbeatAttr.Timeout); - } - if (heartbeatAttr.Interval != 0) - { - heartbeatInterval = TimeSpan.FromMilliseconds(heartbeatAttr.Interval); - } - if (heartbeatAttr.MetadataProvider != null) - { - heartbeatMetadataProvider = (IStreamingHubHeartbeatMetadataProvider)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, heartbeatAttr.MetadataProvider); - } - } - - IStreamingHubHeartbeatManager heartbeatManager; - if (!heartbeatEnable || heartbeatInterval is null) - { - heartbeatManager = NopStreamingHubHeartbeatManager.Instance; - } - else - { - heartbeatManager = new StreamingHubHeartbeatManager( - heartbeatInterval.Value, - heartbeatTimeout ?? Timeout.InfiniteTimeSpan, - heartbeatMetadataProvider ?? serviceProvider.GetService(), - options.TimeProvider ?? TimeProvider.System, - serviceProvider.GetRequiredService>() - ); - } - - return heartbeatManager; - } internal static void VerifyServiceType(Type type) { diff --git a/src/MagicOnion.Server/MethodHandler.cs b/src/MagicOnion.Server/MethodHandler.cs index 05b7c77eb..8f4f526e3 100644 --- a/src/MagicOnion.Server/MethodHandler.cs +++ b/src/MagicOnion.Server/MethodHandler.cs @@ -14,6 +14,7 @@ namespace MagicOnion.Server; +[Obsolete] public class MethodHandler : IEquatable { // reflection cache diff --git a/tests/MagicOnion.Server.Tests/FakeStreamingServiceContext.cs b/tests/MagicOnion.Server.Tests/FakeStreamingServiceContext.cs index 3695710ae..037807515 100644 --- a/tests/MagicOnion.Server.Tests/FakeStreamingServiceContext.cs +++ b/tests/MagicOnion.Server.Tests/FakeStreamingServiceContext.cs @@ -26,7 +26,6 @@ class FakeStreamingServiceContext : IStreamingServiceContex public FakeStreamingServiceContext(Type serviceType, MethodInfo methodInfo, IMagicOnionSerializer messageSerializer, IServiceProvider serviceProvider, ILookup attributeLookup = null) { ServiceType = serviceType; - MethodInfo = methodInfo; MessageSerializer = messageSerializer; ServiceProvider = serviceProvider; diff --git a/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs b/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs index 66b86ce3c..b55111755 100644 --- a/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs +++ b/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Grpc.Net.Client; using MagicOnion.Client; using MagicOnion.Internal; @@ -140,16 +141,16 @@ public IEnumerable GetGrpcMethods() where TServ if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterService)) { yield return new MagicOnionUnaryMethod, string, Box>, string>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService.HelloAsync), static (instance, request, context) => instance.HelloAsync(request.Item1, request.Item2)); + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService.HelloAsync), static (instance, context, request) => instance.HelloAsync(request.Item1, request.Item2)); yield return new MagicOnionUnaryMethod>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService.PingAsync), static (instance, request, context) => instance.PingAsync()); + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService.PingAsync), static (instance, context, request) => instance.PingAsync()); } if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterService2)) { yield return new MagicOnionUnaryMethod, string, Box>, string>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2.GoodByeAsync), static (instance, request, context) => instance.GoodByeAsync(request.Item1, request.Item2)); + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2.GoodByeAsync), static (instance, context, request) => instance.GoodByeAsync(request.Item1, request.Item2)); yield return new MagicOnionUnaryMethod>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2.PingAsync), static (instance, request, context) => instance.PingAsync()); + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2.PingAsync), static (instance, context, request) => instance.PingAsync()); } if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub)) @@ -162,18 +163,18 @@ public IEnumerable GetStreamingHubMethods>( - // nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync), static (instance, request) => instance.JoinAsync(request.Item1, request.Item2)); - //yield return new MagicOnionStreamingHubMethod( - // nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.SendMessageAsync), static (instance, request) => instance.SendMessageAsync(request)); - //yield return new MagicOnionStreamingHubMethod>( - // nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync), static (instance, request) => instance.GetMembersAsync()); yield return new MagicOnionStreamingHubMethod>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync), typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub).GetMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync))!); + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync), static (instance, context, request) => instance.JoinAsync(request.Item1, request.Item2)); yield return new MagicOnionStreamingHubMethod( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.SendMessageAsync), typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub).GetMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.SendMessageAsync))!); + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.SendMessageAsync), static (instance, context, request) => instance.SendMessageAsync(request)); yield return new MagicOnionStreamingHubMethod>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync), typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub).GetMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync))!); + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync), static (instance, context, request) => instance.GetMembersAsync()); + //yield return new MagicOnionStreamingHubMethod>( + // nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync), typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub).GetMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync))!); + //yield return new MagicOnionStreamingHubMethod( + // nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.SendMessageAsync), typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub).GetMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.SendMessageAsync))!); + //yield return new MagicOnionStreamingHubMethod>( + // nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync), typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub).GetMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync))!); } } } diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs index a66945b7a..e1f330c75 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs @@ -3,6 +3,7 @@ using MagicOnion.Internal; using MagicOnion.Serialization; using MagicOnion.Serialization.MessagePack; +using MagicOnion.Server.Binder; using MagicOnion.Server.Hubs; using MessagePack; using Microsoft.Extensions.DependencyInjection; @@ -19,9 +20,12 @@ public async Task Parameterless_Returns_Task() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_Task))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_Task))!; + var hubMethod = new MagicOnionStreamingHubMethod( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_Task), + static (instance, context, _) => instance.Method_Parameterless_Returns_Task()); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -54,9 +58,12 @@ public async Task Parameterless_Returns_TaskOfInt32() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_TaskOfInt32))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_TaskOfInt32))!; + var hubMethod = new MagicOnionStreamingHubMethod( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_TaskOfInt32), + static (instance, context, _) => instance.Method_Parameterless_Returns_TaskOfInt32()); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -88,9 +95,12 @@ public async Task Parameterless_Returns_ValueTask() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTask))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTask))!; + var hubMethod = new MagicOnionStreamingHubMethod( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTask), + static (instance, context, _) => instance.Method_Parameterless_Returns_ValueTask()); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -123,9 +133,12 @@ public async Task Parameterless_Returns_ValueTaskOfInt32() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTaskOfInt32))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTaskOfInt32))!; + var hubMethod = new MagicOnionStreamingHubMethod( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTaskOfInt32), + static (instance, context, _) => instance.Method_Parameterless_Returns_ValueTaskOfInt32()); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -158,9 +171,12 @@ public async Task Parameter_Single_Returns_Task() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Single_Returns_Task))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Single_Returns_Task))!; + var hubMethod = new MagicOnionStreamingHubMethod( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameter_Single_Returns_Task), + static (instance, context, request) => instance.Method_Parameter_Single_Returns_Task(request)); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -193,9 +209,12 @@ public async Task Parameter_Multiple_Returns_Task() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_Task))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_Task))!; + var hubMethod = new MagicOnionStreamingHubMethod>( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_Task), + static (instance, context, request) => instance.Method_Parameter_Multiple_Returns_Task(request.Item1, request.Item2, request.Item3)); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -227,9 +246,12 @@ public async Task Parameter_Multiple_Returns_TaskOfInt32() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32))!; + var hubMethod = new MagicOnionStreamingHubMethod, int>( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32), + static (instance, context, request) => instance.Method_Parameter_Multiple_Returns_TaskOfInt32(request.Item1, request.Item2, request.Item3)); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -261,9 +283,12 @@ public async Task CallRepeated_Parameter_Multiple_Returns_TaskOfInt32() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32))!; + var hubMethod = new MagicOnionStreamingHubMethod, int>( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32), + static (instance, context, request) => instance.Method_Parameter_Multiple_Returns_TaskOfInt32(request.Item1, request.Item2, request.Item3)); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -303,9 +328,12 @@ public async Task Parameterless_Void() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Void))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Void))!; + var hubMethod = new MagicOnionStreamingHubMethod( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameterless_Void), + static (instance, context, _) => instance.Method_Parameterless_Void()); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -338,9 +366,12 @@ public async Task Parameter_Single_Void() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Single_Void))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Single_Void))!; + var hubMethod = new MagicOnionStreamingHubMethod( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameter_Single_Void), + static (instance, context, request) => instance.Method_Parameter_Single_Void(request)); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -373,9 +404,12 @@ public async Task Parameter_Multiple_Void() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Void))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Void))!; + var hubMethod = new MagicOnionStreamingHubMethod>( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Void), + static (instance, context, request) => instance.Method_Parameter_Multiple_Void(request.Item1, request.Item2, request.Item3)); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -408,9 +442,12 @@ public async Task Parameter_Multiple_Void_Without_MessageId() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Void))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Void))!; + var hubMethod = new MagicOnionStreamingHubMethod>( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Void), + static (instance, context, request) => instance.Method_Parameter_Multiple_Void(request.Item1, request.Item2, request.Item3)); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); @@ -430,9 +467,12 @@ public async Task UseCustomMessageSerializer() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32))!; + var hubMethod = new MagicOnionStreamingHubMethod, int>( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32), + static (instance, context, request) => instance.Method_Parameter_Multiple_Returns_TaskOfInt32(request.Item1, request.Item2, request.Item3)); var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, XorMessagePackMagicOnionSerializerProvider.Instance.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, XorMessagePackMagicOnionSerializerProvider.Instance.Create(MethodType.DuplexStreaming, null), serviceProvider); var bufferWriter = new ArrayBufferWriter(); var serializer = XorMessagePackMagicOnionSerializerProvider.Instance.Create(MethodType.DuplexStreaming, null); serializer.Serialize(bufferWriter, new DynamicArgumentTuple(12345, "テスト", true)); @@ -470,7 +510,10 @@ public void MethodAttributeLookup() var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); var hubType = typeof(StreamingHubHandlerTestHub); - var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Attribute))!; + var hubMethodInfo = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Attribute))!; + var hubMethod = new MagicOnionStreamingHubMethod( + nameof(StreamingHubHandlerTestHub), nameof(StreamingHubHandlerTestHub.Method_Attribute), + static (instance, context, _) => instance.Method_Attribute()); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); From 1b9430abf8c8592df64c0c08ad8aa5d96cc4dd26 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Tue, 15 Oct 2024 14:27:46 +0900 Subject: [PATCH 03/27] Remove legacy implementations --- .../Binder/IMagicOnionGrpcMethod.cs | 13 +- .../Internal/MagicOnionGrpcMethodBinder.cs | 395 +---------- .../Internal/MagicOnionGrpcMethodHandler.cs | 379 +++++++++++ .../MagicOnionGrpcServiceMethodProvider.cs | 3 +- .../Binder/Legacy/MagicOnionService.cs | 4 - .../Binder/Legacy/MagicOnionServiceBinder.cs | 170 ----- .../Legacy/MagicOnionServiceMethodProvider.cs | 24 - .../Binder/MagicOnionClientStreamingMethod.cs | 3 + .../Binder/MagicOnionDuplexStreamingMethod.cs | 3 + .../Binder/MagicOnionServerStreamingMethod.cs | 3 + .../MagicOnionStreamingHubConnectMethod.cs | 3 + .../Binder/MagicOnionUnaryMethod.cs | 102 +++ ...agicOnionEndpointRouteBuilderExtensions.cs | 4 +- .../MagicOnionServicesExtensions.cs | 2 - src/MagicOnion.Server/Hubs/StreamingHub.cs | 6 +- .../Hubs/StreamingHubHandlerRepository.cs | 88 --- src/MagicOnion.Server/MagicOnionEngine.cs | 5 +- .../MagicOnionServiceDefinition.cs | 6 +- src/MagicOnion.Server/MethodHandler.cs | 635 ------------------ .../ServiceContext.Streaming.cs | 11 +- src/MagicOnion.Server/ServiceContext.cs | 23 +- .../MagicOnionEngineTest.cs | 2 + 22 files changed, 535 insertions(+), 1349 deletions(-) create mode 100644 src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs delete mode 100644 src/MagicOnion.Server/Binder/Legacy/MagicOnionService.cs delete mode 100644 src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceBinder.cs delete mode 100644 src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceMethodProvider.cs delete mode 100644 src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs delete mode 100644 src/MagicOnion.Server/MethodHandler.cs diff --git a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs index 085a12f30..899b4a630 100644 --- a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs @@ -1,14 +1,19 @@ using System.Reflection; +using Grpc.Core; namespace MagicOnion.Server.Binder; -public interface IMagicOnionGrpcMethod; - -public interface IMagicOnionGrpcMethod : IMagicOnionGrpcMethod - where TService : class +public interface IMagicOnionGrpcMethod { + MethodType MethodType { get; } + Type ServiceType { get; } string ServiceName { get; } string MethodName { get; } MethodInfo MethodInfo { get; } +} + +public interface IMagicOnionGrpcMethod : IMagicOnionGrpcMethod + where TService : class +{ void Bind(IMagicOnionGrpcMethodBinder binder); } diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs index 9d858fb6f..3a0231113 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs @@ -1,17 +1,10 @@ -using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.ExceptionServices; using Grpc.AspNetCore.Server.Model; using Grpc.Core; using MagicOnion.Internal; using MagicOnion.Serialization; -using MagicOnion.Server.Diagnostics; -using MagicOnion.Server.Filters; -using MagicOnion.Server.Filters.Internal; using MagicOnion.Server.Hubs.Internal; using MagicOnion.Server.Internal; -using MessagePack; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -23,22 +16,20 @@ internal class MagicOnionGrpcMethodBinder : IMagicOnionGrpcMethodBinde { readonly ServiceMethodProviderContext providerContext; readonly IMagicOnionSerializerProvider messageSerializerProvider; - readonly IList globalFilters; - readonly IServiceProvider serviceProvider; - readonly ILogger logger; - readonly bool enableCurrentContext; - readonly bool isReturnExceptionStackTraceInErrorDetail; + readonly MagicOnionGrpcMethodHandler handlerBuilder; - public MagicOnionGrpcMethodBinder(ServiceMethodProviderContext context, MagicOnionOptions options, IServiceProvider serviceProvider, ILogger> logger) + public MagicOnionGrpcMethodBinder(ServiceMethodProviderContext context, MagicOnionOptions options, ILoggerFactory loggerFactory, IServiceProvider serviceProvider) { this.providerContext = context; this.messageSerializerProvider = options.MessageSerializer; - this.globalFilters = options.GlobalFilters; - this.serviceProvider = serviceProvider; - this.logger = logger; - this.enableCurrentContext = options.EnableCurrentContext; - this.isReturnExceptionStackTraceInErrorDetail = options.IsReturnExceptionStackTraceInErrorDetail; + this.handlerBuilder = new MagicOnionGrpcMethodHandler( + options.EnableCurrentContext, + options.IsReturnExceptionStackTraceInErrorDetail, + serviceProvider, + options.GlobalFilters, + loggerFactory.CreateLogger>() + ); } public void BindUnary(IMagicOnionUnaryMethod method) @@ -49,7 +40,7 @@ public void BindUnary(IMagicOnio var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.Unary, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method.MethodInfo); - providerContext.AddUnaryMethod(grpcMethod, attrs, BuildUnaryMethodPipeline(method, messageSerializer, attrs)); + providerContext.AddUnaryMethod(grpcMethod, attrs, handlerBuilder.BuildUnaryMethod(method, messageSerializer, attrs)); } public void BindClientStreaming(MagicOnionClientStreamingMethod method) @@ -60,7 +51,7 @@ public void BindClientStreaming( var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ClientStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method.MethodInfo); - providerContext.AddClientStreamingMethod(grpcMethod, attrs, BuildClientStreamingMethodPipeline(method, messageSerializer, attrs)); + providerContext.AddClientStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildClientStreamingMethod(method, messageSerializer, attrs)); } public void BindServerStreaming(MagicOnionServerStreamingMethod method) @@ -71,7 +62,7 @@ public void BindServerStreaming( var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ServerStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method.MethodInfo); - providerContext.AddServerStreamingMethod(grpcMethod, attrs, BuildServerStreamingMethodPipeline(method, messageSerializer, attrs)); + providerContext.AddServerStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildServerStreamingMethod(method, messageSerializer, attrs)); } public void BindDuplexStreaming(MagicOnionDuplexStreamingMethod method) @@ -82,7 +73,7 @@ public void BindDuplexStreaming( var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.DuplexStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method.MethodInfo); - providerContext.AddDuplexStreamingMethod(grpcMethod, attrs, BuildDuplexStreamingMethodPipeline(method, messageSerializer, attrs)); + providerContext.AddDuplexStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildDuplexStreamingMethod(method, messageSerializer, attrs)); } public void BindStreamingHub(MagicOnionStreamingHubConnectMethod method) @@ -107,7 +98,7 @@ public void BindStreamingHub(MagicOnionStreamingHubConnectMethod metho context.CallContext.GetHttpContext().Features.Set(context.ServiceProvider.GetRequiredService>()); return ((IStreamingHubBase)instance).Connect(); }); - providerContext.AddDuplexStreamingMethod(grpcMethod, attrs, BuildDuplexStreamingMethodPipeline(duplexMethod, messageSerializer, attrs)); + providerContext.AddDuplexStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildDuplexStreamingMethod(duplexMethod, messageSerializer, attrs)); } IList GetMetadataFromHandler(MethodInfo methodInfo) @@ -121,362 +112,4 @@ IList GetMetadataFromHandler(MethodInfo methodInfo) metadata.Add(new HttpMethodMetadata(["POST"], acceptCorsPreflight: true)); return metadata; } - - void InitializeServiceProperties(object instance, ServiceContext serviceContext) - { - var service = ((IServiceBase)instance); - service.Context = serviceContext; - service.Metrics = serviceProvider.GetRequiredService(); - } - - ClientStreamingServerMethod BuildClientStreamingMethodPipeline( - MagicOnionClientStreamingMethod method, - IMagicOnionSerializer messageSerializer, - IList metadata - ) - where TRawRequest : class - where TRawResponse : class - { - var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); - var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); - var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext)); - - return InvokeAsync; - - async Task InvokeAsync(TService instance, IAsyncStreamReader rawRequestStream, ServerCallContext context) - { - var isCompletedSuccessfully = false; - var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); - - var requestStream = new MagicOnionAsyncStreamReader(rawRequestStream); - var serviceContext = new StreamingServiceContext( - instance, - typeof(TService), - method.ServiceName, - method.MethodInfo, - attributeLookup, - MethodType.ClientStreaming, - context, - messageSerializer, - logger, - default!, - context.GetHttpContext().RequestServices, - requestStream, - default - ); - - InitializeServiceProperties(instance, serviceContext); - - TResponse response; - try - { - using (rawRequestStream as IDisposable) - { - MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(Nil)); - if (enableCurrentContext) - { - ServiceContext.currentServiceContext.Value = serviceContext; - } - await wrappedBody(serviceContext); - response = serviceContext.Result is TResponse r ? r : default!; - isCompletedSuccessfully = true; - } - } - catch (ReturnStatusException ex) - { - context.Status = ex.ToStatus(); - response = default!; - } - catch (Exception ex) - { - if (TryResolveStatus(ex, out var status)) - { - context.Status = status.Value; - MagicOnionServerLog.Error(logger, ex, context); - response = default!; - } - else - { - throw; - } - } - finally - { - MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); - } - - return GrpcMethodHelper.ToRaw(response); - } - } - - ServerStreamingServerMethod BuildServerStreamingMethodPipeline( - MagicOnionServerStreamingMethod method, - IMagicOnionSerializer messageSerializer, - IList metadata - ) - where TRawRequest : class - where TRawResponse : class - { - var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); - var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); - var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, (TRequest)serviceContext.Request!, serviceContext)); - - return InvokeAsync; - - async Task InvokeAsync(TService instance, TRawRequest rawRequest, IServerStreamWriter rawResponseStream, ServerCallContext context) - { - var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); - var isCompletedSuccessfully = false; - - var request = GrpcMethodHelper.FromRaw(rawRequest); - var responseStream = new MagicOnionServerStreamWriter(rawResponseStream); - var serviceContext = new StreamingServiceContext( - instance, - typeof(TService), - method.ServiceName, - method.MethodInfo, - attributeLookup, - MethodType.ServerStreaming, - context, - messageSerializer, - logger, - default!, - context.GetHttpContext().RequestServices, - default, - responseStream - ); - - serviceContext.SetRawRequest(request); - - InitializeServiceProperties(instance, serviceContext); - - try - { - MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(Nil)); - if (enableCurrentContext) - { - ServiceContext.currentServiceContext.Value = serviceContext; - } - await wrappedBody(serviceContext); - isCompletedSuccessfully = true; - } - catch (ReturnStatusException ex) - { - context.Status = ex.ToStatus(); - } - catch (Exception ex) - { - if (TryResolveStatus(ex, out var status)) - { - context.Status = status.Value; - MagicOnionServerLog.Error(logger, ex, context); - } - else - { - throw; - } - } - finally - { - MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); - } - } - } - - DuplexStreamingServerMethod BuildDuplexStreamingMethodPipeline( - MagicOnionDuplexStreamingMethod method, - IMagicOnionSerializer messageSerializer, - IList metadata - ) - where TRawRequest : class - where TRawResponse : class - { - var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); - var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); - var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext)); - - return InvokeAsync; - - async Task InvokeAsync(TService instance, IAsyncStreamReader rawRequestStream, IServerStreamWriter rawResponseStream, ServerCallContext context) - { - var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); - var isCompletedSuccessfully = false; - - var requestStream = new MagicOnionAsyncStreamReader(rawRequestStream); - var responseStream = new MagicOnionServerStreamWriter(rawResponseStream); - var serviceContext = new StreamingServiceContext( - instance, - typeof(TService), - method.ServiceName, - method.MethodInfo, - attributeLookup, - MethodType.DuplexStreaming, - context, - messageSerializer, - logger, - default!, - context.GetHttpContext().RequestServices, - requestStream, - responseStream - ); - - InitializeServiceProperties(instance, serviceContext); - - try - { - MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(Nil)); - if (enableCurrentContext) - { - ServiceContext.currentServiceContext.Value = serviceContext; - } - - using (rawRequestStream as IDisposable) - { - await wrappedBody(serviceContext); - } - - isCompletedSuccessfully = true; - } - catch (ReturnStatusException ex) - { - context.Status = ex.ToStatus(); - } - catch (Exception ex) - { - if (TryResolveStatus(ex, out var status)) - { - context.Status = status.Value; - MagicOnionServerLog.Error(logger, ex, context); - } - else - { - throw; - } - } - finally - { - MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); - } - } - } - - UnaryServerMethod BuildUnaryMethodPipeline( - IMagicOnionUnaryMethod method, - IMagicOnionSerializer messageSerializer, - IList metadata - ) - where TRawRequest : class - where TRawResponse : class - { - var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); - var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); - var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext, (TRequest)serviceContext.Request!)); - - return InvokeAsync; - - async Task InvokeAsync(TService instance, TRawRequest requestRaw, ServerCallContext context) - { - var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); - var isCompletedSuccessfully = false; - - var serviceContext = new ServiceContext(instance, typeof(TService), method.ServiceName, method.MethodInfo, attributeLookup, MethodType.Unary, context, messageSerializer, logger, default!, context.GetHttpContext().RequestServices); - var request = GrpcMethodHelper.FromRaw(requestRaw); - - serviceContext.SetRawRequest(request); - - InitializeServiceProperties(instance, serviceContext); - - TResponse response = default!; - try - { - MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(TRequest)); - - if (enableCurrentContext) - { - ServiceContext.currentServiceContext.Value = serviceContext; - } - - await wrappedBody(serviceContext); - - isCompletedSuccessfully = true; - - if (serviceContext.Result is not null) - { - response = (TResponse)serviceContext.Result; - } - - if (response is RawBytesBox rawBytesResponse) - { - return Unsafe.As(ref rawBytesResponse); // NOTE: To disguise an object as a `TRawResponse`, `TRawResponse` must be `class`. - } - } - catch (ReturnStatusException ex) - { - context.Status = ex.ToStatus(); - response = default!; - - // WORKAROUND: Grpc.AspNetCore.Server throws a `Cancelled` status exception when it receives `null` response. - // To return the status code correctly, we need to rethrow the exception here. - // https://github.com/grpc/grpc-dotnet/blob/d4ee8babcd90666fc0727163a06527ab9fd7366a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs#L50-L56 - var rpcException = new RpcException(ex.ToStatus()); - if (ex.StackTrace is not null) - { - ExceptionDispatchInfo.SetRemoteStackTrace(rpcException, ex.StackTrace); - } - throw rpcException; - } - catch (Exception ex) - { - if (TryResolveStatus(ex, out var status)) - { - context.Status = status.Value; - MagicOnionServerLog.Error(logger, ex, context); - response = default!; - } - else - { - throw; - } - } - finally - { - MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); - } - - return GrpcMethodHelper.ToRaw(response); - } - } - - bool TryResolveStatus(Exception ex, [NotNullWhen(true)] out Status? status) - { - if (isReturnExceptionStackTraceInErrorDetail) - { - // Trim data. - var msg = ex.ToString(); - var lineSplit = msg.Split(new[] { Environment.NewLine }, StringSplitOptions.None); - var sb = new System.Text.StringBuilder(); - for (int i = 0; i < lineSplit.Length; i++) - { - if (!(lineSplit[i].Contains("System.Runtime.CompilerServices") - || lineSplit[i].Contains("直前に例外がスローされた場所からのスタック トレースの終わり") - || lineSplit[i].Contains("End of stack trace from the previous location where the exception was thrown") - )) - { - sb.AppendLine(lineSplit[i]); - } - if (sb.Length >= 5000) - { - sb.AppendLine("----Omit Message(message size is too long)----"); - break; - } - } - var str = sb.ToString(); - - status = new Status(StatusCode.Unknown, str); - return true; - } - - status = default; - return false; - } } diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs new file mode 100644 index 000000000..c0cabaf8b --- /dev/null +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs @@ -0,0 +1,379 @@ +using Grpc.AspNetCore.Server.Model; +using Grpc.Core; +using MagicOnion.Internal; +using MagicOnion.Serialization; +using MagicOnion.Server.Diagnostics; +using MagicOnion.Server.Filters; +using MagicOnion.Server.Filters.Internal; +using MagicOnion.Server.Internal; +using MessagePack; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; + +namespace MagicOnion.Server.Binder.Internal; + +internal class MagicOnionGrpcMethodHandler where TService : class +{ + readonly IList globalFilters; + readonly IServiceProvider serviceProvider; + readonly ILogger logger; + + readonly bool enableCurrentContext; + readonly bool isReturnExceptionStackTraceInErrorDetail; + + public MagicOnionGrpcMethodHandler(bool enableCurrentContext, bool isReturnExceptionStackTraceInErrorDetail, IServiceProvider serviceProvider, IList globalFilters, ILogger> logger) + { + this.enableCurrentContext = enableCurrentContext; + this.isReturnExceptionStackTraceInErrorDetail = isReturnExceptionStackTraceInErrorDetail; + this.serviceProvider = serviceProvider; + this.globalFilters = globalFilters; + this.logger = logger; + } + + void InitializeServiceProperties(object instance, ServiceContext serviceContext) + { + var service = ((IServiceBase)instance); + service.Context = serviceContext; + service.Metrics = serviceProvider.GetRequiredService(); + } + + public ClientStreamingServerMethod BuildClientStreamingMethod( + MagicOnionClientStreamingMethod method, + IMagicOnionSerializer messageSerializer, + IList metadata + ) + where TRawRequest : class + where TRawResponse : class + { + var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); + var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); + var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext)); + + return InvokeAsync; + + async Task InvokeAsync(TService instance, IAsyncStreamReader rawRequestStream, ServerCallContext context) + { + var isCompletedSuccessfully = false; + var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); + + var requestStream = new MagicOnionAsyncStreamReader(rawRequestStream); + var serviceContext = new StreamingServiceContext( + instance, + method, + attributeLookup, + context, + messageSerializer, + logger, + context.GetHttpContext().RequestServices, + requestStream, + default + ); + + InitializeServiceProperties(instance, serviceContext); + + TResponse response; + try + { + using (rawRequestStream as IDisposable) + { + MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(Nil)); + if (enableCurrentContext) + { + ServiceContext.currentServiceContext.Value = serviceContext; + } + await wrappedBody(serviceContext); + response = serviceContext.Result is TResponse r ? r : default!; + isCompletedSuccessfully = true; + } + } + catch (ReturnStatusException ex) + { + context.Status = ex.ToStatus(); + response = default!; + } + catch (Exception ex) + { + if (TryResolveStatus(ex, out var status)) + { + context.Status = status.Value; + MagicOnionServerLog.Error(logger, ex, context); + response = default!; + } + else + { + throw; + } + } + finally + { + MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); + } + + return GrpcMethodHelper.ToRaw(response); + } + } + + public ServerStreamingServerMethod BuildServerStreamingMethod( + MagicOnionServerStreamingMethod method, + IMagicOnionSerializer messageSerializer, + IList metadata + ) + where TRawRequest : class + where TRawResponse : class + { + var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); + var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); + var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, (TRequest)serviceContext.Request!, serviceContext)); + + return InvokeAsync; + + async Task InvokeAsync(TService instance, TRawRequest rawRequest, IServerStreamWriter rawResponseStream, ServerCallContext context) + { + var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); + var isCompletedSuccessfully = false; + + var request = GrpcMethodHelper.FromRaw(rawRequest); + var responseStream = new MagicOnionServerStreamWriter(rawResponseStream); + var serviceContext = new StreamingServiceContext( + instance, + method, + attributeLookup, + context, + messageSerializer, + logger, + context.GetHttpContext().RequestServices, + default, + responseStream + ); + + serviceContext.SetRawRequest(request); + + InitializeServiceProperties(instance, serviceContext); + + try + { + MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(Nil)); + if (enableCurrentContext) + { + ServiceContext.currentServiceContext.Value = serviceContext; + } + await wrappedBody(serviceContext); + isCompletedSuccessfully = true; + } + catch (ReturnStatusException ex) + { + context.Status = ex.ToStatus(); + } + catch (Exception ex) + { + if (TryResolveStatus(ex, out var status)) + { + context.Status = status.Value; + MagicOnionServerLog.Error(logger, ex, context); + } + else + { + throw; + } + } + finally + { + MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); + } + } + } + + public DuplexStreamingServerMethod BuildDuplexStreamingMethod( + MagicOnionDuplexStreamingMethod method, + IMagicOnionSerializer messageSerializer, + IList metadata + ) + where TRawRequest : class + where TRawResponse : class + { + var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); + var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); + var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext)); + + return InvokeAsync; + + async Task InvokeAsync(TService instance, IAsyncStreamReader rawRequestStream, IServerStreamWriter rawResponseStream, ServerCallContext context) + { + var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); + var isCompletedSuccessfully = false; + + var requestStream = new MagicOnionAsyncStreamReader(rawRequestStream); + var responseStream = new MagicOnionServerStreamWriter(rawResponseStream); + var serviceContext = new StreamingServiceContext( + instance, + method, + attributeLookup, + context, + messageSerializer, + logger, + context.GetHttpContext().RequestServices, + requestStream, + responseStream + ); + + InitializeServiceProperties(instance, serviceContext); + + try + { + MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(Nil)); + if (enableCurrentContext) + { + ServiceContext.currentServiceContext.Value = serviceContext; + } + + using (rawRequestStream as IDisposable) + { + await wrappedBody(serviceContext); + } + + isCompletedSuccessfully = true; + } + catch (ReturnStatusException ex) + { + context.Status = ex.ToStatus(); + } + catch (Exception ex) + { + if (TryResolveStatus(ex, out var status)) + { + context.Status = status.Value; + MagicOnionServerLog.Error(logger, ex, context); + } + else + { + throw; + } + } + finally + { + MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); + } + } + } + + public UnaryServerMethod BuildUnaryMethod( + IMagicOnionUnaryMethod method, + IMagicOnionSerializer messageSerializer, + IList metadata + ) + where TRawRequest : class + where TRawResponse : class + { + var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); + var filters = FilterHelper.GetFilters(globalFilters, method.ServiceType, method.MethodInfo); + var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext, (TRequest)serviceContext.Request!)); + + return InvokeAsync; + + async Task InvokeAsync(TService instance, TRawRequest requestRaw, ServerCallContext context) + { + var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); + var isCompletedSuccessfully = false; + + var serviceContext = new ServiceContext(instance, method, attributeLookup, context, messageSerializer, logger, context.GetHttpContext().RequestServices); + var request = GrpcMethodHelper.FromRaw(requestRaw); + + serviceContext.SetRawRequest(request); + + InitializeServiceProperties(instance, serviceContext); + + TResponse response = default!; + try + { + MagicOnionServerLog.BeginInvokeMethod(logger, serviceContext, typeof(TRequest)); + + if (enableCurrentContext) + { + ServiceContext.currentServiceContext.Value = serviceContext; + } + + await wrappedBody(serviceContext); + + isCompletedSuccessfully = true; + + if (serviceContext.Result is {} result) + { + if (result is RawBytesBox rawBytesResponse) + { + return Unsafe.As(ref rawBytesResponse); // NOTE: To disguise an object as a `TRawResponse`, `TRawResponse` must be `class`. + } + + response = (TResponse)result; + } + } + catch (ReturnStatusException ex) + { + context.Status = ex.ToStatus(); + + // WORKAROUND: Grpc.AspNetCore.Server throws a `Cancelled` status exception when it receives `null` response. + // To return the status code correctly, we need to rethrow the exception here. + // https://github.com/grpc/grpc-dotnet/blob/d4ee8babcd90666fc0727163a06527ab9fd7366a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs#L50-L56 + var rpcException = new RpcException(ex.ToStatus()); + if (ex.StackTrace is not null) + { + ExceptionDispatchInfo.SetRemoteStackTrace(rpcException, ex.StackTrace); + } + throw rpcException; + } + catch (Exception ex) + { + if (TryResolveStatus(ex, out var status)) + { + context.Status = status.Value; + MagicOnionServerLog.Error(logger, ex, context); + } + else + { + throw; + } + } + finally + { + MagicOnionServerLog.EndInvokeMethod(logger, serviceContext, typeof(TResponse), TimeProvider.System.GetElapsedTime(requestBeginTimestamp).TotalMilliseconds, !isCompletedSuccessfully); + } + + return GrpcMethodHelper.ToRaw(response); + } + } + + bool TryResolveStatus(Exception ex, [NotNullWhen(true)] out Status? status) + { + if (isReturnExceptionStackTraceInErrorDetail) + { + // Trim data. + var msg = ex.ToString(); + var lineSplit = msg.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + var sb = new System.Text.StringBuilder(); + for (int i = 0; i < lineSplit.Length; i++) + { + if (!(lineSplit[i].Contains("System.Runtime.CompilerServices") + || lineSplit[i].Contains("直前に例外がスローされた場所からのスタック トレースの終わり") + || lineSplit[i].Contains("End of stack trace from the previous location where the exception was thrown") + )) + { + sb.AppendLine(lineSplit[i]); + } + if (sb.Length >= 5000) + { + sb.AppendLine("----Omit Message(message size is too long)----"); + break; + } + } + var str = sb.ToString(); + + status = new Status(StatusCode.Unknown, str); + return true; + } + + status = default; + return false; + } +} diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs index 12b93aa72..299cef608 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs @@ -29,8 +29,7 @@ public void OnServiceMethodDiscovery(ServiceMethodProviderContext cont { if (!typeof(TService).IsAssignableTo(typeof(IServiceMarker))) return; - var methodBinderLogger = loggerFactory.CreateLogger>(); - var binder = new MagicOnionGrpcMethodBinder(context, options, serviceProvider, methodBinderLogger); + var binder = new MagicOnionGrpcMethodBinder(context, options, loggerFactory, serviceProvider); var registered = false; foreach (var methodProvider in methodProviders.OrderBy(x => x is DynamicMagicOnionMethodProvider ? 1 : 0)) // DynamicMagicOnionMethodProvider is always last. diff --git a/src/MagicOnion.Server/Binder/Legacy/MagicOnionService.cs b/src/MagicOnion.Server/Binder/Legacy/MagicOnionService.cs deleted file mode 100644 index 9c12c5fc8..000000000 --- a/src/MagicOnion.Server/Binder/Legacy/MagicOnionService.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace MagicOnion.Server.Binder; - -// The MagicOnion service methods are bound by `MagicOnionServiceMethodProvider` -internal class MagicOnionService; diff --git a/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceBinder.cs b/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceBinder.cs deleted file mode 100644 index e893152dc..000000000 --- a/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceBinder.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; -using Grpc.AspNetCore.Server.Model; -using Grpc.Core; -using MagicOnion.Internal; -using MessagePack; -using Microsoft.AspNetCore.Routing; - -namespace MagicOnion.Server.Binder; - -internal record MagicOnionMethodBindingContext(IMagicOnionServiceBinder Binder, MethodHandler MethodHandler); - -internal interface IMagicOnionServiceBinder -{ - void BindUnary(MagicOnionMethodBindingContext ctx, Func> serverMethod) - where TRawRequest : class - where TRawResponse : class; - - void BindUnaryParameterless(MagicOnionMethodBindingContext ctx, Func> serverMethod) - where TRawRequest : class - where TRawResponse : class; - - void BindStreamingHub(MagicOnionMethodBindingContext ctx, Func, IServerStreamWriter, ServerCallContext, ValueTask> serverMethod) - where TRawRequest : class - where TRawResponse : class; - - void BindDuplexStreaming(MagicOnionMethodBindingContext ctx, Func, IServerStreamWriter, ServerCallContext, ValueTask> serverMethod) - where TRawRequest : class - where TRawResponse : class; - - void BindServerStreaming(MagicOnionMethodBindingContext ctx, Func, ServerCallContext, ValueTask> serverMethod) - where TRawRequest : class - where TRawResponse : class; - - void BindClientStreaming(MagicOnionMethodBindingContext ctx, Func, ServerCallContext, ValueTask> serverMethod) - where TRawRequest : class - where TRawResponse : class; -} - -internal class MagicOnionServiceBinder : IMagicOnionServiceBinder - where TService : class -{ - readonly ServiceMethodProviderContext context; - - public MagicOnionServiceBinder(ServiceMethodProviderContext context) - { - this.context = context; - } - - IList GetMetadataFromHandler(MethodHandler methodHandler) - { - var serviceType = methodHandler.ServiceType; - - // NOTE: We need to collect Attributes for Endpoint metadata. ([Authorize], [AllowAnonymous] ...) - // https://github.com/grpc/grpc-dotnet/blob/7ef184f3c4cd62fbc3cde55e4bb3e16b58258ca1/src/Grpc.AspNetCore.Server/Model/Internal/ProviderServiceBinder.cs#L89-L98 - var metadata = new List(); - metadata.AddRange(serviceType.GetCustomAttributes(inherit: true)); - metadata.AddRange(methodHandler.MethodInfo.GetCustomAttributes(inherit: true)); - - metadata.Add(new HttpMethodMetadata(["POST"], acceptCorsPreflight: true)); - return metadata; - } - - - public void BindUnary(MagicOnionMethodBindingContext ctx, Func> serverMethod) - where TRawRequest : class - where TRawResponse : class - { - var method = GrpcMethodHelper.CreateMethod(MethodType.Unary, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MessageSerializer); - UnaryServerMethod invoker = async (_, request, context) => - { - var response = await serverMethod(GrpcMethodHelper.FromRaw(request), context); - if (response is RawBytesBox rawBytesResponse) - { - return Unsafe.As(ref rawBytesResponse); // NOTE: To disguise an object as a `TRawResponse`, `TRawResponse` must be `class`. - } - - return GrpcMethodHelper.ToRaw((TResponse?)response!); - }; - - context.AddUnaryMethod(method, GetMetadataFromHandler(ctx.MethodHandler), invoker); - } - - public void BindUnaryParameterless(MagicOnionMethodBindingContext ctx, Func> serverMethod) - where TRawRequest : class - where TRawResponse : class - { - // WORKAROUND: Prior to MagicOnion 5.0, the request type for the parameter-less method was byte[]. - // DynamicClient sends byte[], but GeneratedClient sends Nil, which is incompatible, - // so as a special case we do not serialize/deserialize and always convert to a fixed values. - var method = GrpcMethodHelper.CreateMethod(MethodType.Unary, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MessageSerializer); - UnaryServerMethod, TRawResponse> invoker = async (_, request, context) => - { - var response = await serverMethod(GrpcMethodHelper.FromRaw, Nil>(request), context); - if (response is RawBytesBox rawBytesResponse) - { - return Unsafe.As(ref rawBytesResponse); // NOTE: To disguise an object as a `TRawResponse`, `TRawResponse` must be `class`. - } - - return GrpcMethodHelper.ToRaw((TResponse?)response!); - }; - - context.AddUnaryMethod(method, GetMetadataFromHandler(ctx.MethodHandler), invoker); - } - - public void BindStreamingHub(MagicOnionMethodBindingContext ctx, Func, IServerStreamWriter, ServerCallContext, ValueTask> serverMethod) - where TRawRequest : class - where TRawResponse : class - { - Debug.Assert(typeof(TRequest) == typeof(StreamingHubPayload)); - Debug.Assert(typeof(TResponse) == typeof(StreamingHubPayload)); - // StreamingHub uses the special marshallers for streaming messages serialization. - // TODO: Currently, MagicOnion expects TRawRequest/TRawResponse to be raw-byte array (`StreamingHubPayload`). - var method = new Method( - MethodType.DuplexStreaming, - ctx.MethodHandler.ServiceName, - ctx.MethodHandler.MethodName, - (Marshaller)(object)MagicOnionMarshallers.StreamingHubMarshaller, - (Marshaller)(object)MagicOnionMarshallers.StreamingHubMarshaller - ); - DuplexStreamingServerMethod invoker = async (_, request, response, context) => await serverMethod( - new MagicOnionAsyncStreamReader(request), - new MagicOnionServerStreamWriter(response), - context - ); - - context.AddDuplexStreamingMethod(method, GetMetadataFromHandler(ctx.MethodHandler), invoker); - } - - public void BindDuplexStreaming(MagicOnionMethodBindingContext ctx, Func, IServerStreamWriter, ServerCallContext, ValueTask> serverMethod) - where TRawRequest : class - where TRawResponse : class - { - var method = GrpcMethodHelper.CreateMethod(MethodType.DuplexStreaming, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MessageSerializer); - DuplexStreamingServerMethod invoker = async (_, request, response, context) => await serverMethod( - new MagicOnionAsyncStreamReader(request), - new MagicOnionServerStreamWriter(response), - context - ); - - context.AddDuplexStreamingMethod(method, GetMetadataFromHandler(ctx.MethodHandler), invoker); - } - - public void BindServerStreaming(MagicOnionMethodBindingContext ctx, Func, ServerCallContext, ValueTask> serverMethod) - where TRawRequest : class - where TRawResponse : class - { - var method = GrpcMethodHelper.CreateMethod(MethodType.ServerStreaming, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MessageSerializer); - ServerStreamingServerMethod invoker = async (_, request, response, context) => await serverMethod( - GrpcMethodHelper.FromRaw(request), - new MagicOnionServerStreamWriter(response), - context - ); - - context.AddServerStreamingMethod(method, GetMetadataFromHandler(ctx.MethodHandler), invoker); - } - - public void BindClientStreaming(MagicOnionMethodBindingContext ctx, Func, ServerCallContext, ValueTask> serverMethod) - where TRawRequest : class - where TRawResponse : class - { - var method = GrpcMethodHelper.CreateMethod(MethodType.ClientStreaming, ctx.MethodHandler.ServiceName, ctx.MethodHandler.MethodName, ctx.MethodHandler.MessageSerializer); - ClientStreamingServerMethod invoker = async (_, request, context) => GrpcMethodHelper.ToRaw((await serverMethod( - new MagicOnionAsyncStreamReader(request), - context - ))!); - - context.AddClientStreamingMethod(method, GetMetadataFromHandler(ctx.MethodHandler), invoker); - } -} diff --git a/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceMethodProvider.cs b/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceMethodProvider.cs deleted file mode 100644 index 48f51e7c5..000000000 --- a/src/MagicOnion.Server/Binder/Legacy/MagicOnionServiceMethodProvider.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Grpc.AspNetCore.Server.Model; - -namespace MagicOnion.Server.Binder; - -[Obsolete] -internal class MagicOnionServiceMethodProvider : IServiceMethodProvider - where TService : class -{ - readonly MagicOnionServiceDefinition magicOnionServiceDefinition; - - public MagicOnionServiceMethodProvider(MagicOnionServiceDefinition magicOnionServerServiceDefinition) - { - magicOnionServiceDefinition = magicOnionServerServiceDefinition ?? throw new ArgumentNullException(nameof(magicOnionServerServiceDefinition)); - } - - public void OnServiceMethodDiscovery(ServiceMethodProviderContext context) - { - var binder = new MagicOnionServiceBinder(context); - foreach (var methodHandler in magicOnionServiceDefinition.MethodHandlers) - { - methodHandler.BindHandler(binder); - } - } -} diff --git a/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs index 69129b417..3cfc9fdaf 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Grpc.Core; namespace MagicOnion.Server.Binder; @@ -10,6 +11,8 @@ public class MagicOnionClientStreamingMethod>> invoker; + public MethodType MethodType => MethodType.ClientStreaming; + public Type ServiceType => typeof(TService); public string ServiceName { get; } public string MethodName { get; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs index 7e214f147..09bdf63af 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Grpc.Core; namespace MagicOnion.Server.Binder; @@ -10,6 +11,8 @@ public class MagicOnionDuplexStreamingMethod invoker; + public MethodType MethodType => MethodType.DuplexStreaming; + public Type ServiceType => typeof(TService); public string ServiceName { get; } public string MethodName { get; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs index eef669022..ddd02f1a7 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Grpc.Core; namespace MagicOnion.Server.Binder; @@ -10,6 +11,8 @@ public class MagicOnionServerStreamingMethod invoker; + public MethodType MethodType => MethodType.ServerStreaming; + public Type ServiceType => typeof(TService); public string ServiceName { get; } public string MethodName { get; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs index 6f48daa25..a816617d8 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs @@ -1,10 +1,13 @@ using System.Reflection; +using Grpc.Core; using MagicOnion.Server.Internal; namespace MagicOnion.Server.Binder; public class MagicOnionStreamingHubConnectMethod : IMagicOnionGrpcMethod where TService : class { + public MethodType MethodType => MethodType.DuplexStreaming; + public Type ServiceType => typeof(TService); public string ServiceName { get; } public string MethodName { get; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs index e8f913e66..06796d667 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs @@ -1,5 +1,7 @@ using System.Reflection; +using Grpc.Core; using MagicOnion.Internal; +using MessagePack; namespace MagicOnion.Server.Binder; @@ -17,6 +19,8 @@ public abstract class MagicOnionUnaryMethodBase MethodType.Unary; + public Type ServiceType => typeof(TService); public string ServiceName => serviceName; public string MethodName => methodName; @@ -46,3 +50,101 @@ public sealed class MagicOnionUnaryMethod(strin public override ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) => MethodHandlerResultHelper.SetUnaryResultNonGeneric(invoker(service, context, request), context); } + +internal class MethodHandlerResultHelper +{ + static readonly object BoxedNil = Nil.Default; + + public static ValueTask NewEmptyValueTask(T result) + => default; + + public static ValueTask TaskToEmptyValueTask(Task result) + => new(result); + + public static ValueTask SetUnaryResultNonGeneric(UnaryResult result, ServiceContext context) + { + if (result.hasRawValue) + { + if (result.rawTaskValue is { IsCompletedSuccessfully: true }) + { + return Await(result.rawTaskValue, context); + } + context.Result = BoxedNil; + } + + return default; + + static async ValueTask Await(Task task, ServiceContext context) + { + await task.ConfigureAwait(false); + context.Result = BoxedNil; + } + } + + public static ValueTask SetUnaryResult(UnaryResult result, ServiceContext context) + { + if (result.hasRawValue) + { + if (result.rawTaskValue is { } task) + { + if (task.IsCompletedSuccessfully) + { + context.Result = task.Result; + } + else + { + return Await(task, context); + } + } + else + { + context.Result = result.rawValue; + } + } + + return default; + + static async ValueTask Await(Task task, ServiceContext context) + { + context.Result = await task.ConfigureAwait(false); + } + } + + public static async ValueTask SetTaskUnaryResult(Task> taskResult, ServiceContext context) + { + var result = await taskResult.ConfigureAwait(false); + if (result.hasRawValue) + { + context.Result = (result.rawTaskValue != null) ? await result.rawTaskValue.ConfigureAwait(false) : result.rawValue; + } + } + + public static ValueTask SerializeClientStreamingResult(ClientStreamingResult result, ServiceContext context) + => SerializeValueTaskClientStreamingResult(new ValueTask>(result), context); + + public static ValueTask SerializeTaskClientStreamingResult(Task> taskResult, ServiceContext context) + => SerializeValueTaskClientStreamingResult(new ValueTask>(taskResult), context); + + public static ValueTask SerializeValueTaskClientStreamingResult(ValueTask> taskResult, ServiceContext context) + { + if (taskResult.IsCompletedSuccessfully) + { + if (taskResult.Result.hasRawValue) + { + context.Result = taskResult.Result.rawValue; + return default; + } + } + + return Await(taskResult, context); + + static async ValueTask Await(ValueTask> taskResult, ServiceContext context) + { + var result = await taskResult.ConfigureAwait(false); + if (result.hasRawValue) + { + context.Result = result.rawValue; + } + } + } +} diff --git a/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs b/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs index afa9907e5..1cd637a05 100644 --- a/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs +++ b/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs @@ -6,14 +6,12 @@ namespace Microsoft.AspNetCore.Builder; public static class MagicOnionEndpointRouteBuilderExtensions { - public static GrpcServiceEndpointConventionBuilder MapMagicOnionService(this IEndpointRouteBuilder builder) + public static void MapMagicOnionService(this IEndpointRouteBuilder builder) { var context = new MagicOnionGrpcServiceRegistrationContext(builder); foreach (var methodProvider in builder.ServiceProvider.GetRequiredService>()) { methodProvider.OnRegisterGrpcServices(context); } - - return builder.MapGrpcService(); } } diff --git a/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs index 755141ab0..29feab231 100644 --- a/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs +++ b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs @@ -53,8 +53,6 @@ internal static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollecti services.TryAddSingleton(typeof(StreamingHubRegistry<>)); services.AddSingleton(typeof(IServiceMethodProvider<>), typeof(MagicOnionGrpcServiceMethodProvider<>)); services.AddSingleton(); - services.TryAddSingleton(); // Legacy - services.TryAddSingleton, MagicOnionServiceMethodProvider>(); // Legacy // MagicOnion: Metrics services.TryAddSingleton(); diff --git a/src/MagicOnion.Server/Hubs/StreamingHub.cs b/src/MagicOnion.Server/Hubs/StreamingHub.cs index 92a5c7044..fc7fb3fb8 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHub.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHub.cs @@ -81,11 +81,7 @@ protected virtual ValueTask OnDisconnected() return CompletedTask; } - Task> IStreamingHubBase.Connect() - => Connect(); - - [Obsolete] - internal async Task> Connect() + async Task> IStreamingHubBase.Connect() { Metrics.StreamingHubConnectionIncrement(Context.Metrics, Context.ServiceName); diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs b/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs deleted file mode 100644 index 48b4e6aad..000000000 --- a/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs +++ /dev/null @@ -1,88 +0,0 @@ -#if NET8_0_OR_GREATER -using System.Collections.Frozen; -#endif -using Cysharp.Runtime.Multicast; -using MagicOnion.Server.Internal; - -namespace MagicOnion.Server.Hubs; - -// Global cache of Streaming Handler -[Obsolete] -internal class StreamingHubHandlerRepository -{ - bool frozen; - IDictionary> handlersCache = new Dictionary>(MethodHandler.UniqueEqualityComparer.Instance); - IDictionary groupCache = new Dictionary(MethodHandler.UniqueEqualityComparer.Instance); - IDictionary heartbeats = new Dictionary(MethodHandler.UniqueEqualityComparer.Instance); - - public void RegisterHandler(MethodHandler parent, StreamingHubHandler[] hubHandlers) - { - ThrowIfFrozen(); - - var handlers = VerifyDuplicate(hubHandlers); - var hashDict = new UniqueHashDictionary(handlers); - - handlersCache.Add(parent, hashDict); - } - - public UniqueHashDictionary GetHandlers(MethodHandler parent) - => handlersCache[parent]; - - - public void Freeze() - { - ThrowIfFrozen(); - frozen = true; - -#if NET8_0_OR_GREATER - handlersCache = handlersCache.ToFrozenDictionary(MethodHandler.UniqueEqualityComparer.Instance); - groupCache = groupCache.ToFrozenDictionary(MethodHandler.UniqueEqualityComparer.Instance); - heartbeats = heartbeats.ToFrozenDictionary(MethodHandler.UniqueEqualityComparer.Instance); -#endif - } - - void ThrowIfFrozen() - { - if (frozen) throw new InvalidOperationException($"Cannot modify the {nameof(StreamingHubHandlerRepository)}. The instance is already frozen."); - } - - public void RegisterGroupProvider(MethodHandler methodHandler, IMulticastGroupProvider groupProvider) - { - ThrowIfFrozen(); - groupCache[methodHandler] = new MagicOnionManagedGroupProvider(groupProvider); - } - - public MagicOnionManagedGroupProvider GetGroupProvider(MethodHandler methodHandler) - { - return groupCache[methodHandler]; - } - - public void RegisterHeartbeatManager(MethodHandler methodHandler, IStreamingHubHeartbeatManager heartbeatManager) - { - ThrowIfFrozen(); - heartbeats[methodHandler] = heartbeatManager; - } - - public IStreamingHubHeartbeatManager GetHeartbeatManager(MethodHandler methodHandler) - { - return heartbeats[methodHandler]; - } - - static (int, StreamingHubHandler)[] VerifyDuplicate(StreamingHubHandler[] hubHandlers) - { - var list = new List<(int, StreamingHubHandler)>(); - var map = new Dictionary(); - foreach (var item in hubHandlers) - { - var hash = item.MethodId; - if (map.ContainsKey(hash)) - { - throw new InvalidOperationException($"StreamingHubHandler.MethodName found duplicate hashCode name. Please rename or use [MethodId] to avoid conflict. {map[hash]} and {item.MethodInfo.Name}"); - } - map.Add(hash, item); - list.Add((hash, item)); - } - - return list.ToArray(); - } -} diff --git a/src/MagicOnion.Server/MagicOnionEngine.cs b/src/MagicOnion.Server/MagicOnionEngine.cs index a1cb76603..f3a11892c 100644 --- a/src/MagicOnion.Server/MagicOnionEngine.cs +++ b/src/MagicOnion.Server/MagicOnionEngine.cs @@ -113,9 +113,6 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP /// The options for MagicOnion server public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceProvider serviceProvider, IEnumerable targetTypes, MagicOnionOptions options) { - var handlers = new HashSet(); - var streamingHubHandlers = new List(); - var loggerFactory = serviceProvider.GetRequiredService(); var loggerMagicOnionEngine = loggerFactory.CreateLogger(LoggerNameMagicOnionEngine); @@ -123,7 +120,7 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP var sw = Stopwatch.StartNew(); - var result = new MagicOnionServiceDefinition(handlers.ToArray(), streamingHubHandlers.ToArray(), targetTypes.ToArray()); + var result = new MagicOnionServiceDefinition(targetTypes.ToArray()); sw.Stop(); MagicOnionServerLog.EndBuildServiceDefinition(loggerMagicOnionEngine, sw.Elapsed.TotalMilliseconds); diff --git a/src/MagicOnion.Server/MagicOnionServiceDefinition.cs b/src/MagicOnion.Server/MagicOnionServiceDefinition.cs index 8a4fa1ef7..e39f2ffa2 100644 --- a/src/MagicOnion.Server/MagicOnionServiceDefinition.cs +++ b/src/MagicOnion.Server/MagicOnionServiceDefinition.cs @@ -4,14 +4,10 @@ namespace MagicOnion.Server; public class MagicOnionServiceDefinition { - public IReadOnlyList MethodHandlers { get; } - public IReadOnlyList StreamingHubHandlers { get; } public IReadOnlyList TargetTypes { get; } - public MagicOnionServiceDefinition(IReadOnlyList handlers, IReadOnlyList streamingHubHandlers, IReadOnlyList targetTypes) + public MagicOnionServiceDefinition(IReadOnlyList targetTypes) { - this.MethodHandlers = handlers; - this.StreamingHubHandlers = streamingHubHandlers; this.TargetTypes = targetTypes; } } diff --git a/src/MagicOnion.Server/MethodHandler.cs b/src/MagicOnion.Server/MethodHandler.cs deleted file mode 100644 index 8f4f526e3..000000000 --- a/src/MagicOnion.Server/MethodHandler.cs +++ /dev/null @@ -1,635 +0,0 @@ -using Grpc.Core; -using MessagePack; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.ExceptionServices; -using MagicOnion.Internal; -using MagicOnion.Server.Binder; -using MagicOnion.Server.Diagnostics; -using MagicOnion.Server.Filters; -using MagicOnion.Server.Filters.Internal; -using MagicOnion.Server.Internal; -using MagicOnion.Serialization; -using Microsoft.Extensions.Logging; - -namespace MagicOnion.Server; - -[Obsolete] -public class MethodHandler : IEquatable -{ - // reflection cache - static readonly MethodInfo Helper_CreateService = typeof(ServiceProviderHelper).GetMethod(nameof(ServiceProviderHelper.CreateService), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!; - static readonly MethodInfo Helper_TaskToEmptyValueTask = typeof(MethodHandlerResultHelper).GetMethod(nameof(MethodHandlerResultHelper.TaskToEmptyValueTask), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!; - static readonly MethodInfo Helper_NewEmptyValueTask = typeof(MethodHandlerResultHelper).GetMethod(nameof(MethodHandlerResultHelper.NewEmptyValueTask), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!; - static readonly MethodInfo Helper_SetTaskUnaryResult = typeof(MethodHandlerResultHelper).GetMethod(nameof(MethodHandlerResultHelper.SetTaskUnaryResult), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!; - static readonly MethodInfo Helper_SetUnaryResult = typeof(MethodHandlerResultHelper).GetMethod(nameof(MethodHandlerResultHelper.SetUnaryResult), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!; - static readonly MethodInfo Helper_SetUnaryResultNonGeneric = typeof(MethodHandlerResultHelper).GetMethod(nameof(MethodHandlerResultHelper.SetUnaryResultNonGeneric), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!; - static readonly MethodInfo Helper_SerializeTaskClientStreamingResult = typeof(MethodHandlerResultHelper).GetMethod(nameof(MethodHandlerResultHelper.SerializeTaskClientStreamingResult), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!; - static readonly MethodInfo Helper_SerializeClientStreamingResult = typeof(MethodHandlerResultHelper).GetMethod(nameof(MethodHandlerResultHelper.SerializeClientStreamingResult), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!; - static readonly PropertyInfo ServiceContext_Request = typeof(ServiceContext).GetProperty("Request", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - - static int methodHandlerIdBuild = 0; - - readonly int methodHandlerId; - readonly MethodHandlerMetadata metadata; - readonly bool isStreamingHub; - readonly IMagicOnionSerializer messageSerializer; - readonly Func methodBody; - - // options - readonly bool enableCurrentContext; - - internal ILogger Logger { get; } - internal bool IsReturnExceptionStackTraceInErrorDetail { get; } - - public IMagicOnionSerializer MessageSerializer => messageSerializer; - - public string ServiceName => metadata.ServiceInterface.Name; - public string MethodName { get; } - public Type ServiceType => metadata.ServiceImplementationType; - public MethodInfo MethodInfo => metadata.ServiceMethod; - public MethodType MethodType => metadata.MethodType; - - public ILookup AttributeLookup => metadata.AttributeLookup; - - // use for request handling. - public Type RequestType => metadata.RequestType; - public Type UnwrappedResponseType => metadata.ResponseType; - - public MethodHandler(Type classType, MethodInfo methodInfo, string methodName, MethodHandlerOptions handlerOptions, IServiceProvider serviceProvider, ILogger logger, bool isStreamingHub) - { - this.metadata = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(classType, methodInfo); - this.methodHandlerId = Interlocked.Increment(ref methodHandlerIdBuild); - this.isStreamingHub = isStreamingHub; - - this.MethodName = methodName; - this.messageSerializer = handlerOptions.MessageSerializer.Create(MethodType, methodInfo); - - // options - this.IsReturnExceptionStackTraceInErrorDetail = handlerOptions.IsReturnExceptionStackTraceInErrorDetail; - this.Logger = logger; - this.enableCurrentContext = handlerOptions.EnableCurrentContext; - - var parameters = metadata.Parameters; - var filters = FilterHelper.GetFilters(handlerOptions.GlobalFilters, classType, methodInfo); - - // prepare lambda parameters - var createServiceMethodInfo = Helper_CreateService.MakeGenericMethod(classType, metadata.ServiceInterface); - var contextArg = Expression.Parameter(typeof(ServiceContext), "context"); - var instance = Expression.Call(createServiceMethodInfo, contextArg); - - switch (MethodType) - { - case MethodType.Unary: - case MethodType.ServerStreaming: - // (ServiceContext context) => - // { - // var request = (TRequest)context.Request; - // var result = new FooService() { Context = context }.Bar(request.Item1, request.Item2); - // return MethodHandlerResultHelper.SetUnaryResult(result, context); - // }; - try - { - var requestArg = Expression.Parameter(RequestType, "request"); - var contextRequest = Expression.Property(contextArg, ServiceContext_Request); - var assignRequest = Expression.Assign(requestArg, Expression.Convert(contextRequest, RequestType)); - - Expression[] arguments = new Expression[parameters.Count]; - if (parameters.Count == 1) - { - arguments[0] = requestArg; - } - else - { - for (int i = 0; i < parameters.Count; i++) - { - arguments[i] = Expression.Field(requestArg, "Item" + (i + 1)); - } - } - - var callBody = Expression.Call(instance, methodInfo, arguments); - - if (MethodType == MethodType.ServerStreaming) - { - var finalMethod = (metadata.IsResultTypeTask) - ? Helper_TaskToEmptyValueTask.MakeGenericMethod(MethodInfo.ReturnType.GetGenericArguments()[0]) // Task> - : Helper_NewEmptyValueTask.MakeGenericMethod(MethodInfo.ReturnType); // ServerStreamingResult - callBody = Expression.Call(finalMethod, callBody); - } - else - { - var finalMethod = (metadata.IsResultTypeTask) - ? Helper_SetTaskUnaryResult.MakeGenericMethod(UnwrappedResponseType) - : metadata.ServiceMethod.ReturnType == typeof(UnaryResult) - ? Helper_SetUnaryResultNonGeneric - : Helper_SetUnaryResult.MakeGenericMethod(UnwrappedResponseType); - callBody = Expression.Call(finalMethod, callBody, contextArg); - } - - var body = Expression.Block(new[] { requestArg }, assignRequest, callBody); - var compiledBody = Expression.Lambda(body, contextArg).Compile(); - - this.methodBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (Func)compiledBody); - } - catch (Exception ex) - { - throw new InvalidOperationException($"Can't create handler. Path:{ToString()}", ex); - } - break; - case MethodType.ClientStreaming: - case MethodType.DuplexStreaming: - if (parameters.Count != 0) - { - throw new InvalidOperationException($"{MethodType} does not support method parameters. If you need to send initial parameter, use header instead. Path:{ToString()}"); - } - - // (ServiceContext context) => new FooService() { Context = context }.Bar(); - try - { - var callBody = Expression.Call(instance, methodInfo); - - if (MethodType == MethodType.ClientStreaming) - { - var finalMethod = (metadata.IsResultTypeTask) - ? Helper_SerializeTaskClientStreamingResult.MakeGenericMethod(RequestType, UnwrappedResponseType) - : Helper_SerializeClientStreamingResult.MakeGenericMethod(RequestType, UnwrappedResponseType); - callBody = Expression.Call(finalMethod, callBody, contextArg); - } - else - { - var finalMethod = (metadata.IsResultTypeTask) - ? Helper_TaskToEmptyValueTask.MakeGenericMethod(MethodInfo.ReturnType.GetGenericArguments()[0]) - : Helper_NewEmptyValueTask.MakeGenericMethod(MethodInfo.ReturnType); - callBody = Expression.Call(finalMethod, callBody); - } - - var compiledBody = Expression.Lambda(callBody, contextArg).Compile(); - - this.methodBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (Func)compiledBody); - } - catch (Exception ex) - { - throw new InvalidOperationException($"Can't create handler. Path:{ToString()}", ex); - } - break; - default: - throw new InvalidOperationException("Unknown MethodType:" + MethodType + $"Path:{ToString()}"); - } - } - - internal void BindHandler(IMagicOnionServiceBinder binder) - { - // NOTE: ServiceBinderBase.AddMethod has `class` generic constraint. - // We need to box an instance of the value type. - var rawRequestType = RequestType.IsValueType ? typeof(Box<>).MakeGenericType(RequestType) : RequestType; - var rawResponseType = UnwrappedResponseType.IsValueType ? typeof(Box<>).MakeGenericType(UnwrappedResponseType) : UnwrappedResponseType; - - typeof(MethodHandler) - .GetMethod(nameof(BindHandlerTyped), BindingFlags.Instance | BindingFlags.NonPublic)! - .MakeGenericMethod(RequestType, UnwrappedResponseType, rawRequestType, rawResponseType) - .Invoke(this, new [] { binder }); - } - - void BindHandlerTyped(IMagicOnionServiceBinder binder) - where TRawRequest : class - where TRawResponse : class - { - var bindingContext = new MagicOnionMethodBindingContext(binder, this); - switch (this.MethodType) - { - case MethodType.Unary: - if (this.MethodInfo.GetParameters().Any()) - { - binder.BindUnary(bindingContext, UnaryServerMethod); - } - else - { - binder.BindUnaryParameterless(bindingContext, UnaryServerMethod); - } - break; - case MethodType.ClientStreaming: - binder.BindClientStreaming(bindingContext, ClientStreamingServerMethod); - break; - case MethodType.ServerStreaming: - binder.BindServerStreaming(bindingContext, ServerStreamingServerMethod); - break; - case MethodType.DuplexStreaming: - if (isStreamingHub) - { - binder.BindStreamingHub(bindingContext, DuplexStreamingServerMethod); - } - else - { - binder.BindDuplexStreaming(bindingContext, DuplexStreamingServerMethod); - } - break; - default: - throw new InvalidOperationException("Unknown RegisterType:" + MethodType); - } - } - - async ValueTask UnaryServerMethod(TRequest request, ServerCallContext context) - { - var isErrorOrInterrupted = false; - var serviceContext = new ServiceContext(null!, ServiceType, ServiceName, MethodInfo, AttributeLookup, this.MethodType, context, messageSerializer, Logger, this, context.GetHttpContext().RequestServices); - serviceContext.SetRawRequest(request); - - object? response = default(TResponse?); - try - { - MagicOnionServerLog.BeginInvokeMethod(Logger, serviceContext, typeof(TRequest)); - if (enableCurrentContext) - { - ServiceContext.currentServiceContext.Value = serviceContext; - } - await this.methodBody(serviceContext).ConfigureAwait(false); - if (serviceContext.Result is not null) - { - response = serviceContext.Result; - } - } - catch (ReturnStatusException ex) - { - isErrorOrInterrupted = true; - context.Status = ex.ToStatus(); - response = default(TResponse?); - - // WORKAROUND: Grpc.AspNetCore.Server throws a `Cancelled` status exception when it receives `null` response. - // To return the status code correctly, we needs to rethrow the exception here. - // https://github.com/grpc/grpc-dotnet/blob/d4ee8babcd90666fc0727163a06527ab9fd7366a/src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs#L50-L56 - var rpcException = new RpcException(ex.ToStatus()); -#if NET6_0_OR_GREATER - if (ex.StackTrace is not null) - { - ExceptionDispatchInfo.SetRemoteStackTrace(rpcException, ex.StackTrace); - } -#endif - throw rpcException; - } - catch (Exception ex) - { - isErrorOrInterrupted = true; - if (IsReturnExceptionStackTraceInErrorDetail) - { - // Trim data. - var msg = ex.ToString(); - var lineSplit = msg.Split(new[] { Environment.NewLine }, StringSplitOptions.None); - var sb = new System.Text.StringBuilder(); - for (int i = 0; i < lineSplit.Length; i++) - { - if (!(lineSplit[i].Contains("System.Runtime.CompilerServices") - || lineSplit[i].Contains("直前に例外がスローされた場所からのスタック トレースの終わり") - || lineSplit[i].Contains("End of stack trace from the previous location where the exception was thrown") - )) - { - sb.AppendLine(lineSplit[i]); - } - if (sb.Length >= 5000) - { - sb.AppendLine("----Omit Message(message size is too long)----"); - break; - } - } - var str = sb.ToString(); - - context.Status = new Status(StatusCode.Unknown, str); - MagicOnionServerLog.Error(Logger, ex, context); - response = default(TResponse?); - } - else - { - throw; - } - } - finally - { - MagicOnionServerLog.EndInvokeMethod(Logger, serviceContext, typeof(TResponse), (DateTime.UtcNow - serviceContext.Timestamp).TotalMilliseconds, isErrorOrInterrupted); - } - - return response; - } - - async ValueTask ClientStreamingServerMethod(IAsyncStreamReader requestStream, ServerCallContext context) - { - var isErrorOrInterrupted = false; - var serviceContext = new StreamingServiceContext( - default!, - ServiceType, - ServiceName, - MethodInfo, - AttributeLookup, - this.MethodType, - context, - messageSerializer, - Logger, - this, - context.GetHttpContext().RequestServices, - requestStream, - default - ); - - TResponse? response; - try - { - using (requestStream as IDisposable) - { - MagicOnionServerLog.BeginInvokeMethod(Logger, serviceContext, typeof(Nil)); - if (enableCurrentContext) - { - ServiceContext.currentServiceContext.Value = serviceContext; - } - await this.methodBody(serviceContext).ConfigureAwait(false); - response = serviceContext.Result is TResponse r ? r : default; - } - } - catch (ReturnStatusException ex) - { - isErrorOrInterrupted = true; - context.Status = ex.ToStatus(); - response = default; - } - catch (Exception ex) - { - isErrorOrInterrupted = true; - if (IsReturnExceptionStackTraceInErrorDetail) - { - context.Status = new Status(StatusCode.Unknown, ex.ToString()); - MagicOnionServerLog.Error(Logger, ex, context); - response = default; - } - else - { - throw; - } - } - finally - { - MagicOnionServerLog.EndInvokeMethod(Logger, serviceContext, typeof(TResponse), (DateTime.UtcNow - serviceContext.Timestamp).TotalMilliseconds, isErrorOrInterrupted); - } - - return response; - } - - async ValueTask ServerStreamingServerMethod(TRequest request, IServerStreamWriter responseStream, ServerCallContext context) - { - var isErrorOrInterrupted = false; - var serviceContext = new StreamingServiceContext( - default!, - ServiceType, - ServiceName, - MethodInfo, - AttributeLookup, - this.MethodType, - context, - messageSerializer, - Logger, - this, - context.GetHttpContext().RequestServices, - default, - responseStream - ); - serviceContext.SetRawRequest(request); - try - { - MagicOnionServerLog.BeginInvokeMethod(Logger, serviceContext, typeof(TRequest)); - if (enableCurrentContext) - { - ServiceContext.currentServiceContext.Value = serviceContext; - } - await this.methodBody(serviceContext).ConfigureAwait(false); - return; - } - catch (ReturnStatusException ex) - { - isErrorOrInterrupted = true; - context.Status = ex.ToStatus(); - return; - } - catch (Exception ex) - { - isErrorOrInterrupted = true; - if (IsReturnExceptionStackTraceInErrorDetail) - { - context.Status = new Status(StatusCode.Unknown, ex.ToString()); - MagicOnionServerLog.Error(Logger, ex, context); - return; - } - else - { - throw; - } - } - finally - { - MagicOnionServerLog.EndInvokeMethod(Logger, serviceContext, typeof(Nil), (DateTime.UtcNow - serviceContext.Timestamp).TotalMilliseconds, isErrorOrInterrupted); - } - } - - async ValueTask DuplexStreamingServerMethod(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) - { - var isErrorOrInterrupted = false; - var serviceContext = new StreamingServiceContext( - default!, - ServiceType, - ServiceName, - MethodInfo, - AttributeLookup, - this.MethodType, - context, - messageSerializer, - Logger, - this, - context.GetHttpContext().RequestServices, - requestStream, - responseStream - ); - try - { - MagicOnionServerLog.BeginInvokeMethod(Logger, serviceContext, typeof(Nil)); - using (requestStream as IDisposable) - { - if (enableCurrentContext) - { - ServiceContext.currentServiceContext.Value = serviceContext; - } - await this.methodBody(serviceContext).ConfigureAwait(false); - - return; - } - } - catch (ReturnStatusException ex) - { - isErrorOrInterrupted = true; - context.Status = ex.ToStatus(); - return; - } - catch (Exception ex) - { - isErrorOrInterrupted = true; - if (IsReturnExceptionStackTraceInErrorDetail) - { - context.Status = new Status(StatusCode.Unknown, ex.ToString()); - MagicOnionServerLog.Error(Logger, ex, context); - return; - } - else - { - throw; - } - } - finally - { - MagicOnionServerLog.EndInvokeMethod(Logger, serviceContext, typeof(Nil), (DateTime.UtcNow - serviceContext.Timestamp).TotalMilliseconds, isErrorOrInterrupted); - } - } - - public override string ToString() - { - return ServiceName + "/" + MethodName; - } - - public override int GetHashCode() - { - return ServiceName.GetHashCode() ^ MethodInfo.Name.GetHashCode() << 2; - } - - public bool Equals(MethodHandler? other) - { - return other != null && ServiceName.Equals(other.ServiceName) && MethodInfo.Name.Equals(other.MethodInfo.Name); - } - - public class UniqueEqualityComparer : IEqualityComparer - { - public static UniqueEqualityComparer Instance { get; } = new(); - - public bool Equals(MethodHandler? x, MethodHandler? y) - { - return (x == null && y == null) || (x != null && y != null && x.methodHandlerId.Equals(y.methodHandlerId)); - } - - public int GetHashCode(MethodHandler obj) - { - return obj.methodHandlerId.GetHashCode(); - } - } -} - -/// -/// Options for MethodHandler construction. -/// -public class MethodHandlerOptions -{ - public IList GlobalFilters { get; } - - public bool IsReturnExceptionStackTraceInErrorDetail { get; } - - public bool EnableCurrentContext { get; } - - public IMagicOnionSerializerProvider MessageSerializer { get; } - - public MethodHandlerOptions(MagicOnionOptions options) - { - GlobalFilters = options.GlobalFilters; - IsReturnExceptionStackTraceInErrorDetail = options.IsReturnExceptionStackTraceInErrorDetail; - EnableCurrentContext = options.EnableCurrentContext; - MessageSerializer = options.MessageSerializer; - } -} - -internal class MethodHandlerResultHelper -{ - static readonly object BoxedNil = Nil.Default; - - public static ValueTask NewEmptyValueTask(T result) - => default; - - public static ValueTask TaskToEmptyValueTask(Task result) - => new(result); - - public static ValueTask SetUnaryResultNonGeneric(UnaryResult result, ServiceContext context) - { - if (result.hasRawValue) - { - if (result.rawTaskValue is { IsCompletedSuccessfully: true }) - { - return Await(result.rawTaskValue, context); - } - context.Result = BoxedNil; - } - - return default; - - static async ValueTask Await(Task task, ServiceContext context) - { - await task.ConfigureAwait(false); - context.Result = BoxedNil; - } - } - - public static ValueTask SetUnaryResult(UnaryResult result, ServiceContext context) - { - if (result.hasRawValue) - { - if (result.rawTaskValue is { } task) - { - if (task.IsCompletedSuccessfully) - { - context.Result = task.Result; - } - else - { - return Await(task, context); - } - } - else - { - context.Result = result.rawValue; - } - } - - return default; - - static async ValueTask Await(Task task, ServiceContext context) - { - context.Result = await task.ConfigureAwait(false); - } - } - - public static async ValueTask SetTaskUnaryResult(Task> taskResult, ServiceContext context) - { - var result = await taskResult.ConfigureAwait(false); - if (result.hasRawValue) - { - context.Result = (result.rawTaskValue != null) ? await result.rawTaskValue.ConfigureAwait(false) : result.rawValue; - } - } - - public static ValueTask SerializeClientStreamingResult(ClientStreamingResult result, ServiceContext context) - => SerializeValueTaskClientStreamingResult(new ValueTask>(result), context); - - public static ValueTask SerializeTaskClientStreamingResult(Task> taskResult, ServiceContext context) - => SerializeValueTaskClientStreamingResult(new ValueTask>(taskResult), context); - - public static ValueTask SerializeValueTaskClientStreamingResult(ValueTask> taskResult, ServiceContext context) - { - if (taskResult.IsCompletedSuccessfully) - { - if (taskResult.Result.hasRawValue) - { - context.Result = taskResult.Result.rawValue; - return default; - } - } - - return Await(taskResult, context); - - static async ValueTask Await(ValueTask> taskResult, ServiceContext context) - { - var result = await taskResult.ConfigureAwait(false); - if (result.hasRawValue) - { - context.Result = result.rawValue; - } - } - } -} diff --git a/src/MagicOnion.Server/ServiceContext.Streaming.cs b/src/MagicOnion.Server/ServiceContext.Streaming.cs index 714e5527f..8bfadcd4d 100644 --- a/src/MagicOnion.Server/ServiceContext.Streaming.cs +++ b/src/MagicOnion.Server/ServiceContext.Streaming.cs @@ -1,6 +1,7 @@ using System.Reflection; using Grpc.Core; using MagicOnion.Serialization; +using MagicOnion.Server.Binder; using MagicOnion.Server.Diagnostics; using MagicOnion.Server.Internal; using MessagePack; @@ -40,25 +41,21 @@ internal class StreamingServiceContext : ServiceContext, IS public StreamingServiceContext( object instance, - Type serviceType, - string serviceName, - MethodInfo methodInfo, + IMagicOnionGrpcMethod method, ILookup attributeLookup, - MethodType methodType, ServerCallContext context, IMagicOnionSerializer messageSerializer, ILogger logger, - MethodHandler methodHandler, IServiceProvider serviceProvider, IAsyncStreamReader? requestStream, IServerStreamWriter? responseStream - ) : base(instance, serviceType, serviceName, methodInfo, attributeLookup, methodType, context, messageSerializer, logger, methodHandler, serviceProvider) + ) : base(instance, method, attributeLookup, context, messageSerializer, logger, serviceProvider) { RequestStream = requestStream; ResponseStream = responseStream; // streaming hub - if (methodType == MethodType.DuplexStreaming) + if (MethodType == MethodType.DuplexStreaming) { this.streamingResponseWriter = new Lazy>(() => new QueuedResponseWriter(this)); } diff --git a/src/MagicOnion.Server/ServiceContext.cs b/src/MagicOnion.Server/ServiceContext.cs index afdeb6d34..899ac24a4 100644 --- a/src/MagicOnion.Server/ServiceContext.cs +++ b/src/MagicOnion.Server/ServiceContext.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Reflection; using MagicOnion.Internal; +using MagicOnion.Server.Binder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -63,17 +64,17 @@ public ConcurrentDictionary Items public DateTime Timestamp { get; } - public Type ServiceType { get; } + public Type ServiceType => Method.ServiceType; - public string ServiceName { get; } + public string ServiceName => Method.ServiceName; public string MethodName => MethodInfo.Name; - public MethodInfo MethodInfo { get; } + public MethodInfo MethodInfo => Method.MethodInfo; /// Cached Attributes both service and method. public ILookup AttributeLookup { get; } - public MethodType MethodType { get; } + public MethodType MethodType => Method.MethodType; /// Raw gRPC Context. public ServerCallContext CallContext { get; } @@ -86,35 +87,27 @@ public ConcurrentDictionary Items internal object? Request => request; internal object? Result { get; set; } internal ILogger Logger { get; } - internal MethodHandler MethodHandler { get; } + internal IMagicOnionGrpcMethod Method { get; } internal MetricsContext Metrics { get; } public ServiceContext( object instance, - Type serviceType, - string serviceName, - MethodInfo methodInfo, + IMagicOnionGrpcMethod method, ILookup attributeLookup, - MethodType methodType, ServerCallContext context, IMagicOnionSerializer messageSerializer, ILogger logger, - MethodHandler methodHandler, IServiceProvider serviceProvider ) { this.ContextId = Guid.NewGuid(); this.Instance = instance; - this.ServiceType = serviceType; - this.ServiceName = serviceName; - this.MethodInfo = methodInfo; this.AttributeLookup = attributeLookup; - this.MethodType = methodType; this.CallContext = context; this.Timestamp = DateTime.UtcNow; this.MessageSerializer = messageSerializer; this.Logger = logger; - this.MethodHandler = methodHandler; + this.Method = method; this.ServiceProvider = serviceProvider; this.Metrics = serviceProvider.GetRequiredService().CreateContext(); diff --git a/tests/MagicOnion.Server.Tests/MagicOnionEngineTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionEngineTest.cs index 8118e8058..72b7a6014 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionEngineTest.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionEngineTest.cs @@ -1,3 +1,4 @@ +#if FALSE using System.Reflection; using MagicOnion.Server.Diagnostics; using MagicOnion.Server.Hubs; @@ -349,3 +350,4 @@ public void ShouldIgnoreAssembly() MagicOnionEngine.ShouldIgnoreAssembly("MagicOnionSample").Should().BeFalse(); } } +#endif From e3f10f8e60363b72c4b57ff9ac5d30be98f6b169 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Tue, 15 Oct 2024 14:46:13 +0900 Subject: [PATCH 04/27] Refactor --- .../Binder/MagicOnionClientStreamingMethod.cs | 25 ++++- .../Binder/MagicOnionUnaryMethod.cs | 91 +++++-------------- 2 files changed, 46 insertions(+), 70 deletions(-) diff --git a/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs index 3cfc9fdaf..3ac9c1f22 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs @@ -49,5 +49,28 @@ public void Bind(IMagicOnionGrpcMethodBinder binder) => binder.BindClientStreaming(this); public ValueTask InvokeAsync(TService service, ServiceContext context) - => MethodHandlerResultHelper.SerializeValueTaskClientStreamingResult(invoker(service, context), context); + => SerializeValueTaskClientStreamingResult(invoker(service, context), context); + + static ValueTask SerializeValueTaskClientStreamingResult(ValueTask> taskResult, ServiceContext context) + { + if (taskResult.IsCompletedSuccessfully) + { + if (taskResult.Result.hasRawValue) + { + context.Result = taskResult.Result.rawValue; + return default; + } + } + + return Await(taskResult, context); + + static async ValueTask Await(ValueTask> taskResult, ServiceContext context) + { + var result = await taskResult.ConfigureAwait(false); + if (result.hasRawValue) + { + context.Result = result.rawValue; + } + } + } } diff --git a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs index 06796d667..761769d3b 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs @@ -19,6 +19,8 @@ public abstract class MagicOnionUnaryMethodBase MethodType.Unary; public Type ServiceType => typeof(TService); public string ServiceName => serviceName; @@ -30,38 +32,8 @@ public void Bind(IMagicOnionGrpcMethodBinder binder) => binder.BindUnary(this); public abstract ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request); -} - -public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func> invoker) - : MagicOnionUnaryMethodBase(serviceName, methodName) - where TService : class - where TRawRequest : class - where TRawResponse : class -{ - public override ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) - => MethodHandlerResultHelper.SetUnaryResult(invoker(service, context, request), context); -} - -public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func invoker) - : MagicOnionUnaryMethodBase>(serviceName, methodName) - where TService : class - where TRawRequest : class -{ - public override ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) - => MethodHandlerResultHelper.SetUnaryResultNonGeneric(invoker(service, context, request), context); -} - -internal class MethodHandlerResultHelper -{ - static readonly object BoxedNil = Nil.Default; - public static ValueTask NewEmptyValueTask(T result) - => default; - - public static ValueTask TaskToEmptyValueTask(Task result) - => new(result); - - public static ValueTask SetUnaryResultNonGeneric(UnaryResult result, ServiceContext context) + protected static ValueTask SetUnaryResultNonGeneric(UnaryResult result, ServiceContext context) { if (result.hasRawValue) { @@ -81,7 +53,7 @@ static async ValueTask Await(Task task, ServiceContext context) } } - public static ValueTask SetUnaryResult(UnaryResult result, ServiceContext context) + protected static ValueTask SetUnaryResult(UnaryResult result, ServiceContext context) { if (result.hasRawValue) { @@ -104,47 +76,28 @@ public static ValueTask SetUnaryResult(UnaryResult result, ServiceContext return default; - static async ValueTask Await(Task task, ServiceContext context) + static async ValueTask Await(Task task, ServiceContext context) { context.Result = await task.ConfigureAwait(false); } } +} - public static async ValueTask SetTaskUnaryResult(Task> taskResult, ServiceContext context) - { - var result = await taskResult.ConfigureAwait(false); - if (result.hasRawValue) - { - context.Result = (result.rawTaskValue != null) ? await result.rawTaskValue.ConfigureAwait(false) : result.rawValue; - } - } - - public static ValueTask SerializeClientStreamingResult(ClientStreamingResult result, ServiceContext context) - => SerializeValueTaskClientStreamingResult(new ValueTask>(result), context); - - public static ValueTask SerializeTaskClientStreamingResult(Task> taskResult, ServiceContext context) - => SerializeValueTaskClientStreamingResult(new ValueTask>(taskResult), context); - - public static ValueTask SerializeValueTaskClientStreamingResult(ValueTask> taskResult, ServiceContext context) - { - if (taskResult.IsCompletedSuccessfully) - { - if (taskResult.Result.hasRawValue) - { - context.Result = taskResult.Result.rawValue; - return default; - } - } - - return Await(taskResult, context); +public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func> invoker) + : MagicOnionUnaryMethodBase(serviceName, methodName) + where TService : class + where TRawRequest : class + where TRawResponse : class +{ + public override ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) + => SetUnaryResult(invoker(service, context, request), context); +} - static async ValueTask Await(ValueTask> taskResult, ServiceContext context) - { - var result = await taskResult.ConfigureAwait(false); - if (result.hasRawValue) - { - context.Result = result.rawValue; - } - } - } +public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func invoker) + : MagicOnionUnaryMethodBase>(serviceName, methodName) + where TService : class + where TRawRequest : class +{ + public override ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) + => SetUnaryResultNonGeneric(invoker(service, context, request), context); } From 34ef6f98508f8be89af3b80d2951e56a04f75fb3 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Tue, 15 Oct 2024 15:10:16 +0900 Subject: [PATCH 05/27] Fix --- .../Binder/Internal/MagicOnionGrpcMethodBinder.cs | 3 +-- .../Binder/MagicOnionDuplexStreamingMethod.cs | 9 +++++++++ .../Binder/MagicOnionStreamingHubConnectMethod.cs | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs index 3a0231113..f2657910a 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs @@ -91,8 +91,7 @@ public void BindStreamingHub(MagicOnionStreamingHubConnectMethod metho var attrs = GetMetadataFromHandler(method.MethodInfo); var duplexMethod = new MagicOnionDuplexStreamingMethod( - method.ServiceName, - method.MethodName, + method, static (instance, context) => { context.CallContext.GetHttpContext().Features.Set(context.ServiceProvider.GetRequiredService>()); diff --git a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs index 09bdf63af..fda390bc1 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs @@ -49,6 +49,15 @@ public MagicOnionDuplexStreamingMethod(string serviceName, string methodName, Fu this.invoker = async (service, context) => await invoker(service, context); } + public MagicOnionDuplexStreamingMethod(MagicOnionStreamingHubConnectMethod hubConnectMethod, Func>> invoker) + { + ServiceName = hubConnectMethod.ServiceName; + MethodName = hubConnectMethod.MethodName; + MethodInfo = hubConnectMethod.MethodInfo; + + this.invoker = (service, context) => new ValueTask(invoker(service, context)); + } + public void Bind(IMagicOnionGrpcMethodBinder binder) => binder.BindDuplexStreaming(this); diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs index a816617d8..311e569d1 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs @@ -17,7 +17,7 @@ public MagicOnionStreamingHubConnectMethod(string serviceName) { ServiceName = serviceName; MethodName = nameof(IStreamingHubBase.Connect); - MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + MethodInfo = typeof(TService).GetMethod("MagicOnion.Server.Internal.IStreamingHubBase.Connect", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; } public void Bind(IMagicOnionGrpcMethodBinder binder) From 90015e8bf9c7e408031184328c061168b7ea4341 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Tue, 15 Oct 2024 16:34:21 +0900 Subject: [PATCH 06/27] Fix StreamingServices --- .../DynamicMagicOnionMethodProvider.cs | 118 ++++++++++++++---- .../Internal/MagicOnionGrpcMethodBinder.cs | 18 +-- .../Internal/MagicOnionGrpcMethodHandler.cs | 4 +- .../Binder/MagicOnionServerStreamingMethod.cs | 20 +-- 4 files changed, 112 insertions(+), 48 deletions(-) diff --git a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs index c494042aa..1e67793c8 100644 --- a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs @@ -2,6 +2,7 @@ using System.Reflection; using MagicOnion.Internal; using MagicOnion.Server.Hubs; +using MagicOnion.Server.Internal; namespace MagicOnion.Server.Binder.Internal; @@ -30,7 +31,7 @@ public IEnumerable GetGrpcMethods() where TServ .GenericTypeArguments[0]; // StreamingHub - if (typeof(TService).IsAssignableTo(typeof(IStreamingHubMarker))) + if (typeof(TService).IsAssignableTo(typeof(IStreamingHubBase))) { yield return new MagicOnionStreamingHubConnectMethod(typeServiceInterface.Name); yield break; @@ -49,42 +50,105 @@ public IEnumerable GetGrpcMethods() where TServ var targetMethod = methodInfo; var methodParameters = targetMethod.GetParameters(); - var typeRequest = CreateRequestType(methodParameters); - var typeRawRequest = typeRequest.IsValueType - ? typeof(Box<>).MakeGenericType(typeRequest) - : typeRequest; - Type typeMethod; + Type? typeMethod = default; + Type[] typeMethodTypeArgs = []; + Type typeRequest = typeof(object); + Type? typeResponse = default; if (targetMethod.ReturnType == typeof(UnaryResult)) { // UnaryResult: The method has no return value. - typeMethod = typeof(MagicOnionUnaryMethod<,,>).MakeGenericType(typeServiceImplementation, typeRequest, typeRawRequest); + typeRequest = CreateRequestType(methodParameters); + typeMethod = typeof(MagicOnionUnaryMethod<,,>); } - else + else if (targetMethod.ReturnType is { IsGenericType: true }) { - // UnaryResult - var typeResponse = targetMethod.ReturnType.GetGenericArguments()[0]; - var typeRawResponse = typeResponse.IsValueType - ? typeof(Box<>).MakeGenericType(typeResponse) - : typeResponse; - typeMethod = typeof(MagicOnionUnaryMethod<,,,,>).MakeGenericType(typeServiceImplementation, typeRequest, typeResponse, typeRawRequest, typeRawResponse); + var returnTypeOpen = targetMethod.ReturnType.GetGenericTypeDefinition(); + if (returnTypeOpen == typeof(UnaryResult<>)) + { + // UnaryResult + typeRequest = CreateRequestType(methodParameters); + typeResponse = targetMethod.ReturnType.GetGenericArguments()[0]; + typeMethod = typeof(MagicOnionUnaryMethod<,,,,>); + } + else if (returnTypeOpen == typeof(Task<>)) + { + var returnType2 = targetMethod.ReturnType.GetGenericArguments()[0]; + var returnTypeOpen2 = returnType2.GetGenericTypeDefinition(); + if (returnTypeOpen2 == typeof(ClientStreamingResult<,>)) + { + // ClientStreamingResult + typeRequest = returnType2.GetGenericArguments()[0]; + typeResponse = returnType2.GetGenericArguments()[1]; + typeMethod = typeof(MagicOnionClientStreamingMethod<,,,,>); + } + else if (returnTypeOpen2 == typeof(ServerStreamingResult<>)) + { + // ServerStreamingResult + typeRequest = CreateRequestType(methodParameters); + typeResponse = returnType2.GetGenericArguments()[0]; + typeMethod = typeof(MagicOnionServerStreamingMethod<,,,,>); + } + else if (returnTypeOpen2 == typeof(DuplexStreamingResult<,>)) + { + // DuplexStreamingResult + typeRequest = returnType2.GetGenericArguments()[0]; + typeResponse = returnType2.GetGenericArguments()[1]; + typeMethod = typeof(MagicOnionDuplexStreamingMethod<,,,,>); + } + } } - // (instance, context, request) => instance.Foo(request.Item1, request.Item2...); - var exprParamInstance = Expression.Parameter(typeServiceImplementation); - var exprParamServiceContext = Expression.Parameter(typeof(ServiceContext)); - var exprParamRequest = Expression.Parameter(typeRequest); - var exprArguments = methodParameters.Length == 1 - ? [exprParamRequest] - : methodParameters - .Select((x, i) => Expression.Field(exprParamRequest, "Item" + (i + 1))) - .Cast() - .ToArray(); + if (typeMethod is null) + { + throw new InvalidOperationException("The return type of the service method must be one of 'UnaryResult', 'ClientStreaming', 'ServerStreaming' or 'DuplexStreaming'."); + } - var exprCall = Expression.Call(exprParamInstance, targetMethod, exprArguments); - var invoker = Expression.Lambda(exprCall, [exprParamInstance, exprParamServiceContext, exprParamRequest]).Compile(); + // ***Result<> --> ***Result + var typeRawRequest = typeRequest.IsValueType + ? typeof(Box<>).MakeGenericType(typeRequest) + : typeRequest; + var typeRawResponse = typeResponse is { IsValueType: true } + ? typeof(Box<>).MakeGenericType(typeResponse) + : typeResponse; + if (typeResponse is null || typeRawResponse is null) + { + typeMethodTypeArgs = [typeServiceImplementation, typeRequest, typeRawRequest]; + } + else + { + typeMethodTypeArgs = [typeServiceImplementation, typeRequest, typeResponse, typeRawRequest, typeRawResponse]; + } + + Delegate invoker; + if (typeMethod == typeof(MagicOnionUnaryMethod<,,>) || typeMethod == typeof(MagicOnionUnaryMethod<,,,,>) || typeMethod == typeof(MagicOnionServerStreamingMethod<,,,,>)) + { + // Unary, ServerStreaming + // (instance, context, request) => instance.Foo(request.Item1, request.Item2...); + var exprParamInstance = Expression.Parameter(typeServiceImplementation); + var exprParamServiceContext = Expression.Parameter(typeof(ServiceContext)); + var exprParamRequest = Expression.Parameter(typeRequest); + var exprArguments = methodParameters.Length == 1 + ? [exprParamRequest] + : methodParameters + .Select((x, i) => Expression.Field(exprParamRequest, "Item" + (i + 1))) + .Cast() + .ToArray(); + + var exprCall = Expression.Call(exprParamInstance, targetMethod, exprArguments); + invoker = Expression.Lambda(exprCall, [exprParamInstance, exprParamServiceContext, exprParamRequest]).Compile(); + } + else + { + // ClientStreaming, DuplexStreaming + // (instance, context) => instance.Foo(); + var exprParamInstance = Expression.Parameter(typeServiceImplementation); + var exprParamServiceContext = Expression.Parameter(typeof(ServiceContext)); + var exprCall = Expression.Call(exprParamInstance, targetMethod, []); + invoker = Expression.Lambda(exprCall, [exprParamInstance, exprParamServiceContext]).Compile(); + } - var serviceMethod = Activator.CreateInstance(typeMethod, [typeServiceInterface.Name, targetMethod.Name, invoker])!; + var serviceMethod = Activator.CreateInstance(typeMethod.MakeGenericType(typeMethodTypeArgs), [typeServiceInterface.Name, targetMethod.Name, invoker])!; yield return (IMagicOnionGrpcMethod)serviceMethod; } } diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs index f2657910a..a336828b0 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs @@ -38,7 +38,7 @@ public void BindUnary(IMagicOnio { var messageSerializer = messageSerializerProvider.Create(MethodType.Unary, default); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.Unary, method.ServiceName, method.MethodName, messageSerializer); - var attrs = GetMetadataFromHandler(method.MethodInfo); + var attrs = GetMetadataFromHandler(method); providerContext.AddUnaryMethod(grpcMethod, attrs, handlerBuilder.BuildUnaryMethod(method, messageSerializer, attrs)); } @@ -49,7 +49,7 @@ public void BindClientStreaming( { var messageSerializer = messageSerializerProvider.Create(MethodType.ClientStreaming, default); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ClientStreaming, method.ServiceName, method.MethodName, messageSerializer); - var attrs = GetMetadataFromHandler(method.MethodInfo); + var attrs = GetMetadataFromHandler(method); providerContext.AddClientStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildClientStreamingMethod(method, messageSerializer, attrs)); } @@ -60,9 +60,9 @@ public void BindServerStreaming( { var messageSerializer = messageSerializerProvider.Create(MethodType.ServerStreaming, default); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ServerStreaming, method.ServiceName, method.MethodName, messageSerializer); - var attrs = GetMetadataFromHandler(method.MethodInfo); + var attrs = GetMetadataFromHandler(method); - providerContext.AddServerStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildServerStreamingMethod(method, messageSerializer, attrs)); + providerContext.AddServerStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildServerStreamingMethod(method, messageSerializer, attrs)); } public void BindDuplexStreaming(MagicOnionDuplexStreamingMethod method) @@ -71,7 +71,7 @@ public void BindDuplexStreaming( { var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, default); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.DuplexStreaming, method.ServiceName, method.MethodName, messageSerializer); - var attrs = GetMetadataFromHandler(method.MethodInfo); + var attrs = GetMetadataFromHandler(method); providerContext.AddDuplexStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildDuplexStreamingMethod(method, messageSerializer, attrs)); } @@ -88,7 +88,7 @@ public void BindStreamingHub(MagicOnionStreamingHubConnectMethod metho MagicOnionMarshallers.StreamingHubMarshaller, MagicOnionMarshallers.StreamingHubMarshaller ); - var attrs = GetMetadataFromHandler(method.MethodInfo); + var attrs = GetMetadataFromHandler(method); var duplexMethod = new MagicOnionDuplexStreamingMethod( method, @@ -100,13 +100,13 @@ public void BindStreamingHub(MagicOnionStreamingHubConnectMethod metho providerContext.AddDuplexStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildDuplexStreamingMethod(duplexMethod, messageSerializer, attrs)); } - IList GetMetadataFromHandler(MethodInfo methodInfo) + IList GetMetadataFromHandler(IMagicOnionGrpcMethod magicOnionGrpcMethod) { // NOTE: We need to collect Attributes for Endpoint metadata. ([Authorize], [AllowAnonymous] ...) // https://github.com/grpc/grpc-dotnet/blob/7ef184f3c4cd62fbc3cde55e4bb3e16b58258ca1/src/Grpc.AspNetCore.Server/Model/Internal/ProviderServiceBinder.cs#L89-L98 var metadata = new List(); - metadata.AddRange(methodInfo.DeclaringType!.GetCustomAttributes(inherit: true)); - metadata.AddRange(methodInfo.GetCustomAttributes(inherit: true)); + metadata.AddRange(magicOnionGrpcMethod.ServiceType.GetCustomAttributes(inherit: true)); + metadata.AddRange(magicOnionGrpcMethod.MethodInfo.GetCustomAttributes(inherit: true)); metadata.Add(new HttpMethodMetadata(["POST"], acceptCorsPreflight: true)); return metadata; diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs index c0cabaf8b..02400dcc6 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs @@ -126,7 +126,7 @@ IList metadata { var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); - var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, (TRequest)serviceContext.Request!, serviceContext)); + var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext, (TRequest)serviceContext.Request!)); return InvokeAsync; @@ -137,7 +137,7 @@ async Task InvokeAsync(TService instance, TRawRequest rawRequest, IServerStreamW var request = GrpcMethodHelper.FromRaw(rawRequest); var responseStream = new MagicOnionServerStreamWriter(rawResponseStream); - var serviceContext = new StreamingServiceContext( + var serviceContext = new StreamingServiceContext( instance, method, attributeLookup, diff --git a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs index ddd02f1a7..435be4457 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs @@ -9,7 +9,7 @@ public class MagicOnionServerStreamingMethod invoker; + readonly Func invoker; public MethodType MethodType => MethodType.ServerStreaming; public Type ServiceType => typeof(TService); @@ -18,40 +18,40 @@ public class MagicOnionServerStreamingMethod> invoker) + public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func> invoker) { ServiceName = serviceName; MethodName = methodName; MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - this.invoker = (service, request, context) => + this.invoker = (service, context, request) => { - invoker(service, request, context); + invoker(service, context, request); return default; }; } - public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func>> invoker) + public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func>> invoker) { ServiceName = serviceName; MethodName = methodName; MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - this.invoker = (service, request, context) => new ValueTask(invoker(service, request, context)); + this.invoker = (service, context, request) => new ValueTask(invoker(service, context, request)); } - public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func>> invoker) + public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func>> invoker) { ServiceName = serviceName; MethodName = methodName; MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - this.invoker = async (service, request, context) => await invoker(service, request, context); + this.invoker = async (service, context, request) => await invoker(service, context, request); } public void Bind(IMagicOnionGrpcMethodBinder binder) => binder.BindServerStreaming(this); - public ValueTask InvokeAsync(TService service, TRequest request, ServiceContext context) - => invoker(service, request, context); + public ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) + => invoker(service, context, request); } From 64ac53aca5689b8e4a271d7806801c24286bbdd1 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Tue, 15 Oct 2024 17:37:10 +0900 Subject: [PATCH 07/27] Cleanup --- .../Binder/Internal/MagicOnionGrpcMethodBinder.cs | 2 +- .../Binder/MagicOnionStreamingHubMethod.cs | 1 - .../Features/Internal/IStreamingHubFeature.cs | 14 ++++++++++++++ .../Hubs/Internal/StreamingHubRegistry.cs | 14 +++----------- src/MagicOnion.Server/Hubs/StreamingHub.cs | 2 +- src/MagicOnion.Server/Hubs/StreamingHubHandler.cs | 5 ----- src/MagicOnion.Server/MagicOnion.Server.csproj | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 src/MagicOnion.Server/Features/Internal/IStreamingHubFeature.cs diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs index a336828b0..502af5df6 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs @@ -1,8 +1,8 @@ -using System.Reflection; using Grpc.AspNetCore.Server.Model; using Grpc.Core; using MagicOnion.Internal; using MagicOnion.Serialization; +using MagicOnion.Server.Features.Internal; using MagicOnion.Server.Hubs.Internal; using MagicOnion.Server.Internal; using Microsoft.AspNetCore.Routing; diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs index a0da04a11..81f847d54 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs @@ -14,7 +14,6 @@ public interface IMagicOnionStreamingHubMethod ValueTask InvokeAsync(StreamingHubContext context); } -// TODO: public class MagicOnionStreamingHubMethod : IMagicOnionStreamingHubMethod { public string ServiceName { get; } diff --git a/src/MagicOnion.Server/Features/Internal/IStreamingHubFeature.cs b/src/MagicOnion.Server/Features/Internal/IStreamingHubFeature.cs new file mode 100644 index 000000000..04c5c2125 --- /dev/null +++ b/src/MagicOnion.Server/Features/Internal/IStreamingHubFeature.cs @@ -0,0 +1,14 @@ +using System.Diagnostics.CodeAnalysis; +using MagicOnion.Server.Hubs; +using MagicOnion.Server.Internal; + +namespace MagicOnion.Server.Features.Internal; + +internal interface IStreamingHubFeature +{ + MagicOnionManagedGroupProvider GroupProvider { get; } + IStreamingHubHeartbeatManager HeartbeatManager { get; } + UniqueHashDictionary Handlers { get; } + + bool TryGetMethod(int methodId, [NotNullWhen(true)] out StreamingHubHandler? handler); +} diff --git a/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs b/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs index 8452a42ae..62492d496 100644 --- a/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs +++ b/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs @@ -1,24 +1,16 @@ using System.Diagnostics.CodeAnalysis; -using Cysharp.Runtime.Multicast; -using Microsoft.Extensions.DependencyInjection; using System.Reflection; +using Cysharp.Runtime.Multicast; using MagicOnion.Server.Binder; using MagicOnion.Server.Diagnostics; +using MagicOnion.Server.Features.Internal; using MagicOnion.Server.Internal; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace MagicOnion.Server.Hubs.Internal; -internal interface IStreamingHubFeature -{ - MagicOnionManagedGroupProvider GroupProvider { get; } - IStreamingHubHeartbeatManager HeartbeatManager { get; } - UniqueHashDictionary Handlers { get; } - - bool TryGetMethod(int methodId, [NotNullWhen(true)] out StreamingHubHandler? handler); -} - internal class StreamingHubRegistry : IStreamingHubFeature { readonly MagicOnionOptions options; diff --git a/src/MagicOnion.Server/Hubs/StreamingHub.cs b/src/MagicOnion.Server/Hubs/StreamingHub.cs index fc7fb3fb8..173586893 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHub.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHub.cs @@ -7,7 +7,7 @@ using MagicOnion.Internal.Buffers; using MagicOnion.Server.Diagnostics; using MagicOnion.Server.Features; -using MagicOnion.Server.Hubs.Internal; +using MagicOnion.Server.Features.Internal; using MagicOnion.Server.Internal; using MessagePack; using Microsoft.AspNetCore.Connections; diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs index 9d58b3ec0..b4f45591d 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs @@ -1,9 +1,4 @@ -using System.Buffers; -using MessagePack; -using System.Linq.Expressions; using System.Reflection; -using System.Runtime.CompilerServices; -using Grpc.Core; using MagicOnion.Server.Filters; using MagicOnion.Server.Filters.Internal; using MagicOnion.Server.Internal; diff --git a/src/MagicOnion.Server/MagicOnion.Server.csproj b/src/MagicOnion.Server/MagicOnion.Server.csproj index 82aa4cda4..69343c4bb 100644 --- a/src/MagicOnion.Server/MagicOnion.Server.csproj +++ b/src/MagicOnion.Server/MagicOnion.Server.csproj @@ -1,4 +1,4 @@ - + net6.0;net8.0 From 8517dfa2f7c7c9a5f01382d0b786a6a101ba9dd3 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Tue, 15 Oct 2024 17:49:19 +0900 Subject: [PATCH 08/27] Use fallback resolvers --- .../Binder/Internal/MagicOnionGrpcMethodBinder.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs index 502af5df6..c266f2c5b 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs @@ -36,7 +36,7 @@ public void BindUnary(IMagicOnio where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.Unary, default); + var messageSerializer = messageSerializerProvider.Create(MethodType.Unary, method.MethodInfo); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.Unary, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -47,7 +47,7 @@ public void BindClientStreaming( where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.ClientStreaming, default); + var messageSerializer = messageSerializerProvider.Create(MethodType.ClientStreaming, method.MethodInfo); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ClientStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -58,7 +58,7 @@ public void BindServerStreaming( where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.ServerStreaming, default); + var messageSerializer = messageSerializerProvider.Create(MethodType.ServerStreaming, method.MethodInfo); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ServerStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -69,7 +69,7 @@ public void BindDuplexStreaming( where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, default); + var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.MethodInfo); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.DuplexStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -78,7 +78,7 @@ public void BindDuplexStreaming( public void BindStreamingHub(MagicOnionStreamingHubConnectMethod method) { - var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, default); + var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.MethodInfo); // StreamingHub uses the special marshallers for streaming messages serialization. // TODO: Currently, MagicOnion expects TRawRequest/TRawResponse to be raw-byte array (`StreamingHubPayload`). var grpcMethod = new Method( From 4effd6070d22102757631d46e8bd9618fcebaee5 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Wed, 16 Oct 2024 12:36:44 +0900 Subject: [PATCH 09/27] Refactor route mapping logics --- samples/ChatApp/ChatApp.Server/Program.cs | 1 - .../Binder/IMagicOnionGrpcMethodProvider.cs | 50 ++++- .../DynamicMagicOnionMethodProvider.cs | 15 +- ...agicOnionEndpointRouteBuilderExtensions.cs | 61 ++++++- ...icOnionServiceEndpointConventionBuilder.cs | 0 .../MagicOnionServicesExtensions.cs | 25 +-- .../Internal/MagicOnionServicesDiscoverer.cs | 105 +++++++++++ src/MagicOnion.Server/MagicOnionEngine.cs | 171 ------------------ .../MagicOnionServiceDefinition.cs | 13 -- ...HandCraftedMagicOnionMethodProviderTest.cs | 8 +- ...MagicOnionGrpcServiceMappingContextTest.cs | 45 +++++ tests/samples/AuthSample/Program.cs | 5 +- 12 files changed, 268 insertions(+), 231 deletions(-) rename src/MagicOnion.Server/{Binder => Extensions}/MagicOnionServiceEndpointConventionBuilder.cs (100%) create mode 100644 src/MagicOnion.Server/Internal/MagicOnionServicesDiscoverer.cs delete mode 100644 src/MagicOnion.Server/MagicOnionEngine.cs delete mode 100644 src/MagicOnion.Server/MagicOnionServiceDefinition.cs create mode 100644 tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs diff --git a/samples/ChatApp/ChatApp.Server/Program.cs b/samples/ChatApp/ChatApp.Server/Program.cs index 38cd6f34b..ed65a40b9 100644 --- a/samples/ChatApp/ChatApp.Server/Program.cs +++ b/samples/ChatApp/ChatApp.Server/Program.cs @@ -12,7 +12,6 @@ endpointOptions.Protocols = HttpProtocols.Http2; }); }); -builder.Services.AddGrpc(); // MagicOnion depends on ASP.NET Core gRPC service. builder.Services.AddMagicOnion(); var app = builder.Build(); diff --git a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs index 2abbbad01..b43117f0d 100644 --- a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs @@ -3,26 +3,62 @@ namespace MagicOnion.Server.Binder; -public class MagicOnionGrpcServiceRegistrationContext(IEndpointRouteBuilder builder) +public class MagicOnionGrpcServiceMappingContext(IEndpointRouteBuilder builder) : IEndpointConventionBuilder { - public MagicOnionServiceEndpointConventionBuilder Register() + readonly List innerBuilders = new(); + + public void Map() where T : class, IServiceMarker { - return new MagicOnionServiceEndpointConventionBuilder(builder.MapGrpcService()); + innerBuilders.Add(new MagicOnionServiceEndpointConventionBuilder(builder.MapGrpcService())); } - public MagicOnionServiceEndpointConventionBuilder Register(Type t) + public void Map(Type t) { - return new MagicOnionServiceEndpointConventionBuilder((GrpcServiceEndpointConventionBuilder)typeof(GrpcEndpointRouteBuilderExtensions) + VerifyServiceType(t); + + innerBuilders.Add(new MagicOnionServiceEndpointConventionBuilder((GrpcServiceEndpointConventionBuilder)typeof(GrpcEndpointRouteBuilderExtensions) .GetMethod(nameof(GrpcEndpointRouteBuilderExtensions.MapGrpcService))! .MakeGenericMethod(t) - .Invoke(null, [builder])!); + .Invoke(null, [builder])!)); + } + + static void VerifyServiceType(Type type) + { + if (!typeof(IServiceMarker).IsAssignableFrom(type)) + { + throw new InvalidOperationException($"Type '{type.FullName}' is not marked as MagicOnion service or hub."); + } + if (!type.GetInterfaces().Any(x => x.IsGenericType && (x.GetGenericTypeDefinition() == typeof(IService<>) || x.GetGenericTypeDefinition() == typeof(IStreamingHub<,>)))) + { + throw new InvalidOperationException($"Type '{type.FullName}' has no implementation for Service or StreamingHub"); + } + if (type.IsAbstract) + { + throw new InvalidOperationException($"Type '{type.FullName}' is abstract. A service type must be non-abstract class."); + } + if (type.IsInterface) + { + throw new InvalidOperationException($"Type '{type.FullName}' is interface. A service type must be class."); + } + if (type.IsGenericType && type.IsGenericTypeDefinition) + { + throw new InvalidOperationException($"Type '{type.FullName}' is generic type definition. A service type must be plain or constructed-generic class."); + } + } + + void IEndpointConventionBuilder.Add(Action convention) + { + foreach (var innerBuilder in innerBuilders) + { + innerBuilder.Add(convention); + } } } public interface IMagicOnionGrpcMethodProvider { - void OnRegisterGrpcServices(MagicOnionGrpcServiceRegistrationContext context); + void MapAllSupportedServiceTypes(MagicOnionGrpcServiceMappingContext context); IEnumerable GetGrpcMethods() where TService : class; IEnumerable GetStreamingHubMethods() where TService : class; } diff --git a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs index 1e67793c8..cb0d936da 100644 --- a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs @@ -8,18 +8,11 @@ namespace MagicOnion.Server.Binder.Internal; internal class DynamicMagicOnionMethodProvider : IMagicOnionGrpcMethodProvider { - readonly MagicOnionServiceDefinition definition; - - public DynamicMagicOnionMethodProvider(MagicOnionServiceDefinition definition) - { - this.definition = definition; - } - - public void OnRegisterGrpcServices(MagicOnionGrpcServiceRegistrationContext context) + public void MapAllSupportedServiceTypes(MagicOnionGrpcServiceMappingContext context) { - foreach (var serviceType in this.definition.TargetTypes.Distinct()) + foreach (var serviceType in MagicOnionServicesDiscoverer.GetTypesFromAssemblies(MagicOnionServicesDiscoverer.GetSearchAssemblies())) { - context.Register(serviceType); + context.Map(serviceType); } } @@ -52,7 +45,7 @@ public IEnumerable GetGrpcMethods() where TServ var methodParameters = targetMethod.GetParameters(); Type? typeMethod = default; - Type[] typeMethodTypeArgs = []; + Type[] typeMethodTypeArgs; Type typeRequest = typeof(object); Type? typeResponse = default; if (targetMethod.ReturnType == typeof(UnaryResult)) diff --git a/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs b/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs index 1cd637a05..932acfc24 100644 --- a/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs +++ b/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs @@ -1,4 +1,7 @@ +using System.Reflection; +using MagicOnion; using MagicOnion.Server.Binder; +using MagicOnion.Server.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -6,12 +9,62 @@ namespace Microsoft.AspNetCore.Builder; public static class MagicOnionEndpointRouteBuilderExtensions { - public static void MapMagicOnionService(this IEndpointRouteBuilder builder) + /// + /// Maps MagicOnion Unary and StreamingHub services in the loaded assemblies to the route builder. + /// + /// + public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointRouteBuilder builder) { - var context = new MagicOnionGrpcServiceRegistrationContext(builder); - foreach (var methodProvider in builder.ServiceProvider.GetRequiredService>()) + var context = new MagicOnionGrpcServiceMappingContext(builder); + foreach (var methodProvider in builder.ServiceProvider.GetServices()) { - methodProvider.OnRegisterGrpcServices(context); + methodProvider.MapAllSupportedServiceTypes(context); } + + return context; + } + + /// + /// Maps specified type as a MagicOnion Unary or StreamingHub service to the route builder. + /// + /// + public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointRouteBuilder builder) + where T : class, IServiceMarker + { + var context = new MagicOnionGrpcServiceMappingContext(builder); + context.Map(); + + return context; + } + /// + /// Maps specified types as MagicOnion Unary and StreamingHub services to the route builder. + /// + /// + /// + public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointRouteBuilder builder, params Type[] serviceTypes) + { + var context = new MagicOnionGrpcServiceMappingContext(builder); + foreach (var t in serviceTypes) + { + context.Map(t); + } + + return context; + } + + /// + /// Maps MagicOnion Unary and StreamingHub services in the target assemblies to the route builder. + /// + /// + /// + public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointRouteBuilder builder, params Assembly[] searchAssemblies) + { + var context = new MagicOnionGrpcServiceMappingContext(builder); + foreach (var t in MagicOnionServicesDiscoverer.GetTypesFromAssemblies(searchAssemblies)) + { + context.Map(t); + } + + return context; } } diff --git a/src/MagicOnion.Server/Binder/MagicOnionServiceEndpointConventionBuilder.cs b/src/MagicOnion.Server/Extensions/MagicOnionServiceEndpointConventionBuilder.cs similarity index 100% rename from src/MagicOnion.Server/Binder/MagicOnionServiceEndpointConventionBuilder.cs rename to src/MagicOnion.Server/Extensions/MagicOnionServiceEndpointConventionBuilder.cs diff --git a/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs index 29feab231..4aadcc1be 100644 --- a/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs +++ b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs @@ -11,7 +11,6 @@ using MagicOnion.Server.Hubs.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; @@ -19,25 +18,15 @@ namespace Microsoft.Extensions.DependencyInjection; public static class MagicOnionServicesExtensions { public static IMagicOnionServerBuilder AddMagicOnion(this IServiceCollection services, Action? configureOptions = null) - { - var configName = Options.Options.DefaultName; - services.TryAddSingleton(sp => MagicOnionEngine.BuildServerServiceDefinition(sp, sp.GetRequiredService>().Get(configName))); - return services.AddMagicOnionCore(configureOptions); - } + => services.AddMagicOnionCore(configureOptions); + [Obsolete("Use MapMagicOnionService(Assembly[]) instead.", error: true)] public static IMagicOnionServerBuilder AddMagicOnion(this IServiceCollection services, Assembly[] searchAssemblies, Action? configureOptions = null) - { - var configName = Options.Options.DefaultName; - services.TryAddSingleton(sp => MagicOnionEngine.BuildServerServiceDefinition(sp, searchAssemblies, sp.GetRequiredService>().Get(configName))); - return services.AddMagicOnionCore(configureOptions); - } + => throw new NotSupportedException(); + [Obsolete("Use MapMagicOnionService(Type[]) instead.", error: true)] public static IMagicOnionServerBuilder AddMagicOnion(this IServiceCollection services, IEnumerable searchTypes, Action? configureOptions = null) - { - var configName = Options.Options.DefaultName; - services.TryAddSingleton(sp => MagicOnionEngine.BuildServerServiceDefinition(sp, searchTypes, sp.GetRequiredService>().Get(configName))); - return services.AddMagicOnionCore(configureOptions); - } + => throw new NotSupportedException(); // NOTE: `internal` is required for unit tests. internal static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollection services, Action? configureOptions = null) @@ -50,9 +39,9 @@ internal static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollecti services.AddMetrics(); // MagicOnion: Core services - services.TryAddSingleton(typeof(StreamingHubRegistry<>)); + services.AddSingleton(typeof(StreamingHubRegistry<>)); services.AddSingleton(typeof(IServiceMethodProvider<>), typeof(MagicOnionGrpcServiceMethodProvider<>)); - services.AddSingleton(); + services.TryAddSingleton(); // MagicOnion: Metrics services.TryAddSingleton(); diff --git a/src/MagicOnion.Server/Internal/MagicOnionServicesDiscoverer.cs b/src/MagicOnion.Server/Internal/MagicOnionServicesDiscoverer.cs new file mode 100644 index 000000000..76729b7b8 --- /dev/null +++ b/src/MagicOnion.Server/Internal/MagicOnionServicesDiscoverer.cs @@ -0,0 +1,105 @@ +using System.Reflection; + +namespace MagicOnion.Server.Internal; + +internal static class MagicOnionServicesDiscoverer +{ + static readonly string[] wellKnownIgnoreAssemblies = + [ + "Anonymously Hosted DynamicMethods Assembly", + "netstandard", + "mscorlib", + "NuGet.*", + "System.*", + "Microsoft.AspNetCore.*", + "Microsoft.CSharp.*", + "Microsoft.CodeAnalysis.*", + "Microsoft.Extensions.*", + "Microsoft.Identity.*", + "Microsoft.IdentityModel.*", + "Microsoft.Net.*", + "Microsoft.VisualStudio.*", + "Microsoft.WebTools.*", + "Microsoft.Win32.*", + // Popular 3rd-party libraries + "Newtonsoft.Json", + "Pipelines.Sockets.Unofficial", + "Polly.*", + "StackExchange.Redis.*", + "StatsdClient", + // AWS + "AWSSDK.*", + // Azure + "Azure.*", + "Microsoft.Azure.*", + // gRPC + "Grpc.*", + "Google.Protobuf.*", + // WPF + "Accessibility", + "PresentationFramework", + "PresentationCore", + "WindowsBase", + // MessagePack, MemoryPack + "MessagePack.*", + "MemoryPack.*", + // Multicaster + "Cysharp.Runtime.Multicaster", + // MagicOnion + "MagicOnion.Server.*", + "MagicOnion.Client.*", // MagicOnion.Client.DynamicClient (MagicOnionClient.Create) + "MagicOnion.Abstractions", + "MagicOnion.Shared", + // Cysharp libraries + "Cysharp.Threading.LogicLooper", + "MasterMemory.*", + "MessagePipe.*", + "Ulid", + "ZString", + "ZLogger", + ]; + + + public static IEnumerable GetSearchAssemblies() + { + // NOTE: Exclude well-known system assemblies from automatic discovery of services. + return AppDomain.CurrentDomain.GetAssemblies() + .Where(x => !ShouldIgnoreAssembly(x.GetName().Name!)) + .ToArray(); + } + + public static IEnumerable GetTypesFromAssemblies(IEnumerable searchAssemblies) + { + return searchAssemblies + .SelectMany(x => + { + try + { + return x.GetTypes() + .Where(x => typeof(IServiceMarker).IsAssignableFrom(x)) + .Where(x => x.GetCustomAttribute(false) == null) + .Where(x => x.IsPublic && !x.IsAbstract && !x.IsGenericTypeDefinition); + } + catch (ReflectionTypeLoadException) + { + return Array.Empty(); + } + }); + } + + static bool ShouldIgnoreAssembly(string name) + { + return wellKnownIgnoreAssemblies.Any(y => + { + if (y.EndsWith(".*")) + { + return name.StartsWith(y.Substring(0, y.Length - 1), StringComparison.OrdinalIgnoreCase) || // Starts with 'MagicOnion.Client.' + name.Equals(y.Substring(0, y.Length - 2), StringComparison.OrdinalIgnoreCase); // Exact match 'MagicOnion.Client' (w/o last dot) + } + else + { + return name == y; + } + }); + } +} diff --git a/src/MagicOnion.Server/MagicOnionEngine.cs b/src/MagicOnion.Server/MagicOnionEngine.cs deleted file mode 100644 index f3a11892c..000000000 --- a/src/MagicOnion.Server/MagicOnionEngine.cs +++ /dev/null @@ -1,171 +0,0 @@ -using MagicOnion.Server.Hubs; -using System.Diagnostics; -using System.Reflection; -using System.Runtime.ExceptionServices; -using Cysharp.Runtime.Multicast; -using Grpc.Core; -using Microsoft.Extensions.DependencyInjection; -using MagicOnion.Server.Diagnostics; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Concurrent; -using Microsoft.Extensions.Options; - -namespace MagicOnion.Server; - -public static class MagicOnionEngine -{ - const string LoggerNameMagicOnionEngine = "MagicOnion.Server.MagicOnionEngine"; - const string LoggerNameMethodHandler = "MagicOnion.Server.MethodHandler"; - - static readonly string[] wellKnownIgnoreAssemblies = new[] - { - "netstandard", - "mscorlib", - "Microsoft.AspNetCore.*", - "Microsoft.CSharp.*", - "Microsoft.CodeAnalysis.*", - "Microsoft.Extensions.*", - "Microsoft.Win32.*", - "NuGet.*", - "System.*", - "Newtonsoft.Json", - "Microsoft.Identity.*", - "Microsoft.IdentityModel.*", - "StackExchange.Redis.*", - // gRPC - "Grpc.*", - // WPF - "Accessibility", - "PresentationFramework", - "PresentationCore", - "WindowsBase", - // MessagePack, MemoryPack - "MessagePack.*", - "MemoryPack.*", - // MagicOnion - "MagicOnion.Server.*", - "MagicOnion.Client.*", // MagicOnion.Client.DynamicClient (MagicOnionClient.Create) - "MagicOnion.Abstractions", - "MagicOnion.Shared", - }; - - /// - /// Search MagicOnion service from all assemblies. - /// - /// The service provider is used to resolve dependencies - /// If true, when method body throws exception send to client exception.ToString message. It is useful for debugging. - /// - public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceProvider serviceProvider, bool isReturnExceptionStackTraceInErrorDetail = false) - { - return BuildServerServiceDefinition(serviceProvider, new MagicOnionOptions() { IsReturnExceptionStackTraceInErrorDetail = isReturnExceptionStackTraceInErrorDetail }); - } - - /// - /// Search MagicOnion service from all assemblies. - /// - /// The service provider is used to resolve dependencies - /// The options for MagicOnion server - public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceProvider serviceProvider, MagicOnionOptions options) - { - // NOTE: Exclude well-known system assemblies from automatic discovery of services. - var assemblies = AppDomain.CurrentDomain.GetAssemblies() - .Where(x => !ShouldIgnoreAssembly(x.GetName().Name!)) - .ToArray(); - - return BuildServerServiceDefinition(serviceProvider, assemblies, options); - } - - /// - /// Search MagicOnion service from target assemblies. ex: new[]{ typeof(Startup).GetTypeInfo().Assembly } - /// - /// The service provider is used to resolve dependencies - /// The assemblies to be search for services - /// The options for MagicOnion server - public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceProvider serviceProvider, Assembly[] searchAssemblies, MagicOnionOptions options) - { - var types = searchAssemblies - .SelectMany(x => - { - try - { - return x.GetTypes() - .Where(x => typeof(IServiceMarker).IsAssignableFrom(x)) - .Where(x => x.GetCustomAttribute(false) == null) - .Where(x => x.IsPublic && !x.IsAbstract && !x.IsGenericTypeDefinition); - } - catch (ReflectionTypeLoadException) - { - return Array.Empty(); - } - }); - -#pragma warning disable CS8620 // Argument of type cannot be used for parameter of type in due to differences in the nullability of reference types. - return BuildServerServiceDefinition(serviceProvider, types, options); -#pragma warning restore CS8620 // Argument of type cannot be used for parameter of type in due to differences in the nullability of reference types. - } - - /// - /// Search MagicOnion service from target types. - /// - /// The service provider is used to resolve dependencies - /// The types to be search for services - /// The options for MagicOnion server - public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceProvider serviceProvider, IEnumerable targetTypes, MagicOnionOptions options) - { - var loggerFactory = serviceProvider.GetRequiredService(); - var loggerMagicOnionEngine = loggerFactory.CreateLogger(LoggerNameMagicOnionEngine); - - MagicOnionServerLog.BeginBuildServiceDefinition(loggerMagicOnionEngine); - - var sw = Stopwatch.StartNew(); - - var result = new MagicOnionServiceDefinition(targetTypes.ToArray()); - - sw.Stop(); - MagicOnionServerLog.EndBuildServiceDefinition(loggerMagicOnionEngine, sw.Elapsed.TotalMilliseconds); - - return result; - } - - - internal static void VerifyServiceType(Type type) - { - if (!typeof(IServiceMarker).IsAssignableFrom(type)) - { - throw new InvalidOperationException($"Type '{type.FullName}' is not marked as MagicOnion service or hub."); - } - if (!type.GetInterfaces().Any(x => x.IsGenericType && (x.GetGenericTypeDefinition() == typeof(IService<>) || x.GetGenericTypeDefinition() == typeof(IStreamingHub<,>)))) - { - throw new InvalidOperationException($"Type '{type.FullName}' has no implementation for Service or StreamingHub"); - } - if (type.IsAbstract) - { - throw new InvalidOperationException($"Type '{type.FullName}' is abstract. A service type must be non-abstract class."); - } - if (type.IsInterface) - { - throw new InvalidOperationException($"Type '{type.FullName}' is interface. A service type must be class."); - } - if (type.IsGenericType && type.IsGenericTypeDefinition) - { - throw new InvalidOperationException($"Type '{type.FullName}' is generic type definition. A service type must be plain or constructed-generic class."); - } - } - - internal static bool ShouldIgnoreAssembly(string name) - { - return wellKnownIgnoreAssemblies.Any(y => - { - if (y.EndsWith(".*")) - { - return name.StartsWith(y.Substring(0, y.Length - 1)) || // Starts with 'MagicOnion.Client.' - name == y.Substring(0, y.Length - 2); // Exact match 'MagicOnion.Client' (w/o last dot) - } - else - { - return name == y; - } - }); - } -} diff --git a/src/MagicOnion.Server/MagicOnionServiceDefinition.cs b/src/MagicOnion.Server/MagicOnionServiceDefinition.cs deleted file mode 100644 index e39f2ffa2..000000000 --- a/src/MagicOnion.Server/MagicOnionServiceDefinition.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MagicOnion.Server.Hubs; - -namespace MagicOnion.Server; - -public class MagicOnionServiceDefinition -{ - public IReadOnlyList TargetTypes { get; } - - public MagicOnionServiceDefinition(IReadOnlyList targetTypes) - { - this.TargetTypes = targetTypes; - } -} diff --git a/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs b/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs index b55111755..6729732bc 100644 --- a/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs +++ b/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs @@ -129,11 +129,11 @@ public override ValueTask Invoke(ServiceContext context, Func(); - context.Register(); - context.Register(); + context.Map(); + context.Map(); + context.Map(); } public IEnumerable GetGrpcMethods() where TService : class diff --git a/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs new file mode 100644 index 000000000..6b5b7dec2 --- /dev/null +++ b/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs @@ -0,0 +1,45 @@ +using MagicOnion.Server.Hubs; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace MagicOnion.Server.Tests; + +public class MagicOnionGrpcServiceMappingContextTest +{ + [Fact] + public void MapMagicOnionService_ValidServiceType() + { + // Arrange + var builder = WebApplication.CreateBuilder(); + builder.Services.AddMagicOnion(); + var app = builder.Build(); + + // Act + var ex = Record.Exception(() => app.MapMagicOnionService([typeof(GreeterService), typeof(GreeterHub)])); + + // Assert + Assert.Null(ex); + } + + [Fact] + public void MapMagicOnionService_InvalidType() + { + // Arrange + var builder = WebApplication.CreateBuilder(); + builder.Services.AddMagicOnion(); + var app = builder.Build(); + + // Act + var ex = Record.Exception(() => app.MapMagicOnionService([typeof(object)])); + + // Assert + Assert.NotNull(ex); + Assert.IsType(ex); + } + + public interface IGreeterService : IService; + public interface IGreeterHub : IStreamingHub; + public interface IGreeterHubReceiver; + public class GreeterService : ServiceBase, IGreeterService; + public class GreeterHub : StreamingHubBase, IGreeterHub; +} diff --git a/tests/samples/AuthSample/Program.cs b/tests/samples/AuthSample/Program.cs index f7ff84844..0c11c6517 100644 --- a/tests/samples/AuthSample/Program.cs +++ b/tests/samples/AuthSample/Program.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Options; using System.Security.Claims; using System.Security.Principal; @@ -30,7 +31,7 @@ public class Startup public void ConfigureServices(IServiceCollection services) { services.AddGrpc(); - services.AddMagicOnion(new [] { typeof(Startup).Assembly }); + services.AddMagicOnion(); services.AddAuthentication("Fake") .AddScheme("Fake", options => { }); @@ -45,7 +46,7 @@ public void Configure(IApplicationBuilder app) app.UseAuthorization(); app.UseEndpoints(endpoints => { - endpoints.MapMagicOnionService(); + endpoints.MapMagicOnionService(typeof(Startup).Assembly); }); } } From c5d248f749a3b7906897ed267e2fbc871a75d9d4 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Wed, 16 Oct 2024 12:46:15 +0900 Subject: [PATCH 10/27] Update --- .../Binder/IMagicOnionGrpcMethodProvider.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs index b43117f0d..f12cc8083 100644 --- a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs @@ -3,6 +3,13 @@ namespace MagicOnion.Server.Binder; +public interface IMagicOnionGrpcMethodProvider +{ + void MapAllSupportedServiceTypes(MagicOnionGrpcServiceMappingContext context); + IEnumerable GetGrpcMethods() where TService : class; + IEnumerable GetStreamingHubMethods() where TService : class; +} + public class MagicOnionGrpcServiceMappingContext(IEndpointRouteBuilder builder) : IEndpointConventionBuilder { readonly List innerBuilders = new(); @@ -55,10 +62,3 @@ void IEndpointConventionBuilder.Add(Action convention) } } } - -public interface IMagicOnionGrpcMethodProvider -{ - void MapAllSupportedServiceTypes(MagicOnionGrpcServiceMappingContext context); - IEnumerable GetGrpcMethods() where TService : class; - IEnumerable GetStreamingHubMethods() where TService : class; -} From 7e4d183a7dc98ed05635992bfbba8947e7fc2145 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Wed, 16 Oct 2024 14:26:15 +0900 Subject: [PATCH 11/27] Fix build --- .../PerformanceTest.Shared/ApplicationInformation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perf/BenchmarkApp/PerformanceTest.Shared/ApplicationInformation.cs b/perf/BenchmarkApp/PerformanceTest.Shared/ApplicationInformation.cs index 103ffdfb6..d8582c1e3 100644 --- a/perf/BenchmarkApp/PerformanceTest.Shared/ApplicationInformation.cs +++ b/perf/BenchmarkApp/PerformanceTest.Shared/ApplicationInformation.cs @@ -10,7 +10,7 @@ public class ApplicationInformation public static ApplicationInformation Current { get; } = new ApplicationInformation(); #if SERVER - public string? MagicOnionVersion { get; } = typeof(MagicOnion.Server.MagicOnionEngine).Assembly.GetCustomAttribute()?.InformationalVersion; + public string? MagicOnionVersion { get; } = typeof(MagicOnion.Server.ServiceContext).Assembly.GetCustomAttribute()?.InformationalVersion; public string? GrpcNetVersion { get; } = typeof(Grpc.AspNetCore.Server.GrpcServiceOptions).Assembly.GetCustomAttribute()?.InformationalVersion; #elif CLIENT public string? MagicOnionVersion { get; } = typeof(MagicOnion.Client.MagicOnionClient).Assembly.GetCustomAttribute()?.InformationalVersion; From c4b6a63bbbaa00f1658980ac955ac80611c41490 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:09:13 +0900 Subject: [PATCH 12/27] ci: reuse benchmark id --- .github/workflows/benchmark.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index b7357ad63..50b9744c2 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -12,6 +12,11 @@ on: required: false default: true type: boolean + run_id: + description: "run_id: Use this run id for benchmark name" + required: false + type: string + default: "wf" benchmark-config-name: description: "benchmark-config-name: Select benchmark config name" required: false @@ -37,7 +42,7 @@ jobs: benchmark: uses: Cysharp/Actions/.github/workflows/benchmark-execute.yaml@main with: - benchmark-name: "magiconion-${{ github.event.issue.number || (inputs.reuse && 'wf' || github.run_number) }}" + benchmark-name: "magiconion-${{ github.event.issue.number || (inputs.reuse && inputs.run_id || github.run_number) }}" benchmark-config-path: "perf/BenchmarkApp/configs/${{ inputs.benchmark-config-name || github.event_name }}.yaml" secrets: inherit From 51f08b3763cb7c3bbeffcfd14635187bba9dbafe Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:11:35 +0900 Subject: [PATCH 13/27] ci: benchmark reuse param name --- .github/workflows/benchmark.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 50b9744c2..a5aea483c 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -12,8 +12,8 @@ on: required: false default: true type: boolean - run_id: - description: "run_id: Use this run id for benchmark name" + reuse_runid: + description: "reuse_runid: Use this run id for benchmark name for reuse" required: false type: string default: "wf" @@ -42,7 +42,7 @@ jobs: benchmark: uses: Cysharp/Actions/.github/workflows/benchmark-execute.yaml@main with: - benchmark-name: "magiconion-${{ github.event.issue.number || (inputs.reuse && inputs.run_id || github.run_number) }}" + benchmark-name: "magiconion-${{ github.event.issue.number || (inputs.reuse && inputs.reuse_runid || github.run_number) }}" benchmark-config-path: "perf/BenchmarkApp/configs/${{ inputs.benchmark-config-name || github.event_name }}.yaml" secrets: inherit From 6df9c7deec6319e05c2e29ea3ed255b57e030c12 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Wed, 16 Oct 2024 16:26:20 +0900 Subject: [PATCH 14/27] Fix tests --- .../DynamicMagicOnionMethodProvider.cs | 2 +- .../Binder/MagicOnionClientStreamingMethod.cs | 24 ++------------- .../Binder/MagicOnionDuplexStreamingMethod.cs | 30 +++---------------- .../Binder/MagicOnionServerStreamingMethod.cs | 28 ++--------------- .../MagicOnionApplicationFactory.cs | 12 +++++++- .../MagicOnionApplicationFactory.cs | 11 ++++++- .../MagicOnion.Server.Tests/_ServerFixture.cs | 5 ++-- tests/samples/MagicOnionTestServer/Program.cs | 6 ++-- 8 files changed, 38 insertions(+), 80 deletions(-) diff --git a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs index cb0d936da..4bc76c1b9 100644 --- a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs @@ -94,7 +94,7 @@ public IEnumerable GetGrpcMethods() where TServ if (typeMethod is null) { - throw new InvalidOperationException("The return type of the service method must be one of 'UnaryResult', 'ClientStreaming', 'ServerStreaming' or 'DuplexStreaming'."); + throw new InvalidOperationException("The return type of the service method must be one of 'UnaryResult', 'Task>', 'Task>' or 'Task>'."); } // ***Result<> --> ***Result diff --git a/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs index 3ac9c1f22..cfa505dd9 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs @@ -9,7 +9,7 @@ public class MagicOnionClientStreamingMethod>> invoker; + readonly Func>> invoker; public MethodType MethodType => MethodType.ClientStreaming; public Type ServiceType => typeof(TService); @@ -18,30 +18,12 @@ public class MagicOnionClientStreamingMethod> invoker) - { - ServiceName = serviceName; - MethodName = methodName; - MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - - this.invoker = (service, context) => ValueTask.FromResult(invoker(service, context)); - } - public MagicOnionClientStreamingMethod(string serviceName, string methodName, Func>> invoker) { ServiceName = serviceName; MethodName = methodName; MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - this.invoker = (service, context) => new ValueTask>(invoker(service, context)); - } - - public MagicOnionClientStreamingMethod(string serviceName, string methodName, Func>> invoker) - { - ServiceName = serviceName; - MethodName = methodName; - MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - this.invoker = invoker; } @@ -51,7 +33,7 @@ public void Bind(IMagicOnionGrpcMethodBinder binder) public ValueTask InvokeAsync(TService service, ServiceContext context) => SerializeValueTaskClientStreamingResult(invoker(service, context), context); - static ValueTask SerializeValueTaskClientStreamingResult(ValueTask> taskResult, ServiceContext context) + static ValueTask SerializeValueTaskClientStreamingResult(Task> taskResult, ServiceContext context) { if (taskResult.IsCompletedSuccessfully) { @@ -64,7 +46,7 @@ static ValueTask SerializeValueTaskClientStreamingResult(ValueTask> taskResult, ServiceContext context) + static async ValueTask Await(Task> taskResult, ServiceContext context) { var result = await taskResult.ConfigureAwait(false); if (result.hasRawValue) diff --git a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs index fda390bc1..cbc98e18e 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs @@ -9,7 +9,7 @@ public class MagicOnionDuplexStreamingMethod invoker; + readonly Func invoker; public MethodType MethodType => MethodType.DuplexStreaming; public Type ServiceType => typeof(TService); @@ -18,35 +18,13 @@ public class MagicOnionDuplexStreamingMethod> invoker) - { - ServiceName = serviceName; - MethodName = methodName; - MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - - this.invoker = (service, context) => - { - invoker(service, context); - return default; - }; - } - public MagicOnionDuplexStreamingMethod(string serviceName, string methodName, Func>> invoker) { ServiceName = serviceName; MethodName = methodName; MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - this.invoker = (service, context) => new ValueTask(invoker(service, context)); - } - - public MagicOnionDuplexStreamingMethod(string serviceName, string methodName, Func>> invoker) - { - ServiceName = serviceName; - MethodName = methodName; - MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - - this.invoker = async (service, context) => await invoker(service, context); + this.invoker = invoker; } public MagicOnionDuplexStreamingMethod(MagicOnionStreamingHubConnectMethod hubConnectMethod, Func>> invoker) @@ -55,12 +33,12 @@ public MagicOnionDuplexStreamingMethod(MagicOnionStreamingHubConnectMethod new ValueTask(invoker(service, context)); + this.invoker = invoker; } public void Bind(IMagicOnionGrpcMethodBinder binder) => binder.BindDuplexStreaming(this); public ValueTask InvokeAsync(TService service, ServiceContext context) - => invoker(service, context); + => new(invoker(service, context)); } diff --git a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs index 435be4457..631e9a14d 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs @@ -9,7 +9,7 @@ public class MagicOnionServerStreamingMethod invoker; + readonly Func invoker; public MethodType MethodType => MethodType.ServerStreaming; public Type ServiceType => typeof(TService); @@ -18,40 +18,18 @@ public class MagicOnionServerStreamingMethod> invoker) - { - ServiceName = serviceName; - MethodName = methodName; - MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - - this.invoker = (service, context, request) => - { - invoker(service, context, request); - return default; - }; - } - public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func>> invoker) { ServiceName = serviceName; MethodName = methodName; MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - this.invoker = (service, context, request) => new ValueTask(invoker(service, context, request)); - } - - public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func>> invoker) - { - ServiceName = serviceName; - MethodName = methodName; - MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; - - this.invoker = async (service, context, request) => await invoker(service, context, request); + this.invoker = invoker; } public void Bind(IMagicOnionGrpcMethodBinder binder) => binder.BindServerStreaming(this); public ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) - => invoker(service, context, request); + => new(invoker(service, context, request)); } diff --git a/tests/MagicOnion.Integration.Tests/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Integration.Tests/MagicOnionApplicationFactory.cs index 3ffa3d313..d6ffa3357 100644 --- a/tests/MagicOnion.Integration.Tests/MagicOnionApplicationFactory.cs +++ b/tests/MagicOnion.Integration.Tests/MagicOnionApplicationFactory.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using MagicOnion.Server; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; @@ -8,6 +9,7 @@ namespace MagicOnion.Integration.Tests; #pragma warning disable CS1998 public class MagicOnionApplicationFactory : WebApplicationFactory + where TServiceImplementation : class, IServiceMarker { public const string ItemsKey = "MagicOnionApplicationFactory.Items"; public ConcurrentDictionary Items => Services.GetRequiredKeyedService>(ItemsKey); @@ -17,7 +19,15 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder.ConfigureServices(services => { services.AddKeyedSingleton>(ItemsKey); - services.AddMagicOnion(new[] { typeof(TServiceImplementation) }); + services.AddMagicOnion(); + }); + builder.Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapMagicOnionService(); + }); }); } diff --git a/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs index f9d8f2c39..d0bc829d6 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using System.Collections.Concurrent; using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Builder; namespace MagicOnion.Server.Tests; @@ -34,7 +35,15 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder.ConfigureServices(services => { services.AddKeyedSingleton>(ItemsKey); - services.AddMagicOnion(GetServiceImplementationTypes()); + services.AddMagicOnion(); + }); + builder.Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapMagicOnionService([..GetServiceImplementationTypes()]); + }); }); } diff --git a/tests/MagicOnion.Server.Tests/_ServerFixture.cs b/tests/MagicOnion.Server.Tests/_ServerFixture.cs index 26f5ae26c..f116ff1c0 100644 --- a/tests/MagicOnion.Server.Tests/_ServerFixture.cs +++ b/tests/MagicOnion.Server.Tests/_ServerFixture.cs @@ -88,11 +88,12 @@ void PrepareServer() }) .ConfigureServices(services => { - services.AddMagicOnion(GetServiceTypes(), ConfigureMagicOnion); + services.AddMagicOnion(ConfigureMagicOnion); services.AddKeyedSingleton(ItemsServiceKey, Items); services.AddKeyedSingleton(HostStartupService.WaiterKey, serverStartupWaiter); services.AddHostedService(); + services.AddSingleton(this); }) .ConfigureServices(ConfigureServices) .Build() @@ -140,7 +141,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); app.UseEndpoints(endpoints => { - endpoints.MapMagicOnionService(); + endpoints.MapMagicOnionService([..app.ApplicationServices.GetRequiredService().GetServiceTypes()]); }); } } diff --git a/tests/samples/MagicOnionTestServer/Program.cs b/tests/samples/MagicOnionTestServer/Program.cs index d516487a2..001735349 100644 --- a/tests/samples/MagicOnionTestServer/Program.cs +++ b/tests/samples/MagicOnionTestServer/Program.cs @@ -33,13 +33,13 @@ public class Program public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); - builder.Services.AddGrpc(); + //builder.Services.AddGrpc(); var app = builder.Build(); - app.MapMagicOnionService(); + //app.MapMagicOnionService(); app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); app.Run(); } -} \ No newline at end of file +} From febcc048d47b64c053b20102f2e2f7e49d0cab19 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Thu, 17 Oct 2024 10:36:37 +0900 Subject: [PATCH 15/27] Add unit tests --- .../Binder/IMagicOnionGrpcMethod.cs | 2 +- .../Binder/IMagicOnionGrpcMethodProvider.cs | 4 +- .../DynamicMagicOnionMethodProvider.cs | 21 +- .../Internal/MagicOnionGrpcMethodBinder.cs | 2 +- .../Internal/MagicOnionGrpcMethodHandler.cs | 2 +- .../Binder/MagicOnionClientStreamingMethod.cs | 4 +- .../Binder/MagicOnionDuplexStreamingMethod.cs | 4 +- .../Binder/MagicOnionServerStreamingMethod.cs | 4 +- .../MagicOnionStreamingHubConnectMethod.cs | 4 +- .../Binder/MagicOnionUnaryMethod.cs | 5 +- src/MagicOnion.Server/ServiceContext.cs | 2 +- .../DynamicMagicOnionMethodProviderTest.cs | 347 ++++++++++++++++++ ...HandCraftedMagicOnionMethodProviderTest.cs | 76 +++- 13 files changed, 438 insertions(+), 39 deletions(-) create mode 100644 tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs diff --git a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs index 899b4a630..b6064ee23 100644 --- a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs @@ -6,7 +6,7 @@ namespace MagicOnion.Server.Binder; public interface IMagicOnionGrpcMethod { MethodType MethodType { get; } - Type ServiceType { get; } + Type ServiceImplementationType { get; } string ServiceName { get; } string MethodName { get; } MethodInfo MethodInfo { get; } diff --git a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs index f12cc8083..05f5d33c0 100644 --- a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs @@ -6,8 +6,8 @@ namespace MagicOnion.Server.Binder; public interface IMagicOnionGrpcMethodProvider { void MapAllSupportedServiceTypes(MagicOnionGrpcServiceMappingContext context); - IEnumerable GetGrpcMethods() where TService : class; - IEnumerable GetStreamingHubMethods() where TService : class; + IReadOnlyList GetGrpcMethods() where TService : class; + IReadOnlyList GetStreamingHubMethods() where TService : class; } public class MagicOnionGrpcServiceMappingContext(IEndpointRouteBuilder builder) : IEndpointConventionBuilder diff --git a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs index 4bc76c1b9..419e466ad 100644 --- a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs @@ -16,7 +16,7 @@ public void MapAllSupportedServiceTypes(MagicOnionGrpcServiceMappingContext cont } } - public IEnumerable GetGrpcMethods() where TService : class + public IReadOnlyList GetGrpcMethods() where TService : class { var typeServiceImplementation = typeof(TService); var typeServiceInterface = typeServiceImplementation.GetInterfaces() @@ -26,11 +26,11 @@ public IEnumerable GetGrpcMethods() where TServ // StreamingHub if (typeof(TService).IsAssignableTo(typeof(IStreamingHubBase))) { - yield return new MagicOnionStreamingHubConnectMethod(typeServiceInterface.Name); - yield break; + return [new MagicOnionStreamingHubConnectMethod(typeServiceInterface.Name)]; } // Unary, ClientStreaming, ServerStreaming, DuplexStreaming + var methods = new List(); var interfaceMap = typeServiceImplementation.GetInterfaceMapWithParents(typeServiceInterface); for (var i = 0; i < interfaceMap.TargetMethods.Length; i++) { @@ -94,7 +94,7 @@ public IEnumerable GetGrpcMethods() where TServ if (typeMethod is null) { - throw new InvalidOperationException("The return type of the service method must be one of 'UnaryResult', 'Task>', 'Task>' or 'Task>'."); + throw new InvalidOperationException($"The return type of the service method must be one of 'UnaryResult', 'Task>', 'Task>' or 'Task>'. (TargetMethod={targetMethod})"); } // ***Result<> --> ***Result @@ -142,17 +142,20 @@ public IEnumerable GetGrpcMethods() where TServ } var serviceMethod = Activator.CreateInstance(typeMethod.MakeGenericType(typeMethodTypeArgs), [typeServiceInterface.Name, targetMethod.Name, invoker])!; - yield return (IMagicOnionGrpcMethod)serviceMethod; + methods.Add((IMagicOnionGrpcMethod)serviceMethod); } + + return methods; } - public IEnumerable GetStreamingHubMethods() where TService : class + public IReadOnlyList GetStreamingHubMethods() where TService : class { if (!typeof(TService).IsAssignableTo(typeof(IStreamingHubMarker))) { - yield break; + return []; } + var methods = new List(); var typeServiceImplementation = typeof(TService); var typeServiceInterface = typeServiceImplementation.GetInterfaces() .First(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IService<>)) @@ -204,8 +207,10 @@ public IEnumerable GetStreamingHubMethods GetMetadataFromHandler(IMagicOnionGrpcMethod magicOnionGrpcMethod) // NOTE: We need to collect Attributes for Endpoint metadata. ([Authorize], [AllowAnonymous] ...) // https://github.com/grpc/grpc-dotnet/blob/7ef184f3c4cd62fbc3cde55e4bb3e16b58258ca1/src/Grpc.AspNetCore.Server/Model/Internal/ProviderServiceBinder.cs#L89-L98 var metadata = new List(); - metadata.AddRange(magicOnionGrpcMethod.ServiceType.GetCustomAttributes(inherit: true)); + metadata.AddRange(magicOnionGrpcMethod.ServiceImplementationType.GetCustomAttributes(inherit: true)); metadata.AddRange(magicOnionGrpcMethod.MethodInfo.GetCustomAttributes(inherit: true)); metadata.Add(new HttpMethodMetadata(["POST"], acceptCorsPreflight: true)); diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs index 02400dcc6..a1bc7e143 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs @@ -268,7 +268,7 @@ IList metadata where TRawResponse : class { var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); - var filters = FilterHelper.GetFilters(globalFilters, method.ServiceType, method.MethodInfo); + var filters = FilterHelper.GetFilters(globalFilters, method.ServiceImplementationType, method.MethodInfo); var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext, (TRequest)serviceContext.Request!)); return InvokeAsync; diff --git a/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs index cfa505dd9..7734137a4 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs @@ -1,8 +1,10 @@ +using System.Diagnostics; using System.Reflection; using Grpc.Core; namespace MagicOnion.Server.Binder; +[DebuggerDisplay("MagicOnionClientStreamingMethod: {ServiceName,nq}.{MethodName,nq}; Implementation={typeof(TService).ToString(),nq}; Request={typeof(TRequest).ToString(),nq}; RawRequest={typeof(TRawRequest).ToString(),nq}; Response={typeof(TResponse).ToString(),nq}; RawResponse={typeof(TRawResponse).ToString(),nq}")] public class MagicOnionClientStreamingMethod : IMagicOnionGrpcMethod where TService : class where TRawRequest : class @@ -12,7 +14,7 @@ public class MagicOnionClientStreamingMethod>> invoker; public MethodType MethodType => MethodType.ClientStreaming; - public Type ServiceType => typeof(TService); + public Type ServiceImplementationType => typeof(TService); public string ServiceName { get; } public string MethodName { get; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs index cbc98e18e..ceb7c8964 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs @@ -1,8 +1,10 @@ +using System.Diagnostics; using System.Reflection; using Grpc.Core; namespace MagicOnion.Server.Binder; +[DebuggerDisplay("MagicOnionDuplexStreamingMethod: {ServiceName,nq}.{MethodName,nq}; Implementation={typeof(TService).ToString(),nq}; Request={typeof(TRequest).ToString(),nq}; RawRequest={typeof(TRawRequest).ToString(),nq}; Response={typeof(TResponse).ToString(),nq}; RawResponse={typeof(TRawResponse).ToString(),nq}")] public class MagicOnionDuplexStreamingMethod : IMagicOnionGrpcMethod where TService : class where TRawRequest : class @@ -12,7 +14,7 @@ public class MagicOnionDuplexStreamingMethod invoker; public MethodType MethodType => MethodType.DuplexStreaming; - public Type ServiceType => typeof(TService); + public Type ServiceImplementationType => typeof(TService); public string ServiceName { get; } public string MethodName { get; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs index 631e9a14d..37057fb68 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs @@ -1,8 +1,10 @@ +using System.Diagnostics; using System.Reflection; using Grpc.Core; namespace MagicOnion.Server.Binder; +[DebuggerDisplay("MagicOnionServerStreamingMethod: {ServiceName,nq}.{MethodName,nq}; Implementation={typeof(TService).ToString(),nq}; Request={typeof(TRequest).ToString(),nq}; RawRequest={typeof(TRawRequest).ToString(),nq}; Response={typeof(TResponse).ToString(),nq}; RawResponse={typeof(TRawResponse).ToString(),nq}")] public class MagicOnionServerStreamingMethod : IMagicOnionGrpcMethod where TService : class where TRawRequest : class @@ -12,7 +14,7 @@ public class MagicOnionServerStreamingMethod invoker; public MethodType MethodType => MethodType.ServerStreaming; - public Type ServiceType => typeof(TService); + public Type ServiceImplementationType => typeof(TService); public string ServiceName { get; } public string MethodName { get; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs index 311e569d1..a2a4207d4 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs @@ -1,13 +1,15 @@ +using System.Diagnostics; using System.Reflection; using Grpc.Core; using MagicOnion.Server.Internal; namespace MagicOnion.Server.Binder; +[DebuggerDisplay("MagicOnionStreamingHubConnectMethod: Service={ServiceName,nq}.{MethodName,nq}")] public class MagicOnionStreamingHubConnectMethod : IMagicOnionGrpcMethod where TService : class { public MethodType MethodType => MethodType.DuplexStreaming; - public Type ServiceType => typeof(TService); + public Type ServiceImplementationType => typeof(TService); public string ServiceName { get; } public string MethodName { get; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs index 761769d3b..d3d510564 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Reflection; using Grpc.Core; using MagicOnion.Internal; @@ -22,7 +23,7 @@ public abstract class MagicOnionUnaryMethodBase MethodType.Unary; - public Type ServiceType => typeof(TService); + public Type ServiceImplementationType => typeof(TService); public string ServiceName => serviceName; public string MethodName => methodName; @@ -83,6 +84,7 @@ static async ValueTask Await(Task task, ServiceContext context) } } +[DebuggerDisplay("MagicOnionUnaryMethod: {ServiceName,nq}.{MethodName,nq}; Implementation={typeof(TService).ToString(),nq}; Request={typeof(TRequest).ToString(),nq}; RawRequest={typeof(TRawRequest).ToString(),nq}; Response={typeof(TResponse).ToString(),nq}; RawResponse={typeof(TRawResponse).ToString(),nq}")] public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func> invoker) : MagicOnionUnaryMethodBase(serviceName, methodName) where TService : class @@ -93,6 +95,7 @@ public override ValueTask InvokeAsync(TService service, ServiceContext context, => SetUnaryResult(invoker(service, context, request), context); } +[DebuggerDisplay("MagicOnionUnaryMethod: {ServiceName,nq}.{MethodName,nq}; Implementation={typeof(TService).ToString(),nq}; Request={typeof(TRequest).ToString(),nq}; RawRequest={typeof(TRawRequest).ToString(),nq}")] public sealed class MagicOnionUnaryMethod(string serviceName, string methodName, Func invoker) : MagicOnionUnaryMethodBase>(serviceName, methodName) where TService : class diff --git a/src/MagicOnion.Server/ServiceContext.cs b/src/MagicOnion.Server/ServiceContext.cs index 899ac24a4..821bd21ef 100644 --- a/src/MagicOnion.Server/ServiceContext.cs +++ b/src/MagicOnion.Server/ServiceContext.cs @@ -64,7 +64,7 @@ public ConcurrentDictionary Items public DateTime Timestamp { get; } - public Type ServiceType => Method.ServiceType; + public Type ServiceType => Method.ServiceImplementationType; public string ServiceName => Method.ServiceName; public string MethodName => MethodInfo.Name; diff --git a/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs b/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs new file mode 100644 index 000000000..b5f7241c0 --- /dev/null +++ b/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs @@ -0,0 +1,347 @@ +using Grpc.Core; +using MagicOnion.Internal; +using MagicOnion.Server.Binder; +using MagicOnion.Server.Binder.Internal; +using System.Reflection.Metadata; +using MagicOnion.Serialization; +using MessagePack; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; + +namespace MagicOnion.Server.Tests; + +public class DynamicMagicOnionMethodProviderTest +{ + [Fact] + public void Service_Empty() + { + // Arrange + var provider = new DynamicMagicOnionMethodProvider(); + + // Act + var methods = provider.GetGrpcMethods(); + + // Assert + Assert.Empty(methods); + } + + class Service_EmptyImpl : ServiceBase, Service_EmptyImpl.IServiceDef + { + public interface IServiceDef : IService + { + } + } + + [Fact] + public void Service_GetGrpcMethods() + { + // Arrange + var provider = new DynamicMagicOnionMethodProvider(); + + // Act + var methods = provider.GetGrpcMethods(); + + // Assert + Assert.NotEmpty(methods); + Assert.Equal(3 + 3 + 3 + 3 + 4 + 8 + 4, methods.Count); + + { + var expectedType = typeof(IMagicOnionUnaryMethod, Box>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterZero_NoReturnValue); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(IMagicOnionUnaryMethod, Box>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterZero_ReturnValueValueType); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(IMagicOnionUnaryMethod, string>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterZero_ReturnValueRefType); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(IMagicOnionUnaryMethod, Box>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterOneValueType_NoReturnValue); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(IMagicOnionUnaryMethod, Box>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterOneValueType_ReturnValueValueType); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(IMagicOnionUnaryMethod, string>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterOneValueType_ReturnValueRefType); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(IMagicOnionUnaryMethod>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterOneRefType_NoReturnValue); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(IMagicOnionUnaryMethod>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterOneRefType_ReturnValueValueType); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(IMagicOnionUnaryMethod); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterOneRefType_ReturnValueRefType); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(IMagicOnionUnaryMethod, MessagePack.Nil, Box>, Box>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterMany_NoReturnValue); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(IMagicOnionUnaryMethod, int, Box>, Box>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterMany_ReturnValueValueType); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(IMagicOnionUnaryMethod, string, Box>, string>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterMany_ReturnValueRefType); + var expectedMethodType = MethodType.Unary; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionClientStreamingMethod, Box>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.ClientStreaming_RequestTypeValueType_ResponseTypeValueType); + var expectedMethodType = MethodType.ClientStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionClientStreamingMethod>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.ClientStreaming_RequestTypeRefType_ResponseTypeValueType); + var expectedMethodType = MethodType.ClientStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionClientStreamingMethod, string>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.ClientStreaming_RequestTypeValueType_ResponseTypeRefType); + var expectedMethodType = MethodType.ClientStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionClientStreamingMethod); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.ClientStreaming_RequestTypeRefType_ResponseTypeRefType); + var expectedMethodType = MethodType.ClientStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionServerStreamingMethod, Box>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.ServerStreaming_ParameterZero_ResponseTypeValueType); + var expectedMethodType = MethodType.ServerStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionServerStreamingMethod, string>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.ServerStreaming_ParameterZero_ResponseTypeRefType); + var expectedMethodType = MethodType.ServerStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionServerStreamingMethod, Box>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.ServerStreaming_ParameterOneValueType_ResponseTypeValueType); + var expectedMethodType = MethodType.ServerStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionServerStreamingMethod, string>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.ServerStreaming_ParameterOneValueType_ResponseTypeRefType); + var expectedMethodType = MethodType.ServerStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionServerStreamingMethod>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.ServerStreaming_ParameterOneRefType_ResponseTypeValueType); + var expectedMethodType = MethodType.ServerStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionServerStreamingMethod, int, Box>, Box>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.ServerStreaming_ParameterMany_ResponseTypeValueType); + var expectedMethodType = MethodType.ServerStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionServerStreamingMethod, string, Box>, string>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.ServerStreaming_ParameterMany_ResponseTypeRefType); + var expectedMethodType = MethodType.ServerStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionDuplexStreamingMethod, Box>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.DuplexStreaming_RequestTypeValueType_ResponseTypeValueType); + var expectedMethodType = MethodType.DuplexStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionDuplexStreamingMethod>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.DuplexStreaming_RequestTypeRefType_ResponseTypeValueType); + var expectedMethodType = MethodType.DuplexStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionDuplexStreamingMethod, string>); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.DuplexStreaming_RequestTypeValueType_ResponseTypeRefType); + var expectedMethodType = MethodType.DuplexStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + { + var expectedType = typeof(MagicOnionDuplexStreamingMethod); + var expectedServiceMethodName = nameof(Service_MethodsImpl.IServiceDef.DuplexStreaming_RequestTypeRefType_ResponseTypeRefType); + var expectedMethodType = MethodType.DuplexStreaming; + AssertMethod(expectedType, expectedServiceMethodName, expectedMethodType, methods); + } + + + static void AssertMethod(Type expectedType, string expectedServiceMethodName, MethodType expectedMethodType, IReadOnlyList methods) + { + var expectedServiceName = nameof(Service_MethodsImpl.IServiceDef); + var expectedServiceImplementationType = typeof(Service_MethodsImpl); + var m = methods.SingleOrDefault(x => x.MethodName == expectedServiceMethodName); + Assert.NotNull(m); + Assert.IsAssignableFrom(expectedType, m); + Assert.Equal(expectedServiceImplementationType, m.ServiceImplementationType); + Assert.Equal(expectedServiceMethodName, m.MethodName); + Assert.Equal(expectedServiceName, m.ServiceName); + Assert.Equal(expectedMethodType, m.MethodType); + } + } + + class Service_MethodsImpl : ServiceBase, Service_MethodsImpl.IServiceDef + { + public interface IServiceDef : IService + { + UnaryResult Unary_ParameterZero_NoReturnValue() => throw new NotImplementedException(); + UnaryResult Unary_ParameterZero_ReturnValueValueType() => throw new NotImplementedException(); + UnaryResult Unary_ParameterZero_ReturnValueRefType() => throw new NotImplementedException(); + + UnaryResult Unary_ParameterOneValueType_NoReturnValue(int arg0) => throw new NotImplementedException(); + UnaryResult Unary_ParameterOneValueType_ReturnValueValueType(int arg0) => throw new NotImplementedException(); + UnaryResult Unary_ParameterOneValueType_ReturnValueRefType(int arg0) => throw new NotImplementedException(); + + UnaryResult Unary_ParameterOneRefType_NoReturnValue(string arg0) => throw new NotImplementedException(); + UnaryResult Unary_ParameterOneRefType_ReturnValueValueType(string arg0) => throw new NotImplementedException(); + UnaryResult Unary_ParameterOneRefType_ReturnValueRefType(string arg0) => throw new NotImplementedException(); + + UnaryResult Unary_ParameterMany_NoReturnValue(string arg0, int arg1, bool arg2) => throw new NotImplementedException(); + UnaryResult Unary_ParameterMany_ReturnValueValueType(string arg0, int arg1, bool arg2) => throw new NotImplementedException(); + UnaryResult Unary_ParameterMany_ReturnValueRefType(string arg0, int arg1, bool arg2) => throw new NotImplementedException(); + + Task> ClientStreaming_RequestTypeValueType_ResponseTypeValueType() => throw new NotImplementedException(); + Task> ClientStreaming_RequestTypeRefType_ResponseTypeValueType() => throw new NotImplementedException(); + Task> ClientStreaming_RequestTypeValueType_ResponseTypeRefType() => throw new NotImplementedException(); + Task> ClientStreaming_RequestTypeRefType_ResponseTypeRefType() => throw new NotImplementedException(); + + Task> ServerStreaming_ParameterZero_ResponseTypeValueType() => throw new NotImplementedException(); + Task> ServerStreaming_ParameterZero_ResponseTypeRefType() => throw new NotImplementedException(); + Task> ServerStreaming_ParameterOneValueType_ResponseTypeValueType(int arg0) => throw new NotImplementedException(); + Task> ServerStreaming_ParameterOneValueType_ResponseTypeRefType(int arg0) => throw new NotImplementedException(); + Task> ServerStreaming_ParameterOneRefType_ResponseTypeValueType(string arg0) => throw new NotImplementedException(); + Task> ServerStreaming_ParameterOneRefType_ResponseTypeRefType(string arg0) => throw new NotImplementedException(); + Task> ServerStreaming_ParameterMany_ResponseTypeValueType(string arg0, int arg1, bool arg2) => throw new NotImplementedException(); + Task> ServerStreaming_ParameterMany_ResponseTypeRefType(string arg0, int arg1, bool arg2) => throw new NotImplementedException(); + + Task> DuplexStreaming_RequestTypeValueType_ResponseTypeValueType() => throw new NotImplementedException(); + Task> DuplexStreaming_RequestTypeRefType_ResponseTypeValueType() => throw new NotImplementedException(); + Task> DuplexStreaming_RequestTypeValueType_ResponseTypeRefType() => throw new NotImplementedException(); + Task> DuplexStreaming_RequestTypeRefType_ResponseTypeRefType() => throw new NotImplementedException(); + } + } + + [Fact] + public void Service_Invalid_ReturnType() + { + // Arrange + var provider = new DynamicMagicOnionMethodProvider(); + + // Act + var ex = Record.Exception(() => provider.GetGrpcMethods()); + var ex2 = Record.Exception(() => provider.GetGrpcMethods()); + var ex3 = Record.Exception(() => provider.GetGrpcMethods()); + var ex4 = Record.Exception(() => provider.GetGrpcMethods()); + + // Assert + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.NotNull(ex2); + Assert.IsType(ex2); + Assert.NotNull(ex3); + Assert.IsType(ex3); + Assert.NotNull(ex4); + Assert.IsType(ex4); + } + + class Service_Invalid_ReturnType_1Impl : ServiceBase, Service_Invalid_ReturnType_1Impl.IServiceDef + { + public interface IServiceDef : IService + { + Task MethodAsync() => throw new NotImplementedException(); + } + } + + class Service_Invalid_ReturnType_2Impl : ServiceBase, Service_Invalid_ReturnType_2Impl.IServiceDef + { + public interface IServiceDef : IService + { + ValueTask MethodAsync() => throw new NotImplementedException(); + } + } + + class Service_Invalid_ReturnType_3Impl : ServiceBase, Service_Invalid_ReturnType_3Impl.IServiceDef + { + public interface IServiceDef : IService + { + Task MethodAsync() => throw new NotImplementedException(); + } + } + + class Service_Invalid_ReturnType_4Impl : ServiceBase, Service_Invalid_ReturnType_4Impl.IServiceDef + { + public interface IServiceDef : IService + { + ValueTask MethodAsync() => throw new NotImplementedException(); + } + } +} diff --git a/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs b/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs index 6729732bc..6dab752bb 100644 --- a/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs +++ b/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs @@ -4,6 +4,7 @@ using MagicOnion.Internal; using MagicOnion.Server.Binder; using MagicOnion.Server.Hubs; +using NSubstitute; namespace MagicOnion.Server.Tests; @@ -60,6 +61,23 @@ public async Task Unary_Parameter_Zero_NoReturnValue() // Act & Assert await client.PingAsync(); } + + [Fact] + public async Task StreamingHub() + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var receiver = Substitute.For(); + var client = await StreamingHubClient.ConnectAsync( + GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }), receiver); + + // Act + await client.JoinAsync("Alice", "Room-A"); + var users = await client.GetMembersAsync(); + + // Assert + Assert.Equal(["User-1", "User-2"], users); + } } @@ -105,17 +123,17 @@ class HandCraftedMagicOnionMethodProviderTest_GreeterHub : StreamingHubBase> GetMembersAsync() { - throw new NotImplementedException(); + return new(["User-1", "User-2"]); } } @@ -136,39 +154,55 @@ public void MapAllSupportedServiceTypes(MagicOnionGrpcServiceMappingContext cont context.Map(); } - public IEnumerable GetGrpcMethods() where TService : class + public IReadOnlyList GetGrpcMethods() where TService : class { if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterService)) { - yield return new MagicOnionUnaryMethod, string, Box>, string>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService.HelloAsync), static (instance, context, request) => instance.HelloAsync(request.Item1, request.Item2)); - yield return new MagicOnionUnaryMethod>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService.PingAsync), static (instance, context, request) => instance.PingAsync()); + return + [ + new MagicOnionUnaryMethod, string, Box>, string>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService.HelloAsync), + static (instance, context, request) => instance.HelloAsync(request.Item1, request.Item2)), + new MagicOnionUnaryMethod>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService.PingAsync), static (instance, context, request) => instance.PingAsync()), + ]; } if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterService2)) { - yield return new MagicOnionUnaryMethod, string, Box>, string>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2.GoodByeAsync), static (instance, context, request) => instance.GoodByeAsync(request.Item1, request.Item2)); - yield return new MagicOnionUnaryMethod>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2.PingAsync), static (instance, context, request) => instance.PingAsync()); + return + [ + new MagicOnionUnaryMethod, string, Box>, string>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2.GoodByeAsync), + static (instance, context, request) => instance.GoodByeAsync(request.Item1, request.Item2)), + new MagicOnionUnaryMethod>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterService2.PingAsync), static (instance, context, request) => instance.PingAsync()), + ]; } if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub)) { - yield return new MagicOnionStreamingHubConnectMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub)); + return + [ + new MagicOnionStreamingHubConnectMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub)), + ]; } + + return []; } - public IEnumerable GetStreamingHubMethods() where TService : class + public IReadOnlyList GetStreamingHubMethods() where TService : class { if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub)) { - yield return new MagicOnionStreamingHubMethod>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync), static (instance, context, request) => instance.JoinAsync(request.Item1, request.Item2)); - yield return new MagicOnionStreamingHubMethod( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.SendMessageAsync), static (instance, context, request) => instance.SendMessageAsync(request)); - yield return new MagicOnionStreamingHubMethod>( - nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync), static (instance, context, request) => instance.GetMembersAsync()); + return + [ + new MagicOnionStreamingHubMethod>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync), static (instance, context, request) => instance.JoinAsync(request.Item1, request.Item2)), + new MagicOnionStreamingHubMethod( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.SendMessageAsync), static (instance, context, request) => instance.SendMessageAsync(request)), + new MagicOnionStreamingHubMethod>( + nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync), static (instance, context, request) => instance.GetMembersAsync()), + ]; //yield return new MagicOnionStreamingHubMethod>( // nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync), typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub).GetMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.JoinAsync))!); //yield return new MagicOnionStreamingHubMethod( @@ -176,5 +210,7 @@ public IEnumerable GetStreamingHubMethods>( // nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub), nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync), typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub).GetMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub.GetMembersAsync))!); } + + return []; } } From a84bce701050d3548758a5980f575f05bdcc1ace Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Thu, 17 Oct 2024 18:11:10 +0900 Subject: [PATCH 16/27] Add unit tests --- .../Internal/MagicOnionGrpcMethodHandler.cs | 19 +- .../Binder/MagicOnionUnaryMethod.cs | 9 +- .../Diagnostics/MagicOnionMetrics.cs | 4 +- src/MagicOnion.Server/Service.cs | 6 +- .../ServiceContext.Streaming.cs | 3 +- src/MagicOnion.Server/ServiceContext.cs | 6 +- .../DynamicMagicOnionMethodProviderTest.cs | 163 +++++++++++- .../MagicOnionGrpcMethodTest.cs | 243 ++++++++++++++++++ 8 files changed, 422 insertions(+), 31 deletions(-) create mode 100644 tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs index a1bc7e143..de1e205dc 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs @@ -59,6 +59,8 @@ async Task InvokeAsync(TService instance, IAsyncStreamReader(); var requestStream = new MagicOnionAsyncStreamReader(rawRequestStream); var serviceContext = new StreamingServiceContext( instance, @@ -66,8 +68,9 @@ async Task InvokeAsync(TService instance, IAsyncStreamReader(); var request = GrpcMethodHelper.FromRaw(rawRequest); var responseStream = new MagicOnionServerStreamWriter(rawResponseStream); var serviceContext = new StreamingServiceContext( @@ -143,8 +148,9 @@ async Task InvokeAsync(TService instance, TRawRequest rawRequest, IServerStreamW attributeLookup, context, messageSerializer, + metrics, logger, - context.GetHttpContext().RequestServices, + requestServiceProvider, default, responseStream ); @@ -205,6 +211,8 @@ async Task InvokeAsync(TService instance, IAsyncStreamReader rawReq var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); var isCompletedSuccessfully = false; + var requestServiceProvider = context.GetHttpContext().RequestServices; + var metrics = requestServiceProvider.GetRequiredService(); var requestStream = new MagicOnionAsyncStreamReader(rawRequestStream); var responseStream = new MagicOnionServerStreamWriter(rawResponseStream); var serviceContext = new StreamingServiceContext( @@ -213,8 +221,9 @@ async Task InvokeAsync(TService instance, IAsyncStreamReader rawReq attributeLookup, context, messageSerializer, + metrics, logger, - context.GetHttpContext().RequestServices, + requestServiceProvider, requestStream, responseStream ); @@ -278,7 +287,9 @@ async Task InvokeAsync(TService instance, TRawRequest requestRaw, var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); var isCompletedSuccessfully = false; - var serviceContext = new ServiceContext(instance, method, attributeLookup, context, messageSerializer, logger, context.GetHttpContext().RequestServices); + var requestServiceProvider = context.GetHttpContext().RequestServices; + var metrics = requestServiceProvider.GetRequiredService(); + var serviceContext = new ServiceContext(instance, method, attributeLookup, context, messageSerializer, metrics, logger, requestServiceProvider); var request = GrpcMethodHelper.FromRaw(requestRaw); serviceContext.SetRawRequest(request); diff --git a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs index d3d510564..17b695cc0 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs @@ -36,14 +36,11 @@ public void Bind(IMagicOnionGrpcMethodBinder binder) protected static ValueTask SetUnaryResultNonGeneric(UnaryResult result, ServiceContext context) { - if (result.hasRawValue) + if (result is { hasRawValue: true, rawTaskValue.IsCompletedSuccessfully: true }) { - if (result.rawTaskValue is { IsCompletedSuccessfully: true }) - { - return Await(result.rawTaskValue, context); - } - context.Result = BoxedNil; + return Await(result.rawTaskValue, context); } + context.Result = BoxedNil; return default; diff --git a/src/MagicOnion.Server/Diagnostics/MagicOnionMetrics.cs b/src/MagicOnion.Server/Diagnostics/MagicOnionMetrics.cs index 6525dc18b..a23945873 100644 --- a/src/MagicOnion.Server/Diagnostics/MagicOnionMetrics.cs +++ b/src/MagicOnion.Server/Diagnostics/MagicOnionMetrics.cs @@ -1,7 +1,6 @@ using System.Diagnostics; using System.Diagnostics.Metrics; using MagicOnion.Server.Hubs; -using MagicOnion.Server.Internal; namespace MagicOnion.Server.Diagnostics; @@ -93,8 +92,9 @@ public void Dispose() meter.Dispose(); } + // NOTE: A context needs to be created for each request. An instance of MagicOnionMetrics is registered as a singleton. public MetricsContext CreateContext() - => new MetricsContext( + => new( streamingHubConnections.Enabled, streamingHubMethodDuration.Enabled, streamingHubMethodCompletedCounter.Enabled, diff --git a/src/MagicOnion.Server/Service.cs b/src/MagicOnion.Server/Service.cs index 89a0a11c1..f82594817 100644 --- a/src/MagicOnion.Server/Service.cs +++ b/src/MagicOnion.Server/Service.cs @@ -48,15 +48,15 @@ protected UnaryResult ReturnStatus(StatusCode statusCode, [Ignore] public ClientStreamingContext GetClientStreamingContext() - => new ClientStreamingContext((StreamingServiceContext)Context); + => new((StreamingServiceContext)Context); [Ignore] public ServerStreamingContext GetServerStreamingContext() - => new ServerStreamingContext((StreamingServiceContext)Context); + => new((StreamingServiceContext)Context); [Ignore] public DuplexStreamingContext GetDuplexStreamingContext() - => new DuplexStreamingContext((StreamingServiceContext)Context); + => new((StreamingServiceContext)Context); // Interface methods for Client diff --git a/src/MagicOnion.Server/ServiceContext.Streaming.cs b/src/MagicOnion.Server/ServiceContext.Streaming.cs index 8bfadcd4d..044af9f88 100644 --- a/src/MagicOnion.Server/ServiceContext.Streaming.cs +++ b/src/MagicOnion.Server/ServiceContext.Streaming.cs @@ -45,11 +45,12 @@ public StreamingServiceContext( ILookup attributeLookup, ServerCallContext context, IMagicOnionSerializer messageSerializer, + MagicOnionMetrics metrics, ILogger logger, IServiceProvider serviceProvider, IAsyncStreamReader? requestStream, IServerStreamWriter? responseStream - ) : base(instance, method, attributeLookup, context, messageSerializer, logger, serviceProvider) + ) : base(instance, method, attributeLookup, context, messageSerializer, metrics, logger, serviceProvider) { RequestStream = requestStream; ResponseStream = responseStream; diff --git a/src/MagicOnion.Server/ServiceContext.cs b/src/MagicOnion.Server/ServiceContext.cs index 821bd21ef..21fc8d326 100644 --- a/src/MagicOnion.Server/ServiceContext.cs +++ b/src/MagicOnion.Server/ServiceContext.cs @@ -90,12 +90,13 @@ public ConcurrentDictionary Items internal IMagicOnionGrpcMethod Method { get; } internal MetricsContext Metrics { get; } - public ServiceContext( + internal ServiceContext( object instance, IMagicOnionGrpcMethod method, ILookup attributeLookup, ServerCallContext context, IMagicOnionSerializer messageSerializer, + MagicOnionMetrics metrics, ILogger logger, IServiceProvider serviceProvider ) @@ -109,8 +110,7 @@ IServiceProvider serviceProvider this.Logger = logger; this.Method = method; this.ServiceProvider = serviceProvider; - - this.Metrics = serviceProvider.GetRequiredService().CreateContext(); + this.Metrics = metrics.CreateContext(); } /// Gets a request object. diff --git a/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs b/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs index b5f7241c0..bcd57a5a3 100644 --- a/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs +++ b/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs @@ -4,6 +4,7 @@ using MagicOnion.Server.Binder.Internal; using System.Reflection.Metadata; using MagicOnion.Serialization; +using MagicOnion.Server.Diagnostics; using MessagePack; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; @@ -32,6 +33,144 @@ public interface IServiceDef : IService } } + [Fact] + public async Task Service_Unary_Invoker_ParameterZero_NoReturnValue() + { + // Arrange + var provider = new DynamicMagicOnionMethodProvider(); + var methods = provider.GetGrpcMethods(); + var method = (IMagicOnionUnaryMethod, Box>) + methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterZero_NoReturnValue)); + var instance = new Service_MethodsImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, Nil.Default); + + // Assert + Assert.Equal(MessagePack.Nil.Default, serviceContext.Result); + } + + [Fact] + public async Task Service_Unary_Invoker_ParameterZero_ReturnValueValueType() + { + // Arrange + var provider = new DynamicMagicOnionMethodProvider(); + var methods = provider.GetGrpcMethods(); + var method = (IMagicOnionUnaryMethod, Box>) + methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterZero_ReturnValueValueType)); + var instance = new Service_MethodsImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, Nil.Default); + + // Assert + Assert.Equal(12345, serviceContext.Result); + } + + [Fact] + public async Task Service_Unary_Invoker_ParameterZero_ReturnValueRefType() + { + // Arrange + var provider = new DynamicMagicOnionMethodProvider(); + var methods = provider.GetGrpcMethods(); + var method = (IMagicOnionUnaryMethod, string>) + methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterZero_ReturnValueRefType)); + var instance = new Service_MethodsImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, Nil.Default); + + // Assert + Assert.Equal("Hello", serviceContext.Result); + } + + [Fact] + public async Task Service_Unary_Invoker_ParameterMany_NoReturnValue() + { + // Arrange + var provider = new DynamicMagicOnionMethodProvider(); + var methods = provider.GetGrpcMethods(); + var method = (IMagicOnionUnaryMethod, MessagePack.Nil, Box>, Box>) + methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterMany_NoReturnValue)); + var instance = new Service_MethodsImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, new DynamicArgumentTuple("Hello", 12345, true)); + + // Assert + Assert.Equal(MessagePack.Nil.Default, serviceContext.Result); + } + + [Fact] + public async Task Service_Unary_Invoker_ParameterMany_ReturnValueValueType() + { + // Arrange + var provider = new DynamicMagicOnionMethodProvider(); + var methods = provider.GetGrpcMethods(); + var method = (IMagicOnionUnaryMethod, int, Box>, Box>) + methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterMany_ReturnValueValueType)); + var instance = new Service_MethodsImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, new DynamicArgumentTuple("Hello", 12345, true)); + + // Assert + Assert.Equal(HashCode.Combine("Hello", 12345, true), serviceContext.Result); + } + + [Fact] + public async Task Service_Unary_Invoker_ParameterMany_ReturnValueRefType() + { + // Arrange + var provider = new DynamicMagicOnionMethodProvider(); + var methods = provider.GetGrpcMethods(); + var method = (IMagicOnionUnaryMethod, string, Box>, string>) + methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterMany_ReturnValueRefType)); + var instance = new Service_MethodsImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, new DynamicArgumentTuple("Hello", 12345, true)); + + // Assert + Assert.Equal("Hello;12345;True", serviceContext.Result); + } + [Fact] public void Service_GetGrpcMethods() { @@ -253,21 +392,21 @@ class Service_MethodsImpl : ServiceBase, Servic { public interface IServiceDef : IService { - UnaryResult Unary_ParameterZero_NoReturnValue() => throw new NotImplementedException(); - UnaryResult Unary_ParameterZero_ReturnValueValueType() => throw new NotImplementedException(); - UnaryResult Unary_ParameterZero_ReturnValueRefType() => throw new NotImplementedException(); + UnaryResult Unary_ParameterZero_NoReturnValue() => default; + UnaryResult Unary_ParameterZero_ReturnValueValueType() => UnaryResult.FromResult(12345); + UnaryResult Unary_ParameterZero_ReturnValueRefType() => UnaryResult.FromResult("Hello"); - UnaryResult Unary_ParameterOneValueType_NoReturnValue(int arg0) => throw new NotImplementedException(); - UnaryResult Unary_ParameterOneValueType_ReturnValueValueType(int arg0) => throw new NotImplementedException(); - UnaryResult Unary_ParameterOneValueType_ReturnValueRefType(int arg0) => throw new NotImplementedException(); + UnaryResult Unary_ParameterOneValueType_NoReturnValue(int arg0) => default; + UnaryResult Unary_ParameterOneValueType_ReturnValueValueType(int arg0) => UnaryResult.FromResult(arg0); + UnaryResult Unary_ParameterOneValueType_ReturnValueRefType(int arg0) => UnaryResult.FromResult($"{arg0}"); - UnaryResult Unary_ParameterOneRefType_NoReturnValue(string arg0) => throw new NotImplementedException(); - UnaryResult Unary_ParameterOneRefType_ReturnValueValueType(string arg0) => throw new NotImplementedException(); - UnaryResult Unary_ParameterOneRefType_ReturnValueRefType(string arg0) => throw new NotImplementedException(); + UnaryResult Unary_ParameterOneRefType_NoReturnValue(string arg0) => default; + UnaryResult Unary_ParameterOneRefType_ReturnValueValueType(string arg0) => UnaryResult.FromResult(int.Parse(arg0)); + UnaryResult Unary_ParameterOneRefType_ReturnValueRefType(string arg0) => UnaryResult.FromResult($"{arg0}"); - UnaryResult Unary_ParameterMany_NoReturnValue(string arg0, int arg1, bool arg2) => throw new NotImplementedException(); - UnaryResult Unary_ParameterMany_ReturnValueValueType(string arg0, int arg1, bool arg2) => throw new NotImplementedException(); - UnaryResult Unary_ParameterMany_ReturnValueRefType(string arg0, int arg1, bool arg2) => throw new NotImplementedException(); + UnaryResult Unary_ParameterMany_NoReturnValue(string arg0, int arg1, bool arg2) => default; + UnaryResult Unary_ParameterMany_ReturnValueValueType(string arg0, int arg1, bool arg2) => UnaryResult.FromResult(HashCode.Combine(arg0, arg1, arg2)); + UnaryResult Unary_ParameterMany_ReturnValueRefType(string arg0, int arg1, bool arg2) => UnaryResult.FromResult($"{arg0};{arg1};{arg2}"); Task> ClientStreaming_RequestTypeValueType_ResponseTypeValueType() => throw new NotImplementedException(); Task> ClientStreaming_RequestTypeRefType_ResponseTypeValueType() => throw new NotImplementedException(); diff --git a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs new file mode 100644 index 000000000..8da7e6a7e --- /dev/null +++ b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs @@ -0,0 +1,243 @@ +using Grpc.Core; +using MagicOnion.Internal; +using MagicOnion.Serialization; +using MagicOnion.Server.Binder; +using MagicOnion.Server.Diagnostics; +using MessagePack; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; +using NSubstitute; + +namespace MagicOnion.Server.Tests; + +public class MagicOnionGrpcMethodTest +{ + [Fact] + public async Task Unary_Invoker_NoRequest_NoResponse() + { + // Arrange + var called = false; + var invokerArgInstance = default(object); + var method = new MagicOnionUnaryMethod>("IMyService", "MethodName", (instance, context, _) => + { + called = true; + invokerArgInstance = instance; + return default; + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, Nil.Default); + + // Assert + Assert.Equal(MessagePack.Nil.Default, serviceContext.Result); + Assert.True(called); + Assert.Equal(instance, invokerArgInstance); + } + + [Fact] + public async Task Unary_Invoker_NoRequest_ResponseValueType() + { + // Arrange + var called = false; + var invokerArgInstance = default(object); + var method = new MagicOnionUnaryMethod, Box>("IMyService", "MethodName", (instance, context, _) => + { + called = true; + invokerArgInstance = instance; + return UnaryResult.FromResult(12345); + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, Nil.Default); + + // Assert + Assert.Equal(12345, serviceContext.Result); + Assert.True(called); + Assert.Equal(instance, invokerArgInstance); + } + + [Fact] + public async Task Unary_Invoker_NoRequest_ResponseRefType() + { + // Arrange + var called = false; + var invokerArgInstance = default(object); + var method = new MagicOnionUnaryMethod, string>("IMyService", "MethodName", (instance, context, _) => + { + called = true; + invokerArgInstance = instance; + return UnaryResult.FromResult("Hello"); + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, Nil.Default); + + // Assert + Assert.Equal("Hello", serviceContext.Result); + Assert.True(called); + Assert.Equal(instance, invokerArgInstance); + } + + [Fact] + public async Task Unary_Invoker_RequestValueType_NoResponse() + { + // Arrange + var called = false; + var invokerArgInstance = default(object); + var invokerArgRequest = default(object); + var method = new MagicOnionUnaryMethod>("IMyService", "MethodName", (instance, context, request) => + { + called = true; + invokerArgInstance = instance; + invokerArgRequest = request; + return default; + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, 12345); + + // Assert + Assert.Equal(12345, invokerArgRequest); + Assert.Equal(MessagePack.Nil.Default, serviceContext.Result); + Assert.True(called); + Assert.Equal(instance, invokerArgInstance); + } + + [Fact] + public async Task Unary_Invoker_RequestValueType_ResponseValueType() + { + // Arrange + var called = false; + var invokerArgInstance = default(object); + var invokerArgRequest = default(object); + var method = new MagicOnionUnaryMethod, Box>("IMyService", "MethodName", (instance, context, request) => + { + called = true; + invokerArgInstance = instance; + invokerArgRequest = request; + return UnaryResult.FromResult(request * 2); + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, 12345); + + // Assert + Assert.Equal(12345, invokerArgRequest); + Assert.Equal(12345 * 2, serviceContext.Result); + Assert.True(called); + Assert.Equal(instance, invokerArgInstance); + } + + [Fact] + public async Task Unary_Invoker_RequestValueType_ResponseRefType() + { + // Arrange + var called = false; + var invokerArgInstance = default(object); + var invokerArgRequest = default(object); + var method = new MagicOnionUnaryMethod, string>("IMyService", "MethodName", (instance, context, request) => + { + called = true; + invokerArgInstance = instance; + invokerArgRequest = request; + return UnaryResult.FromResult(request.ToString()); + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + + // Act + await method.InvokeAsync(instance, serviceContext, 12345); + + // Assert + Assert.Equal(12345, invokerArgRequest); + Assert.Equal("12345", serviceContext.Result); + Assert.True(called); + Assert.Equal(instance, invokerArgInstance); + } + + [Fact] + public async Task DuplexStreaming_Invoker_RequestValueType_ResponseValueType() + { + // Arrange + var called = false; + var invokerArgInstance = default(object); + var requestCurrentFirst = default(object); + var method = new MagicOnionDuplexStreamingMethod, Box>("IMyService", "MethodName", async (instance, context) => + { + called = true; + invokerArgInstance = instance; + + var streamingContext = new DuplexStreamingContext((StreamingServiceContext)context); + await streamingContext.WriteAsync(12345); + var request = await streamingContext.MoveNext(); + requestCurrentFirst = streamingContext.Current; + + return default; + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var requestStream = Substitute.For>(); + requestStream.MoveNext(default).ReturnsForAnyArgs(Task.FromResult(true)); + requestStream.Current.Returns(54321); + + var responseStream = Substitute.For>(); + var serviceContext = new StreamingServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider, requestStream, responseStream); + + // Act + await method.InvokeAsync(instance, serviceContext); + + // Assert + Assert.Null(serviceContext.Result); + Assert.True(called); + Assert.Equal(instance, invokerArgInstance); + Assert.Equal(54321, requestCurrentFirst); + _ = responseStream.Received(1).WriteAsync(12345); + } + + + class ServiceImpl; +} From d11b29e4941c92c7f17d16c1b873676d1b795d99 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Thu, 17 Oct 2024 18:26:09 +0900 Subject: [PATCH 17/27] Simplify signatures --- .../Binder/MagicOnionDuplexStreamingMethod.cs | 4 ++-- .../Binder/MagicOnionServerStreamingMethod.cs | 2 +- src/MagicOnion.Server/Hubs/StreamingHub.cs | 4 +--- src/MagicOnion.Server/Internal/IStreamingHubBase.cs | 9 ++++++--- .../MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs | 2 -- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs index ceb7c8964..1f83681ff 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs @@ -20,7 +20,7 @@ public class MagicOnionDuplexStreamingMethod>> invoker) + public MagicOnionDuplexStreamingMethod(string serviceName, string methodName, Func invoker) { ServiceName = serviceName; MethodName = methodName; @@ -29,7 +29,7 @@ public MagicOnionDuplexStreamingMethod(string serviceName, string methodName, Fu this.invoker = invoker; } - public MagicOnionDuplexStreamingMethod(MagicOnionStreamingHubConnectMethod hubConnectMethod, Func>> invoker) + public MagicOnionDuplexStreamingMethod(MagicOnionStreamingHubConnectMethod hubConnectMethod, Func invoker) { ServiceName = hubConnectMethod.ServiceName; MethodName = hubConnectMethod.MethodName; diff --git a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs index 37057fb68..0ba05619e 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs @@ -20,7 +20,7 @@ public class MagicOnionServerStreamingMethod>> invoker) + public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func invoker) { ServiceName = serviceName; MethodName = methodName; diff --git a/src/MagicOnion.Server/Hubs/StreamingHub.cs b/src/MagicOnion.Server/Hubs/StreamingHub.cs index 173586893..59b9dee05 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHub.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHub.cs @@ -81,7 +81,7 @@ protected virtual ValueTask OnDisconnected() return CompletedTask; } - async Task> IStreamingHubBase.Connect() + async Task IStreamingHubBase.Connect() { Metrics.StreamingHubConnectionIncrement(Context.Metrics, Context.ServiceName); @@ -145,8 +145,6 @@ async Task> IStr heartbeatHandle.Dispose(); remoteClientResultPendingTasks.Dispose(); } - - return streamingContext.Result(); } async Task HandleMessageAsync() diff --git a/src/MagicOnion.Server/Internal/IStreamingHubBase.cs b/src/MagicOnion.Server/Internal/IStreamingHubBase.cs index bd896eab4..5a2998f21 100644 --- a/src/MagicOnion.Server/Internal/IStreamingHubBase.cs +++ b/src/MagicOnion.Server/Internal/IStreamingHubBase.cs @@ -1,8 +1,11 @@ -using MagicOnion.Internal; - namespace MagicOnion.Server.Internal; internal interface IStreamingHubBase { - Task> Connect(); + /// + /// Process DuplexStreaming and start StreamingHub processing. + /// DO NOT change this name, as it is used as the name to be exposed as gRPC DuplexStreaming. + /// + /// + Task Connect(); } diff --git a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs index 8da7e6a7e..5f0a6b862 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs @@ -211,8 +211,6 @@ public async Task DuplexStreaming_Invoker_RequestValueType_ResponseValueType() await streamingContext.WriteAsync(12345); var request = await streamingContext.MoveNext(); requestCurrentFirst = streamingContext.Current; - - return default; }); var instance = new ServiceImpl(); var serverCallContext = Substitute.For(); From b035ee05d0e018b7f3a2c82c939e311441a11e1b Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Thu, 17 Oct 2024 18:40:31 +0900 Subject: [PATCH 18/27] Remove ServiceProviderHelper --- src/MagicOnion.Server/ServiceProviderHelper.cs | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 src/MagicOnion.Server/ServiceProviderHelper.cs diff --git a/src/MagicOnion.Server/ServiceProviderHelper.cs b/src/MagicOnion.Server/ServiceProviderHelper.cs deleted file mode 100644 index e7f748eba..000000000 --- a/src/MagicOnion.Server/ServiceProviderHelper.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MagicOnion.Server.Diagnostics; -using Microsoft.Extensions.DependencyInjection; - -namespace MagicOnion.Server; - -internal static class ServiceProviderHelper -{ - internal static TServiceBase CreateService(ServiceContext context) - where TServiceBase : ServiceBase - where TServiceInterface : IServiceMarker - { - var instance = ActivatorUtilities.CreateInstance(context.ServiceProvider); - instance.Context = context; - instance.Metrics = context.ServiceProvider.GetRequiredService(); - return instance; - } -} From 822e4dfedbd976d0ccba1636c5b768988da9f87f Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Fri, 18 Oct 2024 18:58:24 +0900 Subject: [PATCH 19/27] Use MethodHandlerMetadata --- .../Binder/IMagicOnionGrpcMethod.cs | 3 +- .../Internal/MagicOnionGrpcMethodBinder.cs | 15 +++++----- .../Internal/MagicOnionGrpcMethodHandler.cs | 14 ++++------ .../Binder/MagicOnionClientStreamingMethod.cs | 6 ++-- .../Binder/MagicOnionDuplexStreamingMethod.cs | 8 +++--- .../Binder/MagicOnionServerStreamingMethod.cs | 7 ++--- .../MagicOnionStreamingHubConnectMethod.cs | 3 +- .../Binder/MagicOnionStreamingHubMethod.cs | 11 ++++---- .../Binder/MagicOnionUnaryMethod.cs | 6 ++++ .../Filters/Internal/FilterHelper.cs | 12 ++++++-- .../Hubs/Internal/StreamingHubRegistry.cs | 2 +- .../Hubs/StreamingHubHandler.cs | 21 +++++++------- .../Internal/MethodHandlerMetadata.cs | 28 +++++++++++-------- .../ServiceContext.Streaming.cs | 3 +- src/MagicOnion.Server/ServiceContext.cs | 6 ++-- .../DynamicMagicOnionMethodProviderTest.cs | 18 ++++-------- .../MagicOnionGrpcMethodTest.cs | 18 ++++-------- .../StreamingHubHandlerTest.cs | 28 +++++++++---------- ...mingHubMethodHandlerMetadataFactoryTest.cs | 18 ++++++++++++ 19 files changed, 121 insertions(+), 106 deletions(-) diff --git a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs index b6064ee23..4f383fd15 100644 --- a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs @@ -1,5 +1,6 @@ using System.Reflection; using Grpc.Core; +using MagicOnion.Server.Internal; namespace MagicOnion.Server.Binder; @@ -9,7 +10,7 @@ public interface IMagicOnionGrpcMethod Type ServiceImplementationType { get; } string ServiceName { get; } string MethodName { get; } - MethodInfo MethodInfo { get; } + MethodHandlerMetadata Metadata { get; } } public interface IMagicOnionGrpcMethod : IMagicOnionGrpcMethod diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs index 9ff165a66..bbd99fcf0 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs @@ -36,7 +36,7 @@ public void BindUnary(IMagicOnio where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.Unary, method.MethodInfo); + var messageSerializer = messageSerializerProvider.Create(MethodType.Unary, method.Metadata.ServiceMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.Unary, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -47,7 +47,7 @@ public void BindClientStreaming( where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.ClientStreaming, method.MethodInfo); + var messageSerializer = messageSerializerProvider.Create(MethodType.ClientStreaming, method.Metadata.ServiceMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ClientStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -58,7 +58,7 @@ public void BindServerStreaming( where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.ServerStreaming, method.MethodInfo); + var messageSerializer = messageSerializerProvider.Create(MethodType.ServerStreaming, method.Metadata.ServiceMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ServerStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -69,7 +69,7 @@ public void BindDuplexStreaming( where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.MethodInfo); + var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.Metadata.ServiceMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.DuplexStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -78,7 +78,7 @@ public void BindDuplexStreaming( public void BindStreamingHub(MagicOnionStreamingHubConnectMethod method) { - var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.MethodInfo); + var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.Metadata.ServiceMethod); // StreamingHub uses the special marshallers for streaming messages serialization. // TODO: Currently, MagicOnion expects TRawRequest/TRawResponse to be raw-byte array (`StreamingHubPayload`). var grpcMethod = new Method( @@ -105,8 +105,9 @@ IList GetMetadataFromHandler(IMagicOnionGrpcMethod magicOnionGrpcMethod) // NOTE: We need to collect Attributes for Endpoint metadata. ([Authorize], [AllowAnonymous] ...) // https://github.com/grpc/grpc-dotnet/blob/7ef184f3c4cd62fbc3cde55e4bb3e16b58258ca1/src/Grpc.AspNetCore.Server/Model/Internal/ProviderServiceBinder.cs#L89-L98 var metadata = new List(); - metadata.AddRange(magicOnionGrpcMethod.ServiceImplementationType.GetCustomAttributes(inherit: true)); - metadata.AddRange(magicOnionGrpcMethod.MethodInfo.GetCustomAttributes(inherit: true)); + //metadata.AddRange(magicOnionGrpcMethod.ServiceImplementationType.GetCustomAttributes(inherit: true)); + //metadata.AddRange(magicOnionGrpcMethod.Metadata.ServiceMethod.GetCustomAttributes(inherit: true)); + metadata.AddRange(magicOnionGrpcMethod.Metadata.Attributes); metadata.Add(new HttpMethodMetadata(["POST"], acceptCorsPreflight: true)); return metadata; diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs index de1e205dc..3521622a9 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs @@ -49,7 +49,7 @@ IList metadata where TRawResponse : class { var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); - var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); + var filters = FilterHelper.GetFilters(globalFilters, method.Metadata.Attributes); var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext)); return InvokeAsync; @@ -65,7 +65,6 @@ async Task InvokeAsync(TService instance, IAsyncStreamReader( instance, method, - attributeLookup, context, messageSerializer, metrics, @@ -128,7 +127,7 @@ IList metadata where TRawResponse : class { var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); - var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); + var filters = FilterHelper.GetFilters(globalFilters, method.Metadata.Attributes); var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext, (TRequest)serviceContext.Request!)); return InvokeAsync; @@ -145,7 +144,6 @@ async Task InvokeAsync(TService instance, TRawRequest rawRequest, IServerStreamW var serviceContext = new StreamingServiceContext( instance, method, - attributeLookup, context, messageSerializer, metrics, @@ -201,7 +199,7 @@ IList metadata where TRawResponse : class { var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); - var filters = FilterHelper.GetFilters(globalFilters, typeof(TService), method.MethodInfo); + var filters = FilterHelper.GetFilters(globalFilters, method.Metadata.Attributes); var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext)); return InvokeAsync; @@ -218,7 +216,6 @@ async Task InvokeAsync(TService instance, IAsyncStreamReader rawReq var serviceContext = new StreamingServiceContext( instance, method, - attributeLookup, context, messageSerializer, metrics, @@ -276,8 +273,7 @@ IList metadata where TRawRequest : class where TRawResponse : class { - var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); - var filters = FilterHelper.GetFilters(globalFilters, method.ServiceImplementationType, method.MethodInfo); + var filters = FilterHelper.GetFilters(globalFilters, method.Metadata.Attributes); var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext, (TRequest)serviceContext.Request!)); return InvokeAsync; @@ -289,7 +285,7 @@ async Task InvokeAsync(TService instance, TRawRequest requestRaw, var requestServiceProvider = context.GetHttpContext().RequestServices; var metrics = requestServiceProvider.GetRequiredService(); - var serviceContext = new ServiceContext(instance, method, attributeLookup, context, messageSerializer, metrics, logger, requestServiceProvider); + var serviceContext = new ServiceContext(instance, method, context, messageSerializer, metrics, logger, requestServiceProvider); var request = GrpcMethodHelper.FromRaw(requestRaw); serviceContext.SetRawRequest(request); diff --git a/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs index 7734137a4..88b06fc05 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Reflection; using Grpc.Core; +using MagicOnion.Server.Internal; namespace MagicOnion.Server.Binder; @@ -17,14 +18,13 @@ public class MagicOnionClientStreamingMethod typeof(TService); public string ServiceName { get; } public string MethodName { get; } - - public MethodInfo MethodInfo { get; } + public MethodHandlerMetadata Metadata { get; } public MagicOnionClientStreamingMethod(string serviceName, string methodName, Func>> invoker) { ServiceName = serviceName; MethodName = methodName; - MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + Metadata = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod(methodName)!); this.invoker = invoker; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs index 1f83681ff..54947b856 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Reflection; using Grpc.Core; +using MagicOnion.Server.Internal; namespace MagicOnion.Server.Binder; @@ -17,14 +18,13 @@ public class MagicOnionDuplexStreamingMethod typeof(TService); public string ServiceName { get; } public string MethodName { get; } - - public MethodInfo MethodInfo { get; } + public MethodHandlerMetadata Metadata { get; } public MagicOnionDuplexStreamingMethod(string serviceName, string methodName, Func invoker) { ServiceName = serviceName; MethodName = methodName; - MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + Metadata = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod(methodName)!); this.invoker = invoker; } @@ -33,7 +33,7 @@ public MagicOnionDuplexStreamingMethod(MagicOnionStreamingHubConnectMethod typeof(TService); public string ServiceName { get; } public string MethodName { get; } - - public MethodInfo MethodInfo { get; } + public MethodHandlerMetadata Metadata { get; } public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func invoker) { ServiceName = serviceName; MethodName = methodName; - MethodInfo = typeof(TService).GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + Metadata = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod(methodName)!); this.invoker = invoker; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs index a2a4207d4..8b051570f 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs @@ -13,13 +13,12 @@ public class MagicOnionStreamingHubConnectMethod : IMagicOnionGrpcMeth public string ServiceName { get; } public string MethodName { get; } - public MethodInfo MethodInfo { get; } + public MethodHandlerMetadata Metadata { get; } = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod("MagicOnion.Server.Internal.IStreamingHubBase.Connect")!); public MagicOnionStreamingHubConnectMethod(string serviceName) { ServiceName = serviceName; MethodName = nameof(IStreamingHubBase.Connect); - MethodInfo = typeof(TService).GetMethod("MagicOnion.Server.Internal.IStreamingHubBase.Connect", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; } public void Bind(IMagicOnionGrpcMethodBinder binder) diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs index 81f847d54..a41354df1 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Reflection; using MagicOnion.Server.Hubs; +using MagicOnion.Server.Internal; namespace MagicOnion.Server.Binder; @@ -9,7 +10,7 @@ public interface IMagicOnionStreamingHubMethod { string ServiceName { get; } string MethodName { get; } - MethodInfo MethodInfo { get; } + StreamingHubMethodHandlerMetadata Metadata { get; } ValueTask InvokeAsync(StreamingHubContext context); } @@ -18,7 +19,7 @@ public class MagicOnionStreamingHubMethod : IMagi { public string ServiceName { get; } public string MethodName { get; } - public MethodInfo MethodInfo { get; } + public StreamingHubMethodHandlerMetadata Metadata { get; } readonly Func> invoker; @@ -29,7 +30,7 @@ public MagicOnionStreamingHubMethod(string serviceName, string methodName, Deleg this.ServiceName = serviceName; this.MethodName = methodName; - this.MethodInfo = typeof(TService).GetMethod(MethodName) ?? throw new InvalidOperationException(); + this.Metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod(MethodName) ?? throw new InvalidOperationException()); if (invoker is Func> invokerTask) { @@ -64,7 +65,7 @@ public class MagicOnionStreamingHubMethod : IMagicOnionStrea { public string ServiceName { get; } public string MethodName { get; } - public MethodInfo MethodInfo { get; } + public StreamingHubMethodHandlerMetadata Metadata { get; } readonly Func invoker; @@ -75,7 +76,7 @@ public MagicOnionStreamingHubMethod(string serviceName, string methodName, Deleg this.ServiceName = serviceName; this.MethodName = methodName; - this.MethodInfo = typeof(TService).GetMethod(MethodName) ?? throw new InvalidOperationException(); + this.Metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod(MethodName) ?? throw new InvalidOperationException()); if (invoker is Func invokerTask) { diff --git a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs index 17b695cc0..7248d6401 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs @@ -2,6 +2,7 @@ using System.Reflection; using Grpc.Core; using MagicOnion.Internal; +using MagicOnion.Server.Internal; using MessagePack; namespace MagicOnion.Server.Binder; @@ -27,6 +28,7 @@ public abstract class MagicOnionUnaryMethodBase serviceName; public string MethodName => methodName; + public abstract MethodHandlerMetadata Metadata { get; } public MethodInfo MethodInfo { get; } = typeof(TService).GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; public void Bind(IMagicOnionGrpcMethodBinder binder) @@ -88,6 +90,8 @@ public sealed class MagicOnionUnaryMethod SetUnaryResult(invoker(service, context, request), context); } @@ -98,6 +102,8 @@ public sealed class MagicOnionUnaryMethod(strin where TService : class where TRawRequest : class { + public override MethodHandlerMetadata Metadata { get; } = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod(methodName)!); + public override ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) => SetUnaryResultNonGeneric(invoker(service, context, request), context); } diff --git a/src/MagicOnion.Server/Filters/Internal/FilterHelper.cs b/src/MagicOnion.Server/Filters/Internal/FilterHelper.cs index 7d96714d8..0fd07536d 100644 --- a/src/MagicOnion.Server/Filters/Internal/FilterHelper.cs +++ b/src/MagicOnion.Server/Filters/Internal/FilterHelper.cs @@ -6,11 +6,14 @@ namespace MagicOnion.Server.Filters.Internal; internal class FilterHelper { public static IReadOnlyList GetFilters(IEnumerable globalFilters, Type classType, MethodInfo methodInfo) + => GetFilters(globalFilters, classType.GetCustomAttributes(inherit: true).Concat(methodInfo.GetCustomAttributes(inherit: true)).OfType()); + + public static IReadOnlyList GetFilters(IEnumerable globalFilters, IEnumerable attributes) { // Filters are sorted in the following order: // [Manually ordered filters] -> [Global Filters] -> [Class Filters] -> [Method Filters] // The filters has `int.MaxValue` as order by default. If the user specifies an order, it will take precedence. - var attributedFilters = classType.GetCustomAttributes(inherit: true).Concat(methodInfo.GetCustomAttributes(inherit: true)) + var attributedFilters = attributes .OfType() .Where(x => x is IMagicOnionServiceFilter or IMagicOnionFilterFactory) .Select(x => @@ -34,11 +37,14 @@ IMagicOnionFilterFactory filterFactory } public static IReadOnlyList GetFilters(IEnumerable globalFilters, Type classType, MethodInfo methodInfo) + => GetFilters(globalFilters, classType.GetCustomAttributes(inherit: true).Concat(methodInfo.GetCustomAttributes(inherit: true)).OfType()); + + public static IReadOnlyList GetFilters(IEnumerable globalFilters, IEnumerable attributes) { // Filters are sorted in the following order: // [Manually ordered filters] -> [Global Filters] -> [Class Filters] -> [Method Filters] // The filters has `int.MaxValue` as order by default. If the user specifies an order, it will take precedence. - var attributedFilters = classType.GetCustomAttributes(inherit: true).Concat(methodInfo.GetCustomAttributes(inherit: true)) + var attributedFilters = attributes .OfType() .Where(x => x is IStreamingHubFilter or IMagicOnionFilterFactory) .Select(x => @@ -100,4 +106,4 @@ public static TFilter CreateOrGetInstance(IServiceProvider serviceProvi throw new InvalidOperationException($"MagicOnionFilterDescriptor requires instance or factory. but the descriptor has '{descriptor.Filter.GetType()}'"); } } -} \ No newline at end of file +} diff --git a/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs b/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs index 62492d496..155c1f3a6 100644 --- a/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs +++ b/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs @@ -38,7 +38,7 @@ public void RegisterMethods(IEnumerable methods) { var streamingHubHandlerOptions = new StreamingHubHandlerOptions(options); var methodAndIdPairs = methods - .Select(x => new StreamingHubHandler(typeof(TService), x, streamingHubHandlerOptions, serviceProvider)) + .Select(x => new StreamingHubHandler(x, streamingHubHandlerOptions, serviceProvider)) .Select(x => (x.MethodId, x)) .ToArray(); diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs index b4f45591d..5f9a61fb1 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs @@ -1,7 +1,6 @@ using System.Reflection; using MagicOnion.Server.Filters; using MagicOnion.Server.Filters.Internal; -using MagicOnion.Server.Internal; using MagicOnion.Serialization; using MagicOnion.Server.Binder; @@ -9,29 +8,29 @@ namespace MagicOnion.Server.Hubs; public class StreamingHubHandler : IEquatable { - readonly StreamingHubMethodHandlerMetadata metadata; + readonly IMagicOnionStreamingHubMethod hubMethod; readonly string toStringCache; readonly int getHashCodeCache; - public string HubName => metadata.StreamingHubInterfaceType.Name; - public Type HubType => metadata.StreamingHubImplementationType; - public MethodInfo MethodInfo => metadata.ImplementationMethod; - public int MethodId => metadata.MethodId; + public string HubName => hubMethod.Metadata.StreamingHubInterfaceType.Name; + public Type HubType => hubMethod.Metadata.StreamingHubImplementationType; + public MethodInfo MethodInfo => hubMethod.Metadata.ImplementationMethod; + public int MethodId => hubMethod.Metadata.MethodId; - public ILookup AttributeLookup => metadata.AttributeLookup; + public ILookup AttributeLookup => hubMethod.Metadata.AttributeLookup; - internal Type RequestType => metadata.RequestType; + internal Type RequestType => hubMethod.Metadata.RequestType; internal Func MethodBody { get; } - public StreamingHubHandler(Type implementationType, IMagicOnionStreamingHubMethod hubMethod, StreamingHubHandlerOptions handlerOptions, IServiceProvider serviceProvider) + public StreamingHubHandler(IMagicOnionStreamingHubMethod hubMethod, StreamingHubHandlerOptions handlerOptions, IServiceProvider serviceProvider) { - this.metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(implementationType, hubMethod.MethodInfo); + this.hubMethod = hubMethod; this.toStringCache = HubName + "/" + MethodInfo.Name; this.getHashCodeCache = HashCode.Combine(HubName, MethodInfo.Name); try { - var filters = FilterHelper.GetFilters(handlerOptions.GlobalStreamingHubFilters, implementationType, hubMethod.MethodInfo); + var filters = FilterHelper.GetFilters(handlerOptions.GlobalStreamingHubFilters, hubMethod.Metadata.Attributes); this.MethodBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, hubMethod.InvokeAsync); } catch (Exception ex) diff --git a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs index 40311af37..b6c269ff1 100644 --- a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs +++ b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs @@ -5,7 +5,7 @@ namespace MagicOnion.Server.Internal; -public readonly struct MethodHandlerMetadata +public class MethodHandlerMetadata { public Type ServiceImplementationType { get; } public MethodInfo ServiceMethod { get; } @@ -15,6 +15,7 @@ public readonly struct MethodHandlerMetadata public Type RequestType { get; } public IReadOnlyList Parameters { get; } public Type ServiceInterface { get; } + public IReadOnlyList Attributes { get; } public ILookup AttributeLookup { get; } public bool IsResultTypeTask { get; } @@ -26,7 +27,7 @@ public MethodHandlerMetadata( Type requestType, IReadOnlyList parameters, Type serviceInterface, - ILookup attributeLookup, + IReadOnlyList attributes, bool isResultTypeTask ) { @@ -38,12 +39,13 @@ bool isResultTypeTask RequestType = requestType; Parameters = parameters; ServiceInterface = serviceInterface; - AttributeLookup = attributeLookup; + Attributes = attributes; + AttributeLookup = attributes.ToLookup(x => x.GetType()); IsResultTypeTask = isResultTypeTask; } } -public readonly struct StreamingHubMethodHandlerMetadata +public class StreamingHubMethodHandlerMetadata { public int MethodId { get; } public Type StreamingHubImplementationType { get; } @@ -54,8 +56,9 @@ public readonly struct StreamingHubMethodHandlerMetadata public Type RequestType { get; } public IReadOnlyList Parameters { get; } public ILookup AttributeLookup { get; } + public IReadOnlyList Attributes { get; } - public StreamingHubMethodHandlerMetadata(int methodId, Type streamingHubImplementationType, MethodInfo interfaceMethodInfo, MethodInfo implementationMethodInfo, Type? responseType, Type requestType, IReadOnlyList parameters, Type streamingHubInterfaceType, ILookup attributeLookup) + public StreamingHubMethodHandlerMetadata(int methodId, Type streamingHubImplementationType, MethodInfo interfaceMethodInfo, MethodInfo implementationMethodInfo, Type? responseType, Type requestType, IReadOnlyList parameters, Type streamingHubInterfaceType, IReadOnlyList attributes) { MethodId = methodId; StreamingHubImplementationType = streamingHubImplementationType; @@ -65,7 +68,8 @@ public StreamingHubMethodHandlerMetadata(int methodId, Type streamingHubImplemen RequestType = requestType; Parameters = parameters; StreamingHubInterfaceType = streamingHubInterfaceType; - AttributeLookup = attributeLookup; + AttributeLookup = attributes.ToLookup(x => x.GetType()); + Attributes = attributes; } } @@ -78,17 +82,17 @@ public static MethodHandlerMetadata CreateServiceMethodHandlerMetadata(Type serv var responseType = UnwrapUnaryResponseType(methodInfo, out var methodType, out var responseIsTask, out var requestTypeIfExists); var requestType = requestTypeIfExists ?? GetRequestTypeFromMethod(methodInfo, parameters); - var attributeLookup = serviceClass.GetCustomAttributes(true) + var attributes = serviceClass.GetCustomAttributes(true) .Concat(methodInfo.GetCustomAttributes(true)) .Cast() - .ToLookup(x => x.GetType()); + .ToArray(); if (parameters.Any() && methodType is MethodType.ClientStreaming or MethodType.DuplexStreaming) { throw new InvalidOperationException($"{methodType} does not support method parameters. If you need to send some arguments, use request headers instead. (Member:{serviceClass.Name}.{methodInfo.Name})"); } - return new MethodHandlerMetadata(serviceClass, methodInfo, methodType, responseType, requestType, parameters, serviceInterfaceType, attributeLookup, responseIsTask); + return new MethodHandlerMetadata(serviceClass, methodInfo, methodType, responseType, requestType, parameters, serviceInterfaceType, attributes, responseIsTask); } public static StreamingHubMethodHandlerMetadata CreateStreamingHubMethodHandlerMetadata(Type serviceClass, MethodInfo methodInfo) @@ -98,10 +102,10 @@ public static StreamingHubMethodHandlerMetadata CreateStreamingHubMethodHandlerM var responseType = UnwrapStreamingHubResponseType(methodInfo, out var responseIsTaskOrValueTask); var requestType = GetRequestTypeFromMethod(methodInfo, parameters); - var attributeLookup = serviceClass.GetCustomAttributes(true) + var attributes = serviceClass.GetCustomAttributes(true) .Concat(methodInfo.GetCustomAttributes(true)) .Cast() - .ToLookup(x => x.GetType()); + .ToArray(); var interfaceMethodInfo = ResolveInterfaceMethod(serviceClass, hubInterface, methodInfo.Name); @@ -116,7 +120,7 @@ public static StreamingHubMethodHandlerMetadata CreateStreamingHubMethodHandlerM throw new InvalidOperationException($"The '{serviceClass.Name}.{methodInfo.Name}' cannot have MethodId attribute. MethodId attribute must be annotated to a hub interface instead."); } - return new StreamingHubMethodHandlerMetadata(methodId, serviceClass, interfaceMethodInfo, methodInfo, responseType, requestType, parameters, hubInterface, attributeLookup); + return new StreamingHubMethodHandlerMetadata(methodId, serviceClass, interfaceMethodInfo, methodInfo, responseType, requestType, parameters, hubInterface, attributes); } static MethodInfo ResolveInterfaceMethod(Type targetType, Type interfaceType, string targetMethodName) diff --git a/src/MagicOnion.Server/ServiceContext.Streaming.cs b/src/MagicOnion.Server/ServiceContext.Streaming.cs index 044af9f88..3908a6ad5 100644 --- a/src/MagicOnion.Server/ServiceContext.Streaming.cs +++ b/src/MagicOnion.Server/ServiceContext.Streaming.cs @@ -42,7 +42,6 @@ internal class StreamingServiceContext : ServiceContext, IS public StreamingServiceContext( object instance, IMagicOnionGrpcMethod method, - ILookup attributeLookup, ServerCallContext context, IMagicOnionSerializer messageSerializer, MagicOnionMetrics metrics, @@ -50,7 +49,7 @@ public StreamingServiceContext( IServiceProvider serviceProvider, IAsyncStreamReader? requestStream, IServerStreamWriter? responseStream - ) : base(instance, method, attributeLookup, context, messageSerializer, metrics, logger, serviceProvider) + ) : base(instance, method, context, messageSerializer, metrics, logger, serviceProvider) { RequestStream = requestStream; ResponseStream = responseStream; diff --git a/src/MagicOnion.Server/ServiceContext.cs b/src/MagicOnion.Server/ServiceContext.cs index 21fc8d326..3d165aada 100644 --- a/src/MagicOnion.Server/ServiceContext.cs +++ b/src/MagicOnion.Server/ServiceContext.cs @@ -69,10 +69,10 @@ public ConcurrentDictionary Items public string ServiceName => Method.ServiceName; public string MethodName => MethodInfo.Name; - public MethodInfo MethodInfo => Method.MethodInfo; + public MethodInfo MethodInfo => Method.Metadata.ServiceMethod; /// Cached Attributes both service and method. - public ILookup AttributeLookup { get; } + public ILookup AttributeLookup => Method.Metadata.AttributeLookup; public MethodType MethodType => Method.MethodType; @@ -93,7 +93,6 @@ public ConcurrentDictionary Items internal ServiceContext( object instance, IMagicOnionGrpcMethod method, - ILookup attributeLookup, ServerCallContext context, IMagicOnionSerializer messageSerializer, MagicOnionMetrics metrics, @@ -103,7 +102,6 @@ IServiceProvider serviceProvider { this.ContextId = Guid.NewGuid(); this.Instance = instance; - this.AttributeLookup = attributeLookup; this.CallContext = context; this.Timestamp = DateTime.UtcNow; this.MessageSerializer = messageSerializer; diff --git a/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs b/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs index bcd57a5a3..d2412acef 100644 --- a/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs +++ b/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs @@ -43,11 +43,10 @@ public async Task Service_Unary_Invoker_ParameterZero_NoReturnValue() methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterZero_NoReturnValue)); var instance = new Service_MethodsImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, Nil.Default); @@ -66,11 +65,10 @@ public async Task Service_Unary_Invoker_ParameterZero_ReturnValueValueType() methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterZero_ReturnValueValueType)); var instance = new Service_MethodsImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, Nil.Default); @@ -89,11 +87,10 @@ public async Task Service_Unary_Invoker_ParameterZero_ReturnValueRefType() methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterZero_ReturnValueRefType)); var instance = new Service_MethodsImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, Nil.Default); @@ -112,11 +109,10 @@ public async Task Service_Unary_Invoker_ParameterMany_NoReturnValue() methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterMany_NoReturnValue)); var instance = new Service_MethodsImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, new DynamicArgumentTuple("Hello", 12345, true)); @@ -135,11 +131,10 @@ public async Task Service_Unary_Invoker_ParameterMany_ReturnValueValueType() methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterMany_ReturnValueValueType)); var instance = new Service_MethodsImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, new DynamicArgumentTuple("Hello", 12345, true)); @@ -158,11 +153,10 @@ public async Task Service_Unary_Invoker_ParameterMany_ReturnValueRefType() methods.Single(x => x.MethodName == nameof(Service_MethodsImpl.IServiceDef.Unary_ParameterMany_ReturnValueRefType)); var instance = new Service_MethodsImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, new DynamicArgumentTuple("Hello", 12345, true)); diff --git a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs index 5f0a6b862..f73d25ed6 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs @@ -26,11 +26,10 @@ public async Task Unary_Invoker_NoRequest_NoResponse() }); var instance = new ServiceImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, Nil.Default); @@ -55,11 +54,10 @@ public async Task Unary_Invoker_NoRequest_ResponseValueType() }); var instance = new ServiceImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, Nil.Default); @@ -84,11 +82,10 @@ public async Task Unary_Invoker_NoRequest_ResponseRefType() }); var instance = new ServiceImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, Nil.Default); @@ -115,11 +112,10 @@ public async Task Unary_Invoker_RequestValueType_NoResponse() }); var instance = new ServiceImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, 12345); @@ -147,11 +143,10 @@ public async Task Unary_Invoker_RequestValueType_ResponseValueType() }); var instance = new ServiceImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, 12345); @@ -179,11 +174,10 @@ public async Task Unary_Invoker_RequestValueType_ResponseRefType() }); var instance = new ServiceImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); - var serviceContext = new ServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); + var serviceContext = new ServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider); // Act await method.InvokeAsync(instance, serviceContext, 12345); diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs index e1f330c75..1ca6be52b 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs @@ -28,7 +28,7 @@ public async Task Parameterless_Returns_Task() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); var ctx = new StreamingHubContext(); ctx.Initialize(handler, fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(Nil.Default), DateTime.Now, 0); await handler.MethodBody.Invoke(ctx); @@ -66,7 +66,7 @@ public async Task Parameterless_Returns_TaskOfInt32() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); var ctx = new StreamingHubContext(); ctx.Initialize(handler, fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(Nil.Default), DateTime.Now, 0); await handler.MethodBody.Invoke(ctx); @@ -103,7 +103,7 @@ public async Task Parameterless_Returns_ValueTask() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); var ctx = new StreamingHubContext(); ctx.Initialize(handler, fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(Nil.Default), DateTime.Now, 0); await handler.MethodBody.Invoke(ctx); @@ -141,7 +141,7 @@ public async Task Parameterless_Returns_ValueTaskOfInt32() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); var ctx = new StreamingHubContext(); ctx.Initialize(handler, fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(Nil.Default), DateTime.Now, 0); await handler.MethodBody.Invoke(ctx); @@ -179,7 +179,7 @@ public async Task Parameter_Single_Returns_Task() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); var ctx = new StreamingHubContext(); ctx.Initialize(handler, fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(12345), DateTime.Now, 0); await handler.MethodBody.Invoke(ctx); @@ -217,7 +217,7 @@ public async Task Parameter_Multiple_Returns_Task() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); var ctx = new StreamingHubContext(); ctx.Initialize(handler, fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(new DynamicArgumentTuple(12345, "テスト", true)), DateTime.Now, 0); await handler.MethodBody.Invoke(ctx); @@ -254,7 +254,7 @@ public async Task Parameter_Multiple_Returns_TaskOfInt32() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); var ctx = new StreamingHubContext(); ctx.Initialize(handler, fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(new DynamicArgumentTuple(12345, "テスト", true)), DateTime.Now, 0); await handler.MethodBody.Invoke(ctx); @@ -291,7 +291,7 @@ public async Task CallRepeated_Parameter_Multiple_Returns_TaskOfInt32() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); for (var i = 0; i < 3; i++) { var ctx = new StreamingHubContext(); @@ -336,7 +336,7 @@ public async Task Parameterless_Void() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); var ctx = new StreamingHubContext(); ctx.Initialize(handler, fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(Nil.Default), DateTime.Now, 0); await handler.MethodBody.Invoke(ctx); @@ -374,7 +374,7 @@ public async Task Parameter_Single_Void() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); var ctx = new StreamingHubContext(); ctx.Initialize(handler, fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(12345), DateTime.Now, 0); await handler.MethodBody.Invoke(ctx); @@ -412,7 +412,7 @@ public async Task Parameter_Multiple_Void() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); var ctx = new StreamingHubContext(); ctx.Initialize(handler, fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(new DynamicArgumentTuple(12345, "テスト", true)), DateTime.Now, 0); await handler.MethodBody.Invoke(ctx); @@ -450,7 +450,7 @@ public async Task Parameter_Multiple_Void_Without_MessageId() var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethodInfo, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); var ctx = new StreamingHubContext(); ctx.Initialize(handler, fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(new DynamicArgumentTuple(12345, "テスト", true)), DateTime.Now, -1 /* The client requires no response */); await handler.MethodBody.Invoke(ctx); @@ -478,7 +478,7 @@ public async Task UseCustomMessageSerializer() serializer.Serialize(bufferWriter, new DynamicArgumentTuple(12345, "テスト", true)); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions() + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions() { MessageSerializer = XorMessagePackMagicOnionSerializerProvider.Instance, }), serviceProvider); @@ -516,7 +516,7 @@ public void MethodAttributeLookup() static (instance, context, _) => instance.Method_Attribute()); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); // Assert Assert.NotEmpty(handler.AttributeLookup); diff --git a/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs index ec1ceb9bb..bb73a0275 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs @@ -294,6 +294,24 @@ public void AttributeLookup_Class_Many_Multiple() metadata.AttributeLookup[typeof(MySecondAttribute)].Should().Equal(new MySecondAttribute(0), new MySecondAttribute(1), new MySecondAttribute(2)); } + [Fact] + public void Attribute_Class_Order() + { + // Arrange + var serviceType = typeof(MyHub_AttributeLookupWithClassAttriubte); + var methodInfo = serviceType.GetMethod(nameof(MyHub_AttributeLookupWithClassAttriubte.Attribute_Many_Multiple))!; + + // Act + var metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(serviceType, methodInfo); + + // Assert + metadata.Attributes.Should().HaveCount(5); + metadata.Attributes.Select(x => x.GetType().Name).Should().Equal([ /* Class */ nameof(MyThirdAttribute), /* Method */ nameof(MyFirstAttribute), nameof(MySecondAttribute), nameof(MySecondAttribute), nameof(MySecondAttribute)]); + metadata.Attributes[2].Should().BeOfType().Subject.Value.Should().Be(0); + metadata.Attributes[3].Should().BeOfType().Subject.Value.Should().Be(1); + metadata.Attributes[4].Should().BeOfType().Subject.Value.Should().Be(2); + } + [Fact] public void ValueTask() { From 556ef71a47e8a74783d4452fd57e2ebd96d64cc6 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Mon, 21 Oct 2024 15:05:56 +0900 Subject: [PATCH 20/27] Rename property name --- .../Internal/MagicOnionGrpcMethodBinder.cs | 10 +++---- .../Internal/MethodHandlerMetadata.cs | 11 +++----- src/MagicOnion.Server/ServiceContext.cs | 2 +- .../MethodHandlerMetadataFactoryTest.cs | 26 +++++++++---------- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs index bbd99fcf0..6ee24f453 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs @@ -36,7 +36,7 @@ public void BindUnary(IMagicOnio where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.Unary, method.Metadata.ServiceMethod); + var messageSerializer = messageSerializerProvider.Create(MethodType.Unary, method.Metadata.ServiceImplementationMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.Unary, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -47,7 +47,7 @@ public void BindClientStreaming( where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.ClientStreaming, method.Metadata.ServiceMethod); + var messageSerializer = messageSerializerProvider.Create(MethodType.ClientStreaming, method.Metadata.ServiceImplementationMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ClientStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -58,7 +58,7 @@ public void BindServerStreaming( where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.ServerStreaming, method.Metadata.ServiceMethod); + var messageSerializer = messageSerializerProvider.Create(MethodType.ServerStreaming, method.Metadata.ServiceImplementationMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ServerStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -69,7 +69,7 @@ public void BindDuplexStreaming( where TRawRequest : class where TRawResponse : class { - var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.Metadata.ServiceMethod); + var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.Metadata.ServiceImplementationMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.DuplexStreaming, method.ServiceName, method.MethodName, messageSerializer); var attrs = GetMetadataFromHandler(method); @@ -78,7 +78,7 @@ public void BindDuplexStreaming( public void BindStreamingHub(MagicOnionStreamingHubConnectMethod method) { - var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.Metadata.ServiceMethod); + var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.Metadata.ServiceImplementationMethod); // StreamingHub uses the special marshallers for streaming messages serialization. // TODO: Currently, MagicOnion expects TRawRequest/TRawResponse to be raw-byte array (`StreamingHubPayload`). var grpcMethod = new Method( diff --git a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs index b6c269ff1..6dc2e050b 100644 --- a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs +++ b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs @@ -8,7 +8,7 @@ namespace MagicOnion.Server.Internal; public class MethodHandlerMetadata { public Type ServiceImplementationType { get; } - public MethodInfo ServiceMethod { get; } + public MethodInfo ServiceImplementationMethod { get; } public MethodType MethodType { get; } public Type ResponseType { get; } @@ -17,7 +17,6 @@ public class MethodHandlerMetadata public Type ServiceInterface { get; } public IReadOnlyList Attributes { get; } public ILookup AttributeLookup { get; } - public bool IsResultTypeTask { get; } public MethodHandlerMetadata( Type serviceImplementationType, @@ -27,12 +26,11 @@ public MethodHandlerMetadata( Type requestType, IReadOnlyList parameters, Type serviceInterface, - IReadOnlyList attributes, - bool isResultTypeTask + IReadOnlyList attributes ) { ServiceImplementationType = serviceImplementationType; - ServiceMethod = serviceMethod; + ServiceImplementationMethod = serviceMethod; MethodType = methodType; ResponseType = responseType; @@ -41,7 +39,6 @@ bool isResultTypeTask ServiceInterface = serviceInterface; Attributes = attributes; AttributeLookup = attributes.ToLookup(x => x.GetType()); - IsResultTypeTask = isResultTypeTask; } } @@ -92,7 +89,7 @@ public static MethodHandlerMetadata CreateServiceMethodHandlerMetadata(Type serv throw new InvalidOperationException($"{methodType} does not support method parameters. If you need to send some arguments, use request headers instead. (Member:{serviceClass.Name}.{methodInfo.Name})"); } - return new MethodHandlerMetadata(serviceClass, methodInfo, methodType, responseType, requestType, parameters, serviceInterfaceType, attributes, responseIsTask); + return new MethodHandlerMetadata(serviceClass, methodInfo, methodType, responseType, requestType, parameters, serviceInterfaceType, attributes); } public static StreamingHubMethodHandlerMetadata CreateStreamingHubMethodHandlerMetadata(Type serviceClass, MethodInfo methodInfo) diff --git a/src/MagicOnion.Server/ServiceContext.cs b/src/MagicOnion.Server/ServiceContext.cs index 3d165aada..11fa3199b 100644 --- a/src/MagicOnion.Server/ServiceContext.cs +++ b/src/MagicOnion.Server/ServiceContext.cs @@ -69,7 +69,7 @@ public ConcurrentDictionary Items public string ServiceName => Method.ServiceName; public string MethodName => MethodInfo.Name; - public MethodInfo MethodInfo => Method.Metadata.ServiceMethod; + public MethodInfo MethodInfo => Method.Metadata.ServiceImplementationMethod; /// Cached Attributes both service and method. public ILookup AttributeLookup => Method.Metadata.AttributeLookup; diff --git a/tests/MagicOnion.Server.Tests/MethodHandlerMetadataFactoryTest.cs b/tests/MagicOnion.Server.Tests/MethodHandlerMetadataFactoryTest.cs index e8f64c92b..c066f7ff3 100644 --- a/tests/MagicOnion.Server.Tests/MethodHandlerMetadataFactoryTest.cs +++ b/tests/MagicOnion.Server.Tests/MethodHandlerMetadataFactoryTest.cs @@ -167,7 +167,7 @@ public void Unary_Parameterless() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.Unary); metadata.RequestType.Should().Be(); @@ -186,7 +186,7 @@ public void Unary_Parameter_One() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.Unary); metadata.RequestType.Should().Be(); @@ -205,7 +205,7 @@ public void Unary_Parameter_Many() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.Unary); metadata.RequestType.Should().Be>(); @@ -224,7 +224,7 @@ public void ServerStreaming_Parameterless() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ServerStreaming); metadata.RequestType.Should().Be(); @@ -243,7 +243,7 @@ public void ServerStreaming_Parameter_One() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ServerStreaming); metadata.RequestType.Should().Be(); @@ -262,7 +262,7 @@ public void ServerStreaming_Parameter_Many() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ServerStreaming); metadata.RequestType.Should().Be>(); @@ -282,7 +282,7 @@ public void ServerStreaming_Sync_Parameterless() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ServerStreaming); metadata.IsResultTypeTask.Should().BeFalse(); @@ -302,7 +302,7 @@ public void ServerStreaming_Sync_Parameter_One() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ServerStreaming); metadata.IsResultTypeTask.Should().BeFalse(); @@ -322,7 +322,7 @@ public void ServerStreaming_Sync_Parameter_Many() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ServerStreaming); metadata.IsResultTypeTask.Should().BeFalse(); @@ -342,7 +342,7 @@ public void ClientStreaming() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ClientStreaming); metadata.IsResultTypeTask.Should().BeTrue(); @@ -377,7 +377,7 @@ public void ClientStreaming_Sync() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ClientStreaming); metadata.IsResultTypeTask.Should().BeFalse(); @@ -412,7 +412,7 @@ public void DuplexStreaming() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.DuplexStreaming); metadata.IsResultTypeTask.Should().BeTrue(); @@ -447,7 +447,7 @@ public void DuplexStreaming_Sync() // Assert metadata.ServiceImplementationType.Should().Be(); - metadata.ServiceMethod.Should().BeSameAs(methodInfo); + metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.DuplexStreaming); metadata.IsResultTypeTask.Should().BeFalse(); From 89e499e1afd8a620361dcaa2787f246317eeed8a Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Mon, 21 Oct 2024 16:17:02 +0900 Subject: [PATCH 21/27] Cleanup --- .../Binder/IMagicOnionGrpcMethod.cs | 1 - .../Internal/MagicOnionGrpcMethodBinder.cs | 28 ++++--------------- .../Internal/MagicOnionGrpcMethodHandler.cs | 15 +++------- .../Hubs/StreamingHubHandler.cs | 5 ---- .../Internal/MagicOnionServicesDiscoverer.cs | 2 +- src/MagicOnion.Server/Service.cs | 4 +-- 6 files changed, 12 insertions(+), 43 deletions(-) diff --git a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs index 4f383fd15..5de95cf5d 100644 --- a/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs @@ -1,4 +1,3 @@ -using System.Reflection; using Grpc.Core; using MagicOnion.Server.Internal; diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs index 6ee24f453..0e256288c 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs @@ -38,9 +38,8 @@ public void BindUnary(IMagicOnio { var messageSerializer = messageSerializerProvider.Create(MethodType.Unary, method.Metadata.ServiceImplementationMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.Unary, method.ServiceName, method.MethodName, messageSerializer); - var attrs = GetMetadataFromHandler(method); - providerContext.AddUnaryMethod(grpcMethod, attrs, handlerBuilder.BuildUnaryMethod(method, messageSerializer, attrs)); + providerContext.AddUnaryMethod(grpcMethod, method.Metadata.Attributes.OfType().ToArray(), handlerBuilder.BuildUnaryMethod(method, messageSerializer)); } public void BindClientStreaming(MagicOnionClientStreamingMethod method) @@ -49,9 +48,8 @@ public void BindClientStreaming( { var messageSerializer = messageSerializerProvider.Create(MethodType.ClientStreaming, method.Metadata.ServiceImplementationMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ClientStreaming, method.ServiceName, method.MethodName, messageSerializer); - var attrs = GetMetadataFromHandler(method); - providerContext.AddClientStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildClientStreamingMethod(method, messageSerializer, attrs)); + providerContext.AddClientStreamingMethod(grpcMethod, method.Metadata.Attributes.OfType().ToArray(), handlerBuilder.BuildClientStreamingMethod(method, messageSerializer)); } public void BindServerStreaming(MagicOnionServerStreamingMethod method) @@ -60,9 +58,8 @@ public void BindServerStreaming( { var messageSerializer = messageSerializerProvider.Create(MethodType.ServerStreaming, method.Metadata.ServiceImplementationMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ServerStreaming, method.ServiceName, method.MethodName, messageSerializer); - var attrs = GetMetadataFromHandler(method); - providerContext.AddServerStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildServerStreamingMethod(method, messageSerializer, attrs)); + providerContext.AddServerStreamingMethod(grpcMethod, method.Metadata.Attributes.OfType().ToArray(), handlerBuilder.BuildServerStreamingMethod(method, messageSerializer)); } public void BindDuplexStreaming(MagicOnionDuplexStreamingMethod method) @@ -71,9 +68,8 @@ public void BindDuplexStreaming( { var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.Metadata.ServiceImplementationMethod); var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.DuplexStreaming, method.ServiceName, method.MethodName, messageSerializer); - var attrs = GetMetadataFromHandler(method); - providerContext.AddDuplexStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildDuplexStreamingMethod(method, messageSerializer, attrs)); + providerContext.AddDuplexStreamingMethod(grpcMethod, method.Metadata.Attributes.OfType().ToArray(), handlerBuilder.BuildDuplexStreamingMethod(method, messageSerializer)); } public void BindStreamingHub(MagicOnionStreamingHubConnectMethod method) @@ -88,7 +84,6 @@ public void BindStreamingHub(MagicOnionStreamingHubConnectMethod metho MagicOnionMarshallers.StreamingHubMarshaller, MagicOnionMarshallers.StreamingHubMarshaller ); - var attrs = GetMetadataFromHandler(method); var duplexMethod = new MagicOnionDuplexStreamingMethod( method, @@ -97,19 +92,6 @@ public void BindStreamingHub(MagicOnionStreamingHubConnectMethod metho context.CallContext.GetHttpContext().Features.Set(context.ServiceProvider.GetRequiredService>()); return ((IStreamingHubBase)instance).Connect(); }); - providerContext.AddDuplexStreamingMethod(grpcMethod, attrs, handlerBuilder.BuildDuplexStreamingMethod(duplexMethod, messageSerializer, attrs)); - } - - IList GetMetadataFromHandler(IMagicOnionGrpcMethod magicOnionGrpcMethod) - { - // NOTE: We need to collect Attributes for Endpoint metadata. ([Authorize], [AllowAnonymous] ...) - // https://github.com/grpc/grpc-dotnet/blob/7ef184f3c4cd62fbc3cde55e4bb3e16b58258ca1/src/Grpc.AspNetCore.Server/Model/Internal/ProviderServiceBinder.cs#L89-L98 - var metadata = new List(); - //metadata.AddRange(magicOnionGrpcMethod.ServiceImplementationType.GetCustomAttributes(inherit: true)); - //metadata.AddRange(magicOnionGrpcMethod.Metadata.ServiceMethod.GetCustomAttributes(inherit: true)); - metadata.AddRange(magicOnionGrpcMethod.Metadata.Attributes); - - metadata.Add(new HttpMethodMetadata(["POST"], acceptCorsPreflight: true)); - return metadata; + providerContext.AddDuplexStreamingMethod(grpcMethod, method.Metadata.Attributes.OfType().ToArray(), handlerBuilder.BuildDuplexStreamingMethod(duplexMethod, messageSerializer)); } } diff --git a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs index 3521622a9..ebe95267b 100644 --- a/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodHandler.cs @@ -42,13 +42,11 @@ void InitializeServiceProperties(object instance, ServiceContext serviceContext) public ClientStreamingServerMethod BuildClientStreamingMethod( MagicOnionClientStreamingMethod method, - IMagicOnionSerializer messageSerializer, - IList metadata + IMagicOnionSerializer messageSerializer ) where TRawRequest : class where TRawResponse : class { - var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); var filters = FilterHelper.GetFilters(globalFilters, method.Metadata.Attributes); var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext)); @@ -120,13 +118,11 @@ async Task InvokeAsync(TService instance, IAsyncStreamReader BuildServerStreamingMethod( MagicOnionServerStreamingMethod method, - IMagicOnionSerializer messageSerializer, - IList metadata + IMagicOnionSerializer messageSerializer ) where TRawRequest : class where TRawResponse : class { - var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); var filters = FilterHelper.GetFilters(globalFilters, method.Metadata.Attributes); var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext, (TRequest)serviceContext.Request!)); @@ -192,13 +188,11 @@ async Task InvokeAsync(TService instance, TRawRequest rawRequest, IServerStreamW public DuplexStreamingServerMethod BuildDuplexStreamingMethod( MagicOnionDuplexStreamingMethod method, - IMagicOnionSerializer messageSerializer, - IList metadata + IMagicOnionSerializer messageSerializer ) where TRawRequest : class where TRawResponse : class { - var attributeLookup = metadata.OfType().ToLookup(k => k.GetType()); var filters = FilterHelper.GetFilters(globalFilters, method.Metadata.Attributes); var wrappedBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, (serviceContext) => method.InvokeAsync((TService)serviceContext.Instance, serviceContext)); @@ -267,8 +261,7 @@ async Task InvokeAsync(TService instance, IAsyncStreamReader rawReq public UnaryServerMethod BuildUnaryMethod( IMagicOnionUnaryMethod method, - IMagicOnionSerializer messageSerializer, - IList metadata + IMagicOnionSerializer messageSerializer ) where TRawRequest : class where TRawResponse : class diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs index 5f9a61fb1..51a7ae2e6 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs @@ -1,7 +1,6 @@ using System.Reflection; using MagicOnion.Server.Filters; using MagicOnion.Server.Filters.Internal; -using MagicOnion.Serialization; using MagicOnion.Server.Binder; namespace MagicOnion.Server.Hubs; @@ -55,13 +54,9 @@ public bool Equals(StreamingHubHandler? other) public class StreamingHubHandlerOptions { public IList GlobalStreamingHubFilters { get; } - - public IMagicOnionSerializerProvider MessageSerializer { get; } - public StreamingHubHandlerOptions(MagicOnionOptions options) { GlobalStreamingHubFilters = options.GlobalStreamingHubFilters; - MessageSerializer = options.MessageSerializer; } } diff --git a/src/MagicOnion.Server/Internal/MagicOnionServicesDiscoverer.cs b/src/MagicOnion.Server/Internal/MagicOnionServicesDiscoverer.cs index 76729b7b8..4e061511a 100644 --- a/src/MagicOnion.Server/Internal/MagicOnionServicesDiscoverer.cs +++ b/src/MagicOnion.Server/Internal/MagicOnionServicesDiscoverer.cs @@ -78,7 +78,7 @@ public static IEnumerable GetTypesFromAssemblies(IEnumerable sea return x.GetTypes() .Where(x => typeof(IServiceMarker).IsAssignableFrom(x)) .Where(x => x.GetCustomAttribute(false) == null) - .Where(x => x.IsPublic && !x.IsAbstract && !x.IsGenericTypeDefinition); + .Where(x => x is { IsPublic: true, IsAbstract: false, IsGenericTypeDefinition: false }); } catch (ReflectionTypeLoadException) { diff --git a/src/MagicOnion.Server/Service.cs b/src/MagicOnion.Server/Service.cs index f82594817..b2cbc0f66 100644 --- a/src/MagicOnion.Server/Service.cs +++ b/src/MagicOnion.Server/Service.cs @@ -10,8 +10,8 @@ public abstract class ServiceBase : IService Date: Wed, 23 Oct 2024 10:26:48 +0900 Subject: [PATCH 22/27] Fix unit tests --- tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs | 3 +-- .../MethodHandlerMetadataFactoryTest.cs | 7 ------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs index f73d25ed6..aee88f290 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs @@ -208,7 +208,6 @@ public async Task DuplexStreaming_Invoker_RequestValueType_ResponseValueType() }); var instance = new ServiceImpl(); var serverCallContext = Substitute.For(); - var attributeLookup = Array.Empty<(Type, Attribute)>().ToLookup(k => k.Item1, v => v.Item2); var serializer = Substitute.For(); var serviceProvider = Substitute.For(); var metrics = new MagicOnionMetrics(new TestMeterFactory()); @@ -217,7 +216,7 @@ public async Task DuplexStreaming_Invoker_RequestValueType_ResponseValueType() requestStream.Current.Returns(54321); var responseStream = Substitute.For>(); - var serviceContext = new StreamingServiceContext(instance, method, attributeLookup, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider, requestStream, responseStream); + var serviceContext = new StreamingServiceContext(instance, method, serverCallContext, serializer, metrics, NullLogger.Instance, serviceProvider, requestStream, responseStream); // Act await method.InvokeAsync(instance, serviceContext); diff --git a/tests/MagicOnion.Server.Tests/MethodHandlerMetadataFactoryTest.cs b/tests/MagicOnion.Server.Tests/MethodHandlerMetadataFactoryTest.cs index c066f7ff3..6bdd247c2 100644 --- a/tests/MagicOnion.Server.Tests/MethodHandlerMetadataFactoryTest.cs +++ b/tests/MagicOnion.Server.Tests/MethodHandlerMetadataFactoryTest.cs @@ -285,7 +285,6 @@ public void ServerStreaming_Sync_Parameterless() metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ServerStreaming); - metadata.IsResultTypeTask.Should().BeFalse(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } @@ -305,7 +304,6 @@ public void ServerStreaming_Sync_Parameter_One() metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ServerStreaming); - metadata.IsResultTypeTask.Should().BeFalse(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } @@ -325,7 +323,6 @@ public void ServerStreaming_Sync_Parameter_Many() metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ServerStreaming); - metadata.IsResultTypeTask.Should().BeFalse(); metadata.RequestType.Should().Be>(); metadata.ResponseType.Should().Be(); } @@ -345,7 +342,6 @@ public void ClientStreaming() metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ClientStreaming); - metadata.IsResultTypeTask.Should().BeTrue(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } @@ -380,7 +376,6 @@ public void ClientStreaming_Sync() metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.ClientStreaming); - metadata.IsResultTypeTask.Should().BeFalse(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } @@ -415,7 +410,6 @@ public void DuplexStreaming() metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.DuplexStreaming); - metadata.IsResultTypeTask.Should().BeTrue(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } @@ -450,7 +444,6 @@ public void DuplexStreaming_Sync() metadata.ServiceImplementationMethod.Should().BeSameAs(methodInfo); metadata.ServiceInterface.Should().Be(); metadata.MethodType.Should().Be(MethodType.DuplexStreaming); - metadata.IsResultTypeTask.Should().BeFalse(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } From 89f30c25af821bbc235829039fe8d3490b97c100 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Wed, 23 Oct 2024 16:29:25 +0900 Subject: [PATCH 23/27] Fix errors --- .../MagicOnionStreamingHubConnectMethod.cs | 2 +- src/MagicOnion.Server/Hubs/StreamingHub.cs | 4 +- .../Internal/IStreamingHubBase.cs | 4 +- .../MagicOnionApplicationFactory.cs | 27 +++- .../MagicOnionApplicationFactory.cs | 27 +++- .../DynamicMagicOnionMethodProviderTest.cs | 35 ++++++ .../MagicOnionGrpcMethodTest.cs | 41 ++++-- ...MagicOnionGrpcServiceMappingContextTest.cs | 119 +++++++++++++++++- 8 files changed, 238 insertions(+), 21 deletions(-) diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs index 8b051570f..b66723052 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs @@ -13,7 +13,7 @@ public class MagicOnionStreamingHubConnectMethod : IMagicOnionGrpcMeth public string ServiceName { get; } public string MethodName { get; } - public MethodHandlerMetadata Metadata { get; } = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod("MagicOnion.Server.Internal.IStreamingHubBase.Connect")!); + public MethodHandlerMetadata Metadata { get; } = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod("MagicOnion.Server.Internal.IStreamingHubBase.Connect", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!); public MagicOnionStreamingHubConnectMethod(string serviceName) { diff --git a/src/MagicOnion.Server/Hubs/StreamingHub.cs b/src/MagicOnion.Server/Hubs/StreamingHub.cs index 59b9dee05..03ec0f04f 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHub.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHub.cs @@ -81,7 +81,7 @@ protected virtual ValueTask OnDisconnected() return CompletedTask; } - async Task IStreamingHubBase.Connect() + async Task> IStreamingHubBase.Connect() { Metrics.StreamingHubConnectionIncrement(Context.Metrics, Context.ServiceName); @@ -145,6 +145,8 @@ async Task IStreamingHubBase.Connect() heartbeatHandle.Dispose(); remoteClientResultPendingTasks.Dispose(); } + + return default; } async Task HandleMessageAsync() diff --git a/src/MagicOnion.Server/Internal/IStreamingHubBase.cs b/src/MagicOnion.Server/Internal/IStreamingHubBase.cs index 5a2998f21..d8c30ca5a 100644 --- a/src/MagicOnion.Server/Internal/IStreamingHubBase.cs +++ b/src/MagicOnion.Server/Internal/IStreamingHubBase.cs @@ -1,3 +1,5 @@ +using MagicOnion.Internal; + namespace MagicOnion.Server.Internal; internal interface IStreamingHubBase @@ -7,5 +9,5 @@ internal interface IStreamingHubBase /// DO NOT change this name, as it is used as the name to be exposed as gRPC DuplexStreaming. /// /// - Task Connect(); + Task> Connect(); } diff --git a/tests/MagicOnion.Serialization.MemoryPack.Tests/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Serialization.MemoryPack.Tests/MagicOnionApplicationFactory.cs index 9b264d9b4..8dc89cb0a 100644 --- a/tests/MagicOnion.Serialization.MemoryPack.Tests/MagicOnionApplicationFactory.cs +++ b/tests/MagicOnion.Serialization.MemoryPack.Tests/MagicOnionApplicationFactory.cs @@ -1,21 +1,42 @@ +using System.Collections.Concurrent; using MagicOnion.Server; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; namespace MagicOnion.Serialization.MemoryPack.Tests; -#pragma warning disable CS1998 -public class MagicOnionApplicationFactory : WebApplicationFactory +public abstract class MagicOnionApplicationFactory : MagicOnionApplicationFactory { + protected override IEnumerable GetServiceImplementationTypes() => [typeof(TServiceImplementation)]; +} + +public abstract class MagicOnionApplicationFactory : WebApplicationFactory +{ + public const string ItemsKey = "MagicOnionApplicationFactory.Items"; + public ConcurrentDictionary Items => Services.GetRequiredKeyedService>(ItemsKey); + protected override void ConfigureWebHost(IWebHostBuilder builder) { + builder.ConfigureServices(services => { - services.AddMagicOnion(new[] { typeof(TServiceImplementation) }); + services.AddKeyedSingleton>(ItemsKey); + services.AddMagicOnion(); + }); + builder.Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapMagicOnionService([.. GetServiceImplementationTypes()]); + }); }); } + protected abstract IEnumerable GetServiceImplementationTypes(); + public WebApplicationFactory WithMagicOnionOptions(Action configure) { return this.WithWebHostBuilder(x => diff --git a/tests/MagicOnion.Server.Redis.Tests/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Server.Redis.Tests/MagicOnionApplicationFactory.cs index 2680e6526..6932c0006 100644 --- a/tests/MagicOnion.Server.Redis.Tests/MagicOnionApplicationFactory.cs +++ b/tests/MagicOnion.Server.Redis.Tests/MagicOnionApplicationFactory.cs @@ -1,21 +1,42 @@ using MagicOnion.Server; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; namespace MagicOnion.Server.Redis.Tests; -#pragma warning disable CS1998 -public class MagicOnionApplicationFactory : WebApplicationFactory +public abstract class MagicOnionApplicationFactory : MagicOnionApplicationFactory { + protected override IEnumerable GetServiceImplementationTypes() => [typeof(TServiceImplementation)]; +} + +public abstract class MagicOnionApplicationFactory : WebApplicationFactory +{ + public const string ItemsKey = "MagicOnionApplicationFactory.Items"; + public ConcurrentDictionary Items => Services.GetRequiredKeyedService>(ItemsKey); + protected override void ConfigureWebHost(IWebHostBuilder builder) { + builder.ConfigureServices(services => { - services.AddMagicOnion(new[] { typeof(TServiceImplementation) }); + services.AddKeyedSingleton>(ItemsKey); + services.AddMagicOnion(); + }); + builder.Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapMagicOnionService([.. GetServiceImplementationTypes()]); + }); }); } + protected abstract IEnumerable GetServiceImplementationTypes(); + public WebApplicationFactory WithMagicOnionOptions(Action configure) { return this.WithWebHostBuilder(x => diff --git a/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs b/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs index d2412acef..5112a7345 100644 --- a/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs +++ b/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs @@ -384,6 +384,41 @@ static void AssertMethod(Type expectedType, string expectedServiceMethodName, Me class Service_MethodsImpl : ServiceBase, Service_MethodsImpl.IServiceDef { + public UnaryResult Unary_ParameterZero_NoReturnValue() => default; + public UnaryResult Unary_ParameterZero_ReturnValueValueType() => UnaryResult.FromResult(12345); + public UnaryResult Unary_ParameterZero_ReturnValueRefType() => UnaryResult.FromResult("Hello"); + + public UnaryResult Unary_ParameterOneValueType_NoReturnValue(int arg0) => default; + public UnaryResult Unary_ParameterOneValueType_ReturnValueValueType(int arg0) => UnaryResult.FromResult(arg0); + public UnaryResult Unary_ParameterOneValueType_ReturnValueRefType(int arg0) => UnaryResult.FromResult($"{arg0}"); + + public UnaryResult Unary_ParameterOneRefType_NoReturnValue(string arg0) => default; + public UnaryResult Unary_ParameterOneRefType_ReturnValueValueType(string arg0) => UnaryResult.FromResult(int.Parse(arg0)); + public UnaryResult Unary_ParameterOneRefType_ReturnValueRefType(string arg0) => UnaryResult.FromResult($"{arg0}"); + + public UnaryResult Unary_ParameterMany_NoReturnValue(string arg0, int arg1, bool arg2) => default; + public UnaryResult Unary_ParameterMany_ReturnValueValueType(string arg0, int arg1, bool arg2) => UnaryResult.FromResult(HashCode.Combine(arg0, arg1, arg2)); + public UnaryResult Unary_ParameterMany_ReturnValueRefType(string arg0, int arg1, bool arg2) => UnaryResult.FromResult($"{arg0};{arg1};{arg2}"); + + public Task> ClientStreaming_RequestTypeValueType_ResponseTypeValueType() => throw new NotImplementedException(); + public Task> ClientStreaming_RequestTypeRefType_ResponseTypeValueType() => throw new NotImplementedException(); + public Task> ClientStreaming_RequestTypeValueType_ResponseTypeRefType() => throw new NotImplementedException(); + public Task> ClientStreaming_RequestTypeRefType_ResponseTypeRefType() => throw new NotImplementedException(); + + public Task> ServerStreaming_ParameterZero_ResponseTypeValueType() => throw new NotImplementedException(); + public Task> ServerStreaming_ParameterZero_ResponseTypeRefType() => throw new NotImplementedException(); + public Task> ServerStreaming_ParameterOneValueType_ResponseTypeValueType(int arg0) => throw new NotImplementedException(); + public Task> ServerStreaming_ParameterOneValueType_ResponseTypeRefType(int arg0) => throw new NotImplementedException(); + public Task> ServerStreaming_ParameterOneRefType_ResponseTypeValueType(string arg0) => throw new NotImplementedException(); + public Task> ServerStreaming_ParameterOneRefType_ResponseTypeRefType(string arg0) => throw new NotImplementedException(); + public Task> ServerStreaming_ParameterMany_ResponseTypeValueType(string arg0, int arg1, bool arg2) => throw new NotImplementedException(); + public Task> ServerStreaming_ParameterMany_ResponseTypeRefType(string arg0, int arg1, bool arg2) => throw new NotImplementedException(); + + public Task> DuplexStreaming_RequestTypeValueType_ResponseTypeValueType() => throw new NotImplementedException(); + public Task> DuplexStreaming_RequestTypeRefType_ResponseTypeValueType() => throw new NotImplementedException(); + public Task> DuplexStreaming_RequestTypeValueType_ResponseTypeRefType() => throw new NotImplementedException(); + public Task> DuplexStreaming_RequestTypeRefType_ResponseTypeRefType() => throw new NotImplementedException(); + public interface IServiceDef : IService { UnaryResult Unary_ParameterZero_NoReturnValue() => default; diff --git a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs index aee88f290..15f113eab 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs @@ -3,6 +3,7 @@ using MagicOnion.Serialization; using MagicOnion.Server.Binder; using MagicOnion.Server.Diagnostics; +using MagicOnionEngineTest; using MessagePack; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; @@ -18,7 +19,7 @@ public async Task Unary_Invoker_NoRequest_NoResponse() // Arrange var called = false; var invokerArgInstance = default(object); - var method = new MagicOnionUnaryMethod>("IMyService", "MethodName", (instance, context, _) => + var method = new MagicOnionUnaryMethod>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary), (instance, context, _) => { called = true; invokerArgInstance = instance; @@ -46,7 +47,7 @@ public async Task Unary_Invoker_NoRequest_ResponseValueType() // Arrange var called = false; var invokerArgInstance = default(object); - var method = new MagicOnionUnaryMethod, Box>("IMyService", "MethodName", (instance, context, _) => + var method = new MagicOnionUnaryMethod, Box>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary_Parameterless_Int), (instance, context, _) => { called = true; invokerArgInstance = instance; @@ -74,7 +75,7 @@ public async Task Unary_Invoker_NoRequest_ResponseRefType() // Arrange var called = false; var invokerArgInstance = default(object); - var method = new MagicOnionUnaryMethod, string>("IMyService", "MethodName", (instance, context, _) => + var method = new MagicOnionUnaryMethod, string>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary_Parameterless_String), (instance, context, _) => { called = true; invokerArgInstance = instance; @@ -103,7 +104,7 @@ public async Task Unary_Invoker_RequestValueType_NoResponse() var called = false; var invokerArgInstance = default(object); var invokerArgRequest = default(object); - var method = new MagicOnionUnaryMethod>("IMyService", "MethodName", (instance, context, request) => + var method = new MagicOnionUnaryMethod>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary_Int), (instance, context, request) => { called = true; invokerArgInstance = instance; @@ -134,7 +135,7 @@ public async Task Unary_Invoker_RequestValueType_ResponseValueType() var called = false; var invokerArgInstance = default(object); var invokerArgRequest = default(object); - var method = new MagicOnionUnaryMethod, Box>("IMyService", "MethodName", (instance, context, request) => + var method = new MagicOnionUnaryMethod, Box>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary_Int_Int), (instance, context, request) => { called = true; invokerArgInstance = instance; @@ -165,7 +166,7 @@ public async Task Unary_Invoker_RequestValueType_ResponseRefType() var called = false; var invokerArgInstance = default(object); var invokerArgRequest = default(object); - var method = new MagicOnionUnaryMethod, string>("IMyService", "MethodName", (instance, context, request) => + var method = new MagicOnionUnaryMethod, string>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary_Int_String), (instance, context, request) => { called = true; invokerArgInstance = instance; @@ -196,7 +197,7 @@ public async Task DuplexStreaming_Invoker_RequestValueType_ResponseValueType() var called = false; var invokerArgInstance = default(object); var requestCurrentFirst = default(object); - var method = new MagicOnionDuplexStreamingMethod, Box>("IMyService", "MethodName", async (instance, context) => + var method = new MagicOnionDuplexStreamingMethod, Box>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Duplex), async (instance, context) => { called = true; invokerArgInstance = instance; @@ -230,5 +231,29 @@ public async Task DuplexStreaming_Invoker_RequestValueType_ResponseValueType() } - class ServiceImpl; + class ServiceImpl : ServiceBase, ServiceImpl.IMyService + { + public interface IMyService : IService + { + UnaryResult Unary(); + UnaryResult Unary_Int(int arg0); + UnaryResult Unary_String(string arg0); + UnaryResult Unary_Parameterless_Int(); + UnaryResult Unary_Parameterless_String(); + UnaryResult Unary_Int_Int(int arg0); + UnaryResult Unary_Int_String(int arg0); + UnaryResult Unary_String_String(string arg0); + Task> Duplex(); + } + + public UnaryResult Unary() => default; + public UnaryResult Unary_Int(int arg0) => default; + public UnaryResult Unary_String(string arg0) => default; + public UnaryResult Unary_Parameterless_Int() => default; + public UnaryResult Unary_Parameterless_String() => default; + public UnaryResult Unary_Int_Int(int arg0) => UnaryResult.FromResult(0); + public UnaryResult Unary_Int_String(int arg0) => UnaryResult.FromResult(""); + public UnaryResult Unary_String_String(string arg0) => UnaryResult.FromResult(""); + public Task> Duplex() => Task.FromResult(default(DuplexStreamingResult)); + } } diff --git a/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs index 6b5b7dec2..18e677fd1 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs @@ -1,5 +1,7 @@ using MagicOnion.Server.Hubs; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; namespace MagicOnion.Server.Tests; @@ -37,9 +39,118 @@ public void MapMagicOnionService_InvalidType() Assert.IsType(ex); } - public interface IGreeterService : IService; - public interface IGreeterHub : IStreamingHub; + [Fact] + public void MapMagicOnionService_Service() + { + // Arrange + var builder = WebApplication.CreateBuilder(); + builder.Services.AddMagicOnion(); + var app = builder.Build(); + var routeBuilder = new TestEndpointRouteBuilder(app.Services); + + // Act + routeBuilder.MapMagicOnionService([typeof(GreeterService)]); + + // Assert + Assert.Equal(4, routeBuilder.DataSources.First().Endpoints.Count); // HelloAsync + GoodbyeAsync + unimplemented.method + unimplemented.service + Assert.Equal($"gRPC - /{nameof(IGreeterService)}/{nameof(IGreeterService.HelloAsync)}", routeBuilder.DataSources.First().Endpoints[0].DisplayName); + Assert.Equal($"gRPC - /{nameof(IGreeterService)}/{nameof(IGreeterService.GoodbyeAsync)}", routeBuilder.DataSources.First().Endpoints[1].DisplayName); + } + + [Fact] + public void MapMagicOnionService_Hub() + { + // Arrange + var builder = WebApplication.CreateBuilder(); + builder.Services.AddMagicOnion(); + var app = builder.Build(); + var routeBuilder = new TestEndpointRouteBuilder(app.Services); + + // Act + routeBuilder.MapMagicOnionService([typeof(GreeterHub)]); + + // Assert + Assert.Equal(3, routeBuilder.DataSources.First().Endpoints.Count); // Connect + unimplemented.method + unimplemented.service + Assert.Equal($"gRPC - /{nameof(IGreeterHub)}/Connect", routeBuilder.DataSources.First().Endpoints[0].DisplayName); + } + + [Fact] + public void MapMagicOnionService_Metadata() + { + // Arrange + var builder = WebApplication.CreateBuilder(); + builder.Services.AddMagicOnion(); + var app = builder.Build(); + var routeBuilder = new TestEndpointRouteBuilder(app.Services); + + // Act + routeBuilder.MapMagicOnionService([typeof(GreeterService)]).RequireAuthorization(); + + // Assert + var endpoints = routeBuilder.DataSources.First().Endpoints; + var authAttrHelloAsync = endpoints.First(x => x.DisplayName == $"gRPC - /{nameof(IGreeterService)}/{nameof(IGreeterService.HelloAsync)}").Metadata.FirstOrDefault(x => x is AuthorizeAttribute); + var authAttrGoodbyeAsync = endpoints.First(x => x.DisplayName == $"gRPC - /{nameof(IGreeterService)}/{nameof(IGreeterService.GoodbyeAsync)}").Metadata.FirstOrDefault(x => x is AuthorizeAttribute); + var allowAnonymousAttrGoodbyeAsync = endpoints.First(x => x.DisplayName == $"gRPC - /{nameof(IGreeterService)}/{nameof(IGreeterService.GoodbyeAsync)}").Metadata.FirstOrDefault(x => x is AllowAnonymousAttribute); + Assert.NotNull(authAttrHelloAsync); + Assert.NotNull(authAttrGoodbyeAsync); + Assert.NotNull(allowAnonymousAttrGoodbyeAsync); + } + + + [Fact] + public void MapMagicOnionService_MultipleTimes() + { + // Arrange + var builder = WebApplication.CreateBuilder(); + builder.Services.AddMagicOnion(); + var app = builder.Build(); + var routeBuilder = new TestEndpointRouteBuilder(app.Services); + + // Act + routeBuilder.MapMagicOnionService([typeof(GreeterService)]).Add(x => x.Metadata.Add("#1")); + routeBuilder.MapMagicOnionService([typeof(GreeterHub)]).Add(x => x.Metadata.Add("#2")); + + // Assert + Assert.Equal(6, routeBuilder.DataSources.First().Endpoints.Count); // IGreeterService.HelloAsync/GoodbyeAsync + unimplemented.service + IGreeterService.unimplemented method + IGreeterHub.Connect + IGreeterHub.unimplemented method + Assert.Contains("#1", routeBuilder.DataSources.First().Endpoints.First(x => x.DisplayName == $"gRPC - /{nameof(IGreeterService)}/{nameof(IGreeterService.HelloAsync)}").Metadata); + Assert.Contains("#2", routeBuilder.DataSources.First().Endpoints.First(x => x.DisplayName == $"gRPC - /{nameof(IGreeterHub)}/Connect").Metadata); + } + + class TestEndpointRouteBuilder(IServiceProvider serviceProvider) : IEndpointRouteBuilder + { + public IList DataSourcesList { get; } = new List(); + + public IApplicationBuilder CreateApplicationBuilder() + => new ApplicationBuilder(ServiceProvider); + public IServiceProvider ServiceProvider + => serviceProvider; + public ICollection DataSources + => DataSourcesList; + } + + public interface IGreeterService : IService + { + UnaryResult HelloAsync(string name, int age); + UnaryResult GoodbyeAsync(string name, int age); + } + + public interface IGreeterHub : IStreamingHub + { + ValueTask HelloAsync(string name, int age); + ValueTask GoodbyeAsync(string name, int age); + } public interface IGreeterHubReceiver; - public class GreeterService : ServiceBase, IGreeterService; - public class GreeterHub : StreamingHubBase, IGreeterHub; + + public class GreeterService : ServiceBase, IGreeterService + { + public UnaryResult HelloAsync(string name, int age) => UnaryResult.FromResult($"Hello {name} ({age})!"); + [AllowAnonymous] + public UnaryResult GoodbyeAsync(string name, int age) => UnaryResult.FromResult($"Goodbye {name} ({age})!"); + } + + public class GreeterHub : StreamingHubBase, IGreeterHub + { + public ValueTask HelloAsync(string name, int age) => ValueTask.FromResult($"Hello {name} ({age})!"); + public ValueTask GoodbyeAsync(string name, int age) => ValueTask.FromResult($"Goodbye {name} ({age})!"); + } } From 70d7e6a25bb0a01f157fbd7879c6e12fcdcdd8d2 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Wed, 23 Oct 2024 16:31:13 +0900 Subject: [PATCH 24/27] Cleanup usings --- tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs index 15f113eab..ca26ff543 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs @@ -3,10 +3,8 @@ using MagicOnion.Serialization; using MagicOnion.Server.Binder; using MagicOnion.Server.Diagnostics; -using MagicOnionEngineTest; using MessagePack; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; using NSubstitute; namespace MagicOnion.Server.Tests; From 14295353d32c106ebd192b77174a274910a9b931 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Thu, 24 Oct 2024 10:55:20 +0900 Subject: [PATCH 25/27] Throw if there is an overloaded method in the service. --- .../DynamicMagicOnionMethodProvider.cs | 23 ++++++-- .../Binder/MagicOnionClientStreamingMethod.cs | 2 +- .../Binder/MagicOnionDuplexStreamingMethod.cs | 2 +- .../Binder/MagicOnionServerStreamingMethod.cs | 2 +- .../MagicOnionStreamingHubConnectMethod.cs | 2 +- .../Binder/MagicOnionStreamingHubMethod.cs | 4 +- .../Binder/MagicOnionUnaryMethod.cs | 7 +-- .../Internal/MethodHandlerMetadata.cs | 23 ++++++++ .../DynamicMagicOnionMethodProviderTest.cs | 56 +++++++++++++++++++ 9 files changed, 105 insertions(+), 16 deletions(-) diff --git a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs index 419e466ad..0d3099420 100644 --- a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs +++ b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using System.Reflection; +using System.Runtime.ExceptionServices; using MagicOnion.Internal; using MagicOnion.Server.Hubs; using MagicOnion.Server.Internal; @@ -141,8 +142,15 @@ public IReadOnlyList GetGrpcMethods() where TSe invoker = Expression.Lambda(exprCall, [exprParamInstance, exprParamServiceContext]).Compile(); } - var serviceMethod = Activator.CreateInstance(typeMethod.MakeGenericType(typeMethodTypeArgs), [typeServiceInterface.Name, targetMethod.Name, invoker])!; - methods.Add((IMagicOnionGrpcMethod)serviceMethod); + try + { + var serviceMethod = Activator.CreateInstance(typeMethod.MakeGenericType(typeMethodTypeArgs), [typeServiceInterface.Name, targetMethod.Name, invoker])!; + methods.Add((IMagicOnionGrpcMethod)serviceMethod); + } + catch (TargetInvocationException e) + { + ExceptionDispatchInfo.Throw(e.InnerException!); + } } return methods; @@ -206,8 +214,15 @@ public IReadOnlyList GetStreamingHubMethods(methodName); this.invoker = invoker; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs index 54947b856..3d16af0d8 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs @@ -24,7 +24,7 @@ public MagicOnionDuplexStreamingMethod(string serviceName, string methodName, Fu { ServiceName = serviceName; MethodName = methodName; - Metadata = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod(methodName)!); + Metadata = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(methodName); this.invoker = invoker; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs index 9891fc79d..3fbebb1e8 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs @@ -23,7 +23,7 @@ public MagicOnionServerStreamingMethod(string serviceName, string methodName, Fu { ServiceName = serviceName; MethodName = methodName; - Metadata = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod(methodName)!); + Metadata = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(methodName); this.invoker = invoker; } diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs index b66723052..b821d835e 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs @@ -13,7 +13,7 @@ public class MagicOnionStreamingHubConnectMethod : IMagicOnionGrpcMeth public string ServiceName { get; } public string MethodName { get; } - public MethodHandlerMetadata Metadata { get; } = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod("MagicOnion.Server.Internal.IStreamingHubBase.Connect", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!); + public MethodHandlerMetadata Metadata { get; } = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata("MagicOnion.Server.Internal.IStreamingHubBase.Connect"); public MagicOnionStreamingHubConnectMethod(string serviceName) { diff --git a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs index a41354df1..785ac11cd 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs @@ -30,7 +30,7 @@ public MagicOnionStreamingHubMethod(string serviceName, string methodName, Deleg this.ServiceName = serviceName; this.MethodName = methodName; - this.Metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod(MethodName) ?? throw new InvalidOperationException()); + this.Metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(MethodName); if (invoker is Func> invokerTask) { @@ -76,7 +76,7 @@ public MagicOnionStreamingHubMethod(string serviceName, string methodName, Deleg this.ServiceName = serviceName; this.MethodName = methodName; - this.Metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod(MethodName) ?? throw new InvalidOperationException()); + this.Metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(MethodName); if (invoker is Func invokerTask) { diff --git a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs index 7248d6401..4a415fad5 100644 --- a/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs +++ b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs @@ -28,8 +28,7 @@ public abstract class MagicOnionUnaryMethodBase serviceName; public string MethodName => methodName; - public abstract MethodHandlerMetadata Metadata { get; } - public MethodInfo MethodInfo { get; } = typeof(TService).GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; + public MethodHandlerMetadata Metadata { get; } = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(methodName); public void Bind(IMagicOnionGrpcMethodBinder binder) => binder.BindUnary(this); @@ -90,8 +89,6 @@ public sealed class MagicOnionUnaryMethod SetUnaryResult(invoker(service, context, request), context); } @@ -102,8 +99,6 @@ public sealed class MagicOnionUnaryMethod(strin where TService : class where TRawRequest : class { - public override MethodHandlerMetadata Metadata { get; } = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(typeof(TService), typeof(TService).GetMethod(methodName)!); - public override ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) => SetUnaryResultNonGeneric(invoker(service, context, request), context); } diff --git a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs index 6dc2e050b..565f0ff6e 100644 --- a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs +++ b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs @@ -72,6 +72,18 @@ public StreamingHubMethodHandlerMetadata(int methodId, Type streamingHubImplemen internal class MethodHandlerMetadataFactory { + + public static MethodHandlerMetadata CreateServiceMethodHandlerMetadata(string methodName) + { + var methods = typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(x => x.Name == methodName).ToArray(); + switch (methods.Length) + { + case 0: throw new InvalidOperationException($"The method '{methodName}' was not found in the Service '{typeof(T).Name}'"); + case 1: return CreateServiceMethodHandlerMetadata(typeof(T), methods[0]); + default: throw new InvalidOperationException($"There are two or more methods with the same name in the Service '{typeof(T).Name}' (Method: {methodName}). Service does not support method overloading."); + } + } + public static MethodHandlerMetadata CreateServiceMethodHandlerMetadata(Type serviceClass, MethodInfo methodInfo) { var serviceInterfaceType = serviceClass.GetInterfaces().First(x => x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == typeof(IService<>)).GetGenericArguments()[0]; @@ -92,6 +104,17 @@ public static MethodHandlerMetadata CreateServiceMethodHandlerMetadata(Type serv return new MethodHandlerMetadata(serviceClass, methodInfo, methodType, responseType, requestType, parameters, serviceInterfaceType, attributes); } + public static StreamingHubMethodHandlerMetadata CreateStreamingHubMethodHandlerMetadata(string methodName) + { + var methods = typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(x => x.Name == methodName).ToArray(); + switch (methods.Length) + { + case 0: throw new InvalidOperationException($"The method '{methodName}' was not found in the StreamingHub '{typeof(T).Name}'"); + case 1: return CreateStreamingHubMethodHandlerMetadata(typeof(T), methods[0]); + default: throw new InvalidOperationException($"There are two or more methods with the same name in the StreamingHub '{typeof(T).Name}'. StreamingHub does not support method overloading."); + } + } + public static StreamingHubMethodHandlerMetadata CreateStreamingHubMethodHandlerMetadata(Type serviceClass, MethodInfo methodInfo) { var hubInterface = serviceClass.GetInterfaces().First(x => x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == typeof(IStreamingHub<,>)).GetGenericArguments()[0]; diff --git a/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs b/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs index 5112a7345..aed16baf9 100644 --- a/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs +++ b/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs @@ -5,6 +5,7 @@ using System.Reflection.Metadata; using MagicOnion.Serialization; using MagicOnion.Server.Diagnostics; +using MagicOnion.Server.Hubs; using MessagePack; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; @@ -512,4 +513,59 @@ public interface IServiceDef : IService ValueTask MethodAsync() => throw new NotImplementedException(); } } + + [Fact] + public void Service_Invalid_Overload() + { + // Arrange + var provider = new DynamicMagicOnionMethodProvider(); + + // Act + var ex = Record.Exception(() => provider.GetGrpcMethods()); + + // Assert + Assert.NotNull(ex); + Assert.IsType(ex); + } + + + class Service_Invalid_Overload_Impl : ServiceBase, Service_Invalid_Overload_Impl.IServiceDef + { + public UnaryResult MethodAsync() => throw new NotImplementedException(); + public UnaryResult MethodAsync(int arg0) => throw new NotImplementedException(); + + public interface IServiceDef : IService + { + UnaryResult MethodAsync(); + UnaryResult MethodAsync(int arg0); + } + } + + [Fact] + public void StreamingHub_Invalid_Overload() + { + // Arrange + var provider = new DynamicMagicOnionMethodProvider(); + + // Act + var ex = Record.Exception(() => provider.GetStreamingHubMethods()); + + // Assert + Assert.NotNull(ex); + Assert.IsType(ex); + } + + class StreamingHub_Invalid_Overload_Impl : StreamingHubBase, StreamingHub_Invalid_Overload_Impl.IStreamingHub_Invalid_Overload + { + public interface IStreamingHub_Invalid_Overload : IStreamingHub + { + ValueTask MethodAsync(); + ValueTask MethodAsync(int arg0); + } + + public interface IStreamingHub_Invalid_Overload_Receiver; + + public ValueTask MethodAsync() => throw new NotImplementedException(); + public ValueTask MethodAsync(int arg0) => throw new NotImplementedException(); + } } From 2ffa7061ea615c09b73f1ea8a243be16d2320a88 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Thu, 24 Oct 2024 11:17:48 +0900 Subject: [PATCH 26/27] Verify whether the services are registered --- ...MagicOnionEndpointRouteBuilderExtensions.cs | 17 +++++++++++++++++ .../Extensions/MagicOnionServicesExtensions.cs | 8 ++++++++ .../Internal/MagicOnionServiceMarker.cs | 6 ++++++ .../MagicOnionGrpcServiceMappingContextTest.cs | 18 ++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 src/MagicOnion.Server/Internal/MagicOnionServiceMarker.cs diff --git a/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs b/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs index 932acfc24..6ef9ddede 100644 --- a/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs +++ b/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs @@ -15,6 +15,8 @@ public static class MagicOnionEndpointRouteBuilderExtensions /// public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointRouteBuilder builder) { + ThrowIfMagicOnionServicesNotRegistered(builder); + var context = new MagicOnionGrpcServiceMappingContext(builder); foreach (var methodProvider in builder.ServiceProvider.GetServices()) { @@ -31,11 +33,14 @@ public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointRout public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointRouteBuilder builder) where T : class, IServiceMarker { + ThrowIfMagicOnionServicesNotRegistered(builder); + var context = new MagicOnionGrpcServiceMappingContext(builder); context.Map(); return context; } + /// /// Maps specified types as MagicOnion Unary and StreamingHub services to the route builder. /// @@ -43,6 +48,8 @@ public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointR /// public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointRouteBuilder builder, params Type[] serviceTypes) { + ThrowIfMagicOnionServicesNotRegistered(builder); + var context = new MagicOnionGrpcServiceMappingContext(builder); foreach (var t in serviceTypes) { @@ -59,6 +66,8 @@ public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointRout /// public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointRouteBuilder builder, params Assembly[] searchAssemblies) { + ThrowIfMagicOnionServicesNotRegistered(builder); + var context = new MagicOnionGrpcServiceMappingContext(builder); foreach (var t in MagicOnionServicesDiscoverer.GetTypesFromAssemblies(searchAssemblies)) { @@ -67,4 +76,12 @@ public static IEndpointConventionBuilder MapMagicOnionService(this IEndpointRout return context; } + + static void ThrowIfMagicOnionServicesNotRegistered(IEndpointRouteBuilder builder) + { + if (builder.ServiceProvider.GetService() is null) + { + throw new InvalidOperationException("AddMagicOnion must be called to register the services before route mapping."); + } + } } diff --git a/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs index 4aadcc1be..9ad246378 100644 --- a/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs +++ b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs @@ -9,6 +9,7 @@ using MagicOnion.Server.Diagnostics; using MagicOnion.Server.Hubs; using MagicOnion.Server.Hubs.Internal; +using MagicOnion.Server.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -31,6 +32,12 @@ public static IMagicOnionServerBuilder AddMagicOnion(this IServiceCollection ser // NOTE: `internal` is required for unit tests. internal static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollection services, Action? configureOptions = null) { + // Return if the services are already registered. + if (services.Any(x => x.ServiceType == typeof(MagicOnionServiceMarker))) + { + return new MagicOnionServerBuilder(services); + } + var configName = Options.Options.DefaultName; // Required services (ASP.NET Core, gRPC) @@ -39,6 +46,7 @@ internal static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollecti services.AddMetrics(); // MagicOnion: Core services + services.AddSingleton(); services.AddSingleton(typeof(StreamingHubRegistry<>)); services.AddSingleton(typeof(IServiceMethodProvider<>), typeof(MagicOnionGrpcServiceMethodProvider<>)); services.TryAddSingleton(); diff --git a/src/MagicOnion.Server/Internal/MagicOnionServiceMarker.cs b/src/MagicOnion.Server/Internal/MagicOnionServiceMarker.cs new file mode 100644 index 000000000..234c0181e --- /dev/null +++ b/src/MagicOnion.Server/Internal/MagicOnionServiceMarker.cs @@ -0,0 +1,6 @@ +namespace MagicOnion.Server.Internal; + +/// +/// Indicates that MagicOnion.Server services are registered in the service collection. +/// +internal class MagicOnionServiceMarker; diff --git a/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs index 18e677fd1..ca52eaafd 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs @@ -8,6 +8,24 @@ namespace MagicOnion.Server.Tests; public class MagicOnionGrpcServiceMappingContextTest { + [Fact] + public void Throw_NoMagicOnionServicesAreRegistered() + { + // Arrange + var builder = WebApplication.CreateBuilder(); + var app = builder.Build(); + + // Act + var ex1 = Record.Exception(() => app.MapMagicOnionService()); + var ex2 = Record.Exception(() => app.MapMagicOnionService()); + var ex3 = Record.Exception(() => app.MapMagicOnionService([typeof(GreeterService), typeof(GreeterHub)])); + + // Assert + Assert.NotNull(ex1); + Assert.NotNull(ex2); + Assert.NotNull(ex3); + } + [Fact] public void MapMagicOnionService_ValidServiceType() { From d26a7d9d5ce10896466e912936a98ba399cace31 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Thu, 24 Oct 2024 12:54:51 +0900 Subject: [PATCH 27/27] Fix tests --- .../MagicOnionApplicationFactory.cs | 2 +- .../MagicOnionApplicationFactory.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/MagicOnion.Serialization.MemoryPack.Tests/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Serialization.MemoryPack.Tests/MagicOnionApplicationFactory.cs index 8dc89cb0a..633466ff4 100644 --- a/tests/MagicOnion.Serialization.MemoryPack.Tests/MagicOnionApplicationFactory.cs +++ b/tests/MagicOnion.Serialization.MemoryPack.Tests/MagicOnionApplicationFactory.cs @@ -7,7 +7,7 @@ namespace MagicOnion.Serialization.MemoryPack.Tests; -public abstract class MagicOnionApplicationFactory : MagicOnionApplicationFactory +public class MagicOnionApplicationFactory : MagicOnionApplicationFactory { protected override IEnumerable GetServiceImplementationTypes() => [typeof(TServiceImplementation)]; } diff --git a/tests/MagicOnion.Server.Redis.Tests/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Server.Redis.Tests/MagicOnionApplicationFactory.cs index 6932c0006..1b8f39723 100644 --- a/tests/MagicOnion.Server.Redis.Tests/MagicOnionApplicationFactory.cs +++ b/tests/MagicOnion.Server.Redis.Tests/MagicOnionApplicationFactory.cs @@ -7,7 +7,7 @@ namespace MagicOnion.Server.Redis.Tests; -public abstract class MagicOnionApplicationFactory : MagicOnionApplicationFactory +public class MagicOnionApplicationFactory : MagicOnionApplicationFactory { protected override IEnumerable GetServiceImplementationTypes() => [typeof(TServiceImplementation)]; }