diff --git a/perf/BenchmarkApp/PerformanceTest.Shared/ApplicationInformation.cs b/perf/BenchmarkApp/PerformanceTest.Shared/ApplicationInformation.cs index d85640784..843a11262 100644 --- a/perf/BenchmarkApp/PerformanceTest.Shared/ApplicationInformation.cs +++ b/perf/BenchmarkApp/PerformanceTest.Shared/ApplicationInformation.cs @@ -14,8 +14,8 @@ public class ApplicationInformation public string? BenchmarkerVersion { get; } = typeof(ApplicationInformation).Assembly.GetCustomAttribute()?.InformationalVersion; #if SERVER - public string? TagMagicOnionVersion { get; } = RemoveHashFromVersion(typeof(MagicOnion.Server.MagicOnionEngine).Assembly.GetCustomAttribute()?.InformationalVersion); - public string? MagicOnionVersion { get; } = typeof(MagicOnion.Server.MagicOnionEngine).Assembly.GetCustomAttribute()?.InformationalVersion; + public string? TagMagicOnionVersion { get; } = RemoveHashFromVersion(typeof(MagicOnion.Server.ServiceContext).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? TagMagicOnionVersion { get; } = RemoveHashFromVersion(typeof(MagicOnion.Client.MagicOnionClient).Assembly.GetCustomAttribute()?.InformationalVersion); 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.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..5de95cf5d --- /dev/null +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethod.cs @@ -0,0 +1,19 @@ +using Grpc.Core; +using MagicOnion.Server.Internal; + +namespace MagicOnion.Server.Binder; + +public interface IMagicOnionGrpcMethod +{ + MethodType MethodType { get; } + Type ServiceImplementationType { get; } + string ServiceName { get; } + string MethodName { get; } + MethodHandlerMetadata Metadata { get; } +} + +public interface IMagicOnionGrpcMethod : IMagicOnionGrpcMethod + where TService : class +{ + 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..05f5d33c0 --- /dev/null +++ b/src/MagicOnion.Server/Binder/IMagicOnionGrpcMethodProvider.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace MagicOnion.Server.Binder; + +public interface IMagicOnionGrpcMethodProvider +{ + void MapAllSupportedServiceTypes(MagicOnionGrpcServiceMappingContext context); + IReadOnlyList GetGrpcMethods() where TService : class; + IReadOnlyList GetStreamingHubMethods() where TService : class; +} + +public class MagicOnionGrpcServiceMappingContext(IEndpointRouteBuilder builder) : IEndpointConventionBuilder +{ + readonly List innerBuilders = new(); + + public void Map() + where T : class, IServiceMarker + { + innerBuilders.Add(new MagicOnionServiceEndpointConventionBuilder(builder.MapGrpcService())); + } + + public void Map(Type t) + { + VerifyServiceType(t); + + innerBuilders.Add(new MagicOnionServiceEndpointConventionBuilder((GrpcServiceEndpointConventionBuilder)typeof(GrpcEndpointRouteBuilderExtensions) + .GetMethod(nameof(GrpcEndpointRouteBuilderExtensions.MapGrpcService))! + .MakeGenericMethod(t) + .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); + } + } +} diff --git a/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs new file mode 100644 index 000000000..0d3099420 --- /dev/null +++ b/src/MagicOnion.Server/Binder/Internal/DynamicMagicOnionMethodProvider.cs @@ -0,0 +1,254 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.ExceptionServices; +using MagicOnion.Internal; +using MagicOnion.Server.Hubs; +using MagicOnion.Server.Internal; + +namespace MagicOnion.Server.Binder.Internal; + +internal class DynamicMagicOnionMethodProvider : IMagicOnionGrpcMethodProvider +{ + public void MapAllSupportedServiceTypes(MagicOnionGrpcServiceMappingContext context) + { + foreach (var serviceType in MagicOnionServicesDiscoverer.GetTypesFromAssemblies(MagicOnionServicesDiscoverer.GetSearchAssemblies())) + { + context.Map(serviceType); + } + } + + public IReadOnlyList 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(IStreamingHubBase))) + { + 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++) + { + 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(); + + Type? typeMethod = default; + Type[] typeMethodTypeArgs; + Type typeRequest = typeof(object); + Type? typeResponse = default; + if (targetMethod.ReturnType == typeof(UnaryResult)) + { + // UnaryResult: The method has no return value. + typeRequest = CreateRequestType(methodParameters); + typeMethod = typeof(MagicOnionUnaryMethod<,,>); + } + else if (targetMethod.ReturnType is { IsGenericType: true }) + { + 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<,,,,>); + } + } + } + + if (typeMethod is null) + { + throw new InvalidOperationException($"The return type of the service method must be one of 'UnaryResult', 'Task>', 'Task>' or 'Task>'. (TargetMethod={targetMethod})"); + } + + // ***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(); + } + + 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; + } + + public IReadOnlyList GetStreamingHubMethods() where TService : class + { + if (!typeof(TService).IsAssignableTo(typeof(IStreamingHubMarker))) + { + return []; + } + + var methods = new List(); + 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 = CreateRequestType(methodParameters); + var typeResponse = methodInfo.ReturnType; + + Type hubMethodType; + if (typeResponse == typeof(ValueTask) || typeResponse == typeof(Task) || typeResponse == typeof(void)) + { + hubMethodType = typeof(MagicOnionStreamingHubMethod<,>).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 + { + throw new InvalidOperationException("Unsupported method return type. The return type of StreamingHub method must be one of 'void', 'Task', 'ValueTask', 'Task' or 'ValueTask'."); + } + + // 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(); + + try + { + var hubMethod = (IMagicOnionStreamingHubMethod)Activator.CreateInstance(hubMethodType, [typeServiceInterface.Name, methodInfo.Name, invoker])!; + methods.Add(hubMethod); + } + catch (TargetInvocationException e) + { + ExceptionDispatchInfo.Throw(e.InnerException!); + } + } + + return methods; + } + + 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 new file mode 100644 index 000000000..0e256288c --- /dev/null +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcMethodBinder.cs @@ -0,0 +1,97 @@ +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; +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 MagicOnionGrpcMethodHandler handlerBuilder; + + public MagicOnionGrpcMethodBinder(ServiceMethodProviderContext context, MagicOnionOptions options, ILoggerFactory loggerFactory, IServiceProvider serviceProvider) + { + this.providerContext = context; + this.messageSerializerProvider = options.MessageSerializer; + this.handlerBuilder = new MagicOnionGrpcMethodHandler( + options.EnableCurrentContext, + options.IsReturnExceptionStackTraceInErrorDetail, + serviceProvider, + options.GlobalFilters, + loggerFactory.CreateLogger>() + ); + } + + public void BindUnary(IMagicOnionUnaryMethod method) + where TRawRequest : class + where TRawResponse : class + { + var messageSerializer = messageSerializerProvider.Create(MethodType.Unary, method.Metadata.ServiceImplementationMethod); + var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.Unary, method.ServiceName, method.MethodName, messageSerializer); + + providerContext.AddUnaryMethod(grpcMethod, method.Metadata.Attributes.OfType().ToArray(), handlerBuilder.BuildUnaryMethod(method, messageSerializer)); + } + + public void BindClientStreaming(MagicOnionClientStreamingMethod method) + where TRawRequest : class + where TRawResponse : class + { + var messageSerializer = messageSerializerProvider.Create(MethodType.ClientStreaming, method.Metadata.ServiceImplementationMethod); + var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ClientStreaming, method.ServiceName, method.MethodName, messageSerializer); + + providerContext.AddClientStreamingMethod(grpcMethod, method.Metadata.Attributes.OfType().ToArray(), handlerBuilder.BuildClientStreamingMethod(method, messageSerializer)); + } + + public void BindServerStreaming(MagicOnionServerStreamingMethod method) + where TRawRequest : class + where TRawResponse : class + { + var messageSerializer = messageSerializerProvider.Create(MethodType.ServerStreaming, method.Metadata.ServiceImplementationMethod); + var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.ServerStreaming, method.ServiceName, method.MethodName, messageSerializer); + + providerContext.AddServerStreamingMethod(grpcMethod, method.Metadata.Attributes.OfType().ToArray(), handlerBuilder.BuildServerStreamingMethod(method, messageSerializer)); + } + + public void BindDuplexStreaming(MagicOnionDuplexStreamingMethod method) + where TRawRequest : class + where TRawResponse : class + { + var messageSerializer = messageSerializerProvider.Create(MethodType.DuplexStreaming, method.Metadata.ServiceImplementationMethod); + var grpcMethod = GrpcMethodHelper.CreateMethod(MethodType.DuplexStreaming, method.ServiceName, method.MethodName, messageSerializer); + + providerContext.AddDuplexStreamingMethod(grpcMethod, method.Metadata.Attributes.OfType().ToArray(), handlerBuilder.BuildDuplexStreamingMethod(method, messageSerializer)); + } + + public void BindStreamingHub(MagicOnionStreamingHubConnectMethod method) + { + 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( + MethodType.DuplexStreaming, + method.ServiceName, + method.MethodName, + MagicOnionMarshallers.StreamingHubMarshaller, + MagicOnionMarshallers.StreamingHubMarshaller + ); + + var duplexMethod = new MagicOnionDuplexStreamingMethod( + method, + static (instance, context) => + { + context.CallContext.GetHttpContext().Features.Set(context.ServiceProvider.GetRequiredService>()); + return ((IStreamingHubBase)instance).Connect(); + }); + 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 new file mode 100644 index 000000000..ebe95267b --- /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 + ) + where TRawRequest : class + where TRawResponse : class + { + var filters = FilterHelper.GetFilters(globalFilters, method.Metadata.Attributes); + 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 requestServiceProvider = context.GetHttpContext().RequestServices; + var metrics = requestServiceProvider.GetRequiredService(); + var requestStream = new MagicOnionAsyncStreamReader(rawRequestStream); + var serviceContext = new StreamingServiceContext( + instance, + method, + context, + messageSerializer, + metrics, + logger, + requestServiceProvider, + 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 + ) + where TRawRequest : class + where TRawResponse : class + { + 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; + + async Task InvokeAsync(TService instance, TRawRequest rawRequest, IServerStreamWriter rawResponseStream, ServerCallContext context) + { + var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); + var isCompletedSuccessfully = false; + + var requestServiceProvider = context.GetHttpContext().RequestServices; + var metrics = requestServiceProvider.GetRequiredService(); + var request = GrpcMethodHelper.FromRaw(rawRequest); + var responseStream = new MagicOnionServerStreamWriter(rawResponseStream); + var serviceContext = new StreamingServiceContext( + instance, + method, + context, + messageSerializer, + metrics, + logger, + requestServiceProvider, + 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 + ) + where TRawRequest : class + where TRawResponse : class + { + var filters = FilterHelper.GetFilters(globalFilters, method.Metadata.Attributes); + 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 requestServiceProvider = context.GetHttpContext().RequestServices; + var metrics = requestServiceProvider.GetRequiredService(); + var requestStream = new MagicOnionAsyncStreamReader(rawRequestStream); + var responseStream = new MagicOnionServerStreamWriter(rawResponseStream); + var serviceContext = new StreamingServiceContext( + instance, + method, + context, + messageSerializer, + metrics, + logger, + requestServiceProvider, + 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 + ) + where TRawRequest : class + where TRawResponse : class + { + 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; + + async Task InvokeAsync(TService instance, TRawRequest requestRaw, ServerCallContext context) + { + var requestBeginTimestamp = TimeProvider.System.GetTimestamp(); + var isCompletedSuccessfully = false; + + var requestServiceProvider = context.GetHttpContext().RequestServices; + var metrics = requestServiceProvider.GetRequiredService(); + var serviceContext = new ServiceContext(instance, method, context, messageSerializer, metrics, logger, requestServiceProvider); + 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 new file mode 100644 index 000000000..299cef608 --- /dev/null +++ b/src/MagicOnion.Server/Binder/Internal/MagicOnionGrpcServiceMethodProvider.cs @@ -0,0 +1,61 @@ +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 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. + { + 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/MagicOnionClientStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs new file mode 100644 index 000000000..3a2355e86 --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionClientStreamingMethod.cs @@ -0,0 +1,60 @@ +using System.Diagnostics; +using System.Reflection; +using Grpc.Core; +using MagicOnion.Server.Internal; + +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 + where TRawResponse : class +{ + + readonly Func>> invoker; + + public MethodType MethodType => MethodType.ClientStreaming; + public Type ServiceImplementationType => typeof(TService); + public string ServiceName { get; } + public string MethodName { get; } + public MethodHandlerMetadata Metadata { get; } + + public MagicOnionClientStreamingMethod(string serviceName, string methodName, Func>> invoker) + { + ServiceName = serviceName; + MethodName = methodName; + Metadata = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(methodName); + + this.invoker = invoker; + } + + public void Bind(IMagicOnionGrpcMethodBinder binder) + => binder.BindClientStreaming(this); + + public ValueTask InvokeAsync(TService service, ServiceContext context) + => SerializeValueTaskClientStreamingResult(invoker(service, context), context); + + static ValueTask SerializeValueTaskClientStreamingResult(Task> 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(Task> taskResult, ServiceContext context) + { + var result = await taskResult.ConfigureAwait(false); + if (result.hasRawValue) + { + context.Result = result.rawValue; + } + } + } +} diff --git a/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs new file mode 100644 index 000000000..3d16af0d8 --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionDuplexStreamingMethod.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; +using System.Reflection; +using Grpc.Core; +using MagicOnion.Server.Internal; + +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 + where TRawResponse : class +{ + + readonly Func invoker; + + public MethodType MethodType => MethodType.DuplexStreaming; + public Type ServiceImplementationType => typeof(TService); + public string ServiceName { get; } + public string MethodName { get; } + public MethodHandlerMetadata Metadata { get; } + + public MagicOnionDuplexStreamingMethod(string serviceName, string methodName, Func invoker) + { + ServiceName = serviceName; + MethodName = methodName; + Metadata = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(methodName); + + this.invoker = invoker; + } + + public MagicOnionDuplexStreamingMethod(MagicOnionStreamingHubConnectMethod hubConnectMethod, Func invoker) + { + ServiceName = hubConnectMethod.ServiceName; + MethodName = hubConnectMethod.MethodName; + Metadata = hubConnectMethod.Metadata; + + this.invoker = invoker; + } + + public void Bind(IMagicOnionGrpcMethodBinder binder) + => binder.BindDuplexStreaming(this); + + public ValueTask InvokeAsync(TService service, ServiceContext context) + => new(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..3fbebb1e8 --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionServerStreamingMethod.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; +using Grpc.Core; +using MagicOnion.Server.Internal; + +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 + where TRawResponse : class +{ + + readonly Func invoker; + + public MethodType MethodType => MethodType.ServerStreaming; + public Type ServiceImplementationType => typeof(TService); + public string ServiceName { get; } + public string MethodName { get; } + public MethodHandlerMetadata Metadata { get; } + + public MagicOnionServerStreamingMethod(string serviceName, string methodName, Func invoker) + { + ServiceName = serviceName; + MethodName = methodName; + Metadata = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(methodName); + + this.invoker = invoker; + } + + public void Bind(IMagicOnionGrpcMethodBinder binder) + => binder.BindServerStreaming(this); + + public ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) + => new(invoker(service, context, request)); +} diff --git a/src/MagicOnion.Server/Binder/MagicOnionService.cs b/src/MagicOnion.Server/Binder/MagicOnionService.cs deleted file mode 100644 index 9c12c5fc8..000000000 --- a/src/MagicOnion.Server/Binder/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/MagicOnionServiceBinder.cs b/src/MagicOnion.Server/Binder/MagicOnionServiceBinder.cs deleted file mode 100644 index 3b2e29b5b..000000000 --- a/src/MagicOnion.Server/Binder/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.MethodInfo, 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.MethodInfo, 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.MethodInfo, 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.MethodInfo, 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.MethodInfo, 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/MagicOnionServiceMethodProvider.cs b/src/MagicOnion.Server/Binder/MagicOnionServiceMethodProvider.cs deleted file mode 100644 index 67387d518..000000000 --- a/src/MagicOnion.Server/Binder/MagicOnionServiceMethodProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Grpc.AspNetCore.Server.Model; - -namespace MagicOnion.Server.Binder; - -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/MagicOnionStreamingHubConnectMethod.cs b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs new file mode 100644 index 000000000..b821d835e --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubConnectMethod.cs @@ -0,0 +1,26 @@ +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 ServiceImplementationType => typeof(TService); + public string ServiceName { get; } + public string MethodName { get; } + + public MethodHandlerMetadata Metadata { get; } = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata("MagicOnion.Server.Internal.IStreamingHubBase.Connect"); + + public MagicOnionStreamingHubConnectMethod(string serviceName) + { + ServiceName = serviceName; + MethodName = nameof(IStreamingHubBase.Connect); + } + + 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..785ac11cd --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionStreamingHubMethod.cs @@ -0,0 +1,122 @@ +using System.Buffers; +using System.Diagnostics; +using System.Reflection; +using MagicOnion.Server.Hubs; +using MagicOnion.Server.Internal; + +namespace MagicOnion.Server.Binder; + +public interface IMagicOnionStreamingHubMethod +{ + string ServiceName { get; } + string MethodName { get; } + StreamingHubMethodHandlerMetadata Metadata { get; } + + ValueTask InvokeAsync(StreamingHubContext context); +} + +public class MagicOnionStreamingHubMethod : IMagicOnionStreamingHubMethod +{ + public string ServiceName { get; } + public string MethodName { get; } + public StreamingHubMethodHandlerMetadata Metadata { get; } + + 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.Metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(MethodName); + + 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, (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 : IMagicOnionStreamingHubMethod +{ + public string ServiceName { get; } + public string MethodName { get; } + public StreamingHubMethodHandlerMetadata Metadata { 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.Metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(MethodName); + + 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, Action 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 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 new file mode 100644 index 000000000..4a415fad5 --- /dev/null +++ b/src/MagicOnion.Server/Binder/MagicOnionUnaryMethod.cs @@ -0,0 +1,104 @@ +using System.Diagnostics; +using System.Reflection; +using Grpc.Core; +using MagicOnion.Internal; +using MagicOnion.Server.Internal; +using MessagePack; + +namespace MagicOnion.Server.Binder; + +public interface IMagicOnionUnaryMethod : IMagicOnionGrpcMethod + where TService : class + where TRawRequest : class + where TRawResponse : class +{ + ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request); +} + +public abstract class MagicOnionUnaryMethodBase(string serviceName, string methodName) + : IMagicOnionUnaryMethod + where TService : class + where TRawRequest : class + where TRawResponse : class +{ + static readonly object BoxedNil = Nil.Default; + + public MethodType MethodType => MethodType.Unary; + public Type ServiceImplementationType => typeof(TService); + public string ServiceName => serviceName; + public string MethodName => methodName; + + public MethodHandlerMetadata Metadata { get; } = MethodHandlerMetadataFactory.CreateServiceMethodHandlerMetadata(methodName); + + public void Bind(IMagicOnionGrpcMethodBinder binder) + => binder.BindUnary(this); + + public abstract ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request); + + protected static ValueTask SetUnaryResultNonGeneric(UnaryResult result, ServiceContext context) + { + if (result is { hasRawValue: true, rawTaskValue.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; + } + } + + protected 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); + } + } +} + +[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 + where TRawRequest : class + where TRawResponse : class +{ + public override ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) + => 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 + where TRawRequest : class +{ + public override ValueTask InvokeAsync(TService service, ServiceContext context, TRequest request) + => SetUnaryResultNonGeneric(invoker(service, context, request), context); +} 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/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..6ef9ddede 100644 --- a/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs +++ b/src/MagicOnion.Server/Extensions/MagicOnionEndpointRouteBuilderExtensions.cs @@ -1,12 +1,87 @@ +using System.Reflection; +using MagicOnion; using MagicOnion.Server.Binder; +using MagicOnion.Server.Internal; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Builder; public static class MagicOnionEndpointRouteBuilderExtensions { - public static GrpcServiceEndpointConventionBuilder 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) { - return builder.MapGrpcService(); + ThrowIfMagicOnionServicesNotRegistered(builder); + + var context = new MagicOnionGrpcServiceMappingContext(builder); + foreach (var methodProvider in builder.ServiceProvider.GetServices()) + { + 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 + { + ThrowIfMagicOnionServicesNotRegistered(builder); + + 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) + { + ThrowIfMagicOnionServicesNotRegistered(builder); + + 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) + { + ThrowIfMagicOnionServicesNotRegistered(builder); + + var context = new MagicOnionGrpcServiceMappingContext(builder); + foreach (var t in MagicOnionServicesDiscoverer.GetTypesFromAssemblies(searchAssemblies)) + { + context.Map(t); + } + + 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/MagicOnionServiceEndpointConventionBuilder.cs b/src/MagicOnion.Server/Extensions/MagicOnionServiceEndpointConventionBuilder.cs new file mode 100644 index 000000000..0bde69062 --- /dev/null +++ b/src/MagicOnion.Server/Extensions/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/Extensions/MagicOnionServicesExtensions.cs b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs index f64a5247d..9ad246378 100644 --- a/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs +++ b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs @@ -5,11 +5,13 @@ 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 MagicOnion.Server.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; @@ -17,29 +19,25 @@ 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) { + // 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) @@ -48,8 +46,10 @@ internal static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollecti services.AddMetrics(); // MagicOnion: Core services - services.TryAddSingleton(); - services.TryAddSingleton, MagicOnionServiceMethodProvider>(); + services.AddSingleton(); + services.AddSingleton(typeof(StreamingHubRegistry<>)); + services.AddSingleton(typeof(IServiceMethodProvider<>), typeof(MagicOnionGrpcServiceMethodProvider<>)); + services.TryAddSingleton(); // MagicOnion: Metrics services.TryAddSingleton(); @@ -70,6 +70,7 @@ internal static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollecti services.TryAddSingleton(); services.TryAddSingleton(); + return new MagicOnionServerBuilder(services); } } 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/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/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..155c1f3a6 --- /dev/null +++ b/src/MagicOnion.Server/Hubs/Internal/StreamingHubRegistry.cs @@ -0,0 +1,113 @@ +using System.Diagnostics.CodeAnalysis; +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 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, 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..03ec0f04f 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.Features.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,27 @@ protected virtual ValueTask OnDisconnected() return CompletedTask; } - internal async Task> Connect() + async Task> IStreamingHubBase.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 +134,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 +277,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 +292,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/Hubs/StreamingHubHandler.cs b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs index c2d646961..51a7ae2e6 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs @@ -1,70 +1,36 @@ -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; -using MagicOnion.Serialization; +using MagicOnion.Server.Binder; 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 classType, MethodInfo methodInfo, StreamingHubHandlerOptions handlerOptions, IServiceProvider serviceProvider) + public StreamingHubHandler(IMagicOnionStreamingHubMethod hubMethod, StreamingHubHandlerOptions handlerOptions, IServiceProvider serviceProvider) { - this.metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(classType, methodInfo); + this.hubMethod = hubMethod; 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, hubMethod.Metadata.Attributes); + this.MethodBody = FilterHelper.WrapMethodBodyWithFilter(serviceProvider, filters, hubMethod.InvokeAsync); } catch (Exception ex) { @@ -88,133 +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; } } -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 deleted file mode 100644 index 863c7cba0..000000000 --- a/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs +++ /dev/null @@ -1,87 +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 -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/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..d8c30ca5a --- /dev/null +++ b/src/MagicOnion.Server/Internal/IStreamingHubBase.cs @@ -0,0 +1,13 @@ +using MagicOnion.Internal; + +namespace MagicOnion.Server.Internal; + +internal interface IStreamingHubBase +{ + /// + /// 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/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/src/MagicOnion.Server/Internal/MagicOnionServicesDiscoverer.cs b/src/MagicOnion.Server/Internal/MagicOnionServicesDiscoverer.cs new file mode 100644 index 000000000..4e061511a --- /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 is { IsPublic: true, IsAbstract: false, IsGenericTypeDefinition: false }); + } + 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/Internal/MethodHandlerMetadata.cs b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs index 40311af37..565f0ff6e 100644 --- a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs +++ b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs @@ -5,18 +5,18 @@ namespace MagicOnion.Server.Internal; -public readonly struct MethodHandlerMetadata +public class MethodHandlerMetadata { public Type ServiceImplementationType { get; } - public MethodInfo ServiceMethod { get; } + public MethodInfo ServiceImplementationMethod { get; } public MethodType MethodType { get; } public Type ResponseType { get; } public Type RequestType { get; } public IReadOnlyList Parameters { get; } public Type ServiceInterface { get; } + public IReadOnlyList Attributes { get; } public ILookup AttributeLookup { get; } - public bool IsResultTypeTask { get; } public MethodHandlerMetadata( Type serviceImplementationType, @@ -26,24 +26,23 @@ public MethodHandlerMetadata( Type requestType, IReadOnlyList parameters, Type serviceInterface, - ILookup attributeLookup, - bool isResultTypeTask + IReadOnlyList attributes ) { ServiceImplementationType = serviceImplementationType; - ServiceMethod = serviceMethod; + ServiceImplementationMethod = serviceMethod; MethodType = methodType; ResponseType = responseType; RequestType = requestType; Parameters = parameters; ServiceInterface = serviceInterface; - AttributeLookup = attributeLookup; - IsResultTypeTask = isResultTypeTask; + Attributes = attributes; + AttributeLookup = attributes.ToLookup(x => x.GetType()); } } -public readonly struct StreamingHubMethodHandlerMetadata +public class StreamingHubMethodHandlerMetadata { public int MethodId { get; } public Type StreamingHubImplementationType { get; } @@ -54,8 +53,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,12 +65,25 @@ public StreamingHubMethodHandlerMetadata(int methodId, Type streamingHubImplemen RequestType = requestType; Parameters = parameters; StreamingHubInterfaceType = streamingHubInterfaceType; - AttributeLookup = attributeLookup; + AttributeLookup = attributes.ToLookup(x => x.GetType()); + Attributes = attributes; } } 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]; @@ -78,17 +91,28 @@ 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); + } + + 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) @@ -98,10 +122,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 +140,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/MagicOnionEngine.cs b/src/MagicOnion.Server/MagicOnionEngine.cs deleted file mode 100644 index 75255ff5f..000000000 --- a/src/MagicOnion.Server/MagicOnionEngine.cs +++ /dev/null @@ -1,325 +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 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 - { - // 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}"); - } - } - } - - 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()); - - sw.Stop(); - MagicOnionServerLog.EndBuildServiceDefinition(loggerMagicOnionEngine, sw.Elapsed.TotalMilliseconds); - - 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) - { - 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 f3d9bc81e..000000000 --- a/src/MagicOnion.Server/MagicOnionServiceDefinition.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MagicOnion.Server.Hubs; - -namespace MagicOnion.Server; - -public class MagicOnionServiceDefinition -{ - public IReadOnlyList MethodHandlers { get; } - public IReadOnlyList StreamingHubHandlers { get; } - - public MagicOnionServiceDefinition(IReadOnlyList handlers, IReadOnlyList streamingHubHandlers) - { - this.MethodHandlers = handlers; - this.StreamingHubHandlers = streamingHubHandlers; - } -} diff --git a/src/MagicOnion.Server/MethodHandler.cs b/src/MagicOnion.Server/MethodHandler.cs deleted file mode 100644 index 0dc458df3..000000000 --- a/src/MagicOnion.Server/MethodHandler.cs +++ /dev/null @@ -1,596 +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; - -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(ServiceType, 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( - ServiceType, - 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( - ServiceType, - 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( - ServiceType, - 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 ValueTask CopmletedValueTask = new ValueTask(); - static readonly object BoxedNil = Nil.Default; - - public static ValueTask NewEmptyValueTask(T result) - { - // ignore result. - return CopmletedValueTask; - } - - public static async ValueTask TaskToEmptyValueTask(Task result) - { - // wait and ignore result. - await result; - } - - public static async ValueTask SetUnaryResultNonGeneric(UnaryResult result, ServiceContext context) - { - if (result.hasRawValue) - { - if (result.rawTaskValue != null) - { - await result.rawTaskValue.ConfigureAwait(false); - } - context.Result = BoxedNil; - } - } - - public static async ValueTask SetUnaryResult(UnaryResult result, ServiceContext context) - { - if (result.hasRawValue) - { - context.Result = (result.rawTaskValue != null) ? await result.rawTaskValue.ConfigureAwait(false) : result.rawValue; - } - } - - 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) - { - if (result.hasRawValue) - { - context.Result = result.rawValue; - } - - return default(ValueTask); - } - - public static async ValueTask SerializeTaskClientStreamingResult(Task> taskResult, ServiceContext context) - { - 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..b2cbc0f66 100644 --- a/src/MagicOnion.Server/Service.cs +++ b/src/MagicOnion.Server/Service.cs @@ -1,16 +1,28 @@ 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. // For details, please refer to `ServiceProviderHelper.CreateService`. - public ServiceContext Context { get; internal set; } - internal MagicOnionMetrics Metrics { get; set; } + public ServiceContext Context { get; private set; } + internal MagicOnionMetrics Metrics { get; private set; } + + ServiceContext IServiceBase.Context + { + get => this.Context; + set => this.Context = value; + } + MagicOnionMetrics IServiceBase.Metrics + { + get => this.Metrics; + set => this.Metrics = value; + } public ServiceBase() { @@ -36,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 facce8626..3908a6ad5 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; @@ -39,24 +40,22 @@ internal class StreamingServiceContext : ServiceContext, IS public bool IsDisconnected { get; private set; } public StreamingServiceContext( - Type serviceType, - MethodInfo methodInfo, - ILookup attributeLookup, - MethodType methodType, + object instance, + IMagicOnionGrpcMethod method, ServerCallContext context, IMagicOnionSerializer messageSerializer, + MagicOnionMetrics metrics, ILogger logger, - MethodHandler methodHandler, IServiceProvider serviceProvider, IAsyncStreamReader? requestStream, IServerStreamWriter? responseStream - ) : base(serviceType, methodInfo, attributeLookup, methodType, context, messageSerializer, logger, methodHandler, serviceProvider) + ) : base(instance, method, context, messageSerializer, metrics, 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 cc2f231d2..11fa3199b 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,14 +64,17 @@ public ConcurrentDictionary Items public DateTime Timestamp { get; } - public Type ServiceType { get; } + public Type ServiceType => Method.ServiceImplementationType; - public MethodInfo MethodInfo { get; } + public string ServiceName => Method.ServiceName; + public string MethodName => MethodInfo.Name; + + public MethodInfo MethodInfo => Method.Metadata.ServiceImplementationMethod; /// Cached Attributes both service and method. - public ILookup AttributeLookup { get; } + public ILookup AttributeLookup => Method.Metadata.AttributeLookup; - public MethodType MethodType { get; } + public MethodType MethodType => Method.MethodType; /// Raw gRPC Context. public ServerCallContext CallContext { get; } @@ -79,37 +83,32 @@ public ConcurrentDictionary Items public IServiceProvider ServiceProvider { get; } + internal object Instance { get; } 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( - Type serviceType, - MethodInfo methodInfo, - ILookup attributeLookup, - MethodType methodType, + internal ServiceContext( + object instance, + IMagicOnionGrpcMethod method, ServerCallContext context, IMagicOnionSerializer messageSerializer, + MagicOnionMetrics metrics, ILogger logger, - MethodHandler methodHandler, IServiceProvider serviceProvider ) { this.ContextId = Guid.NewGuid(); - this.ServiceType = serviceType; - this.MethodInfo = methodInfo; - this.AttributeLookup = attributeLookup; - this.MethodType = methodType; + this.Instance = instance; 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(); + this.Metrics = metrics.CreateContext(); } /// Gets a request object. 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; - } -} 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.Serialization.MemoryPack.Tests/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Serialization.MemoryPack.Tests/MagicOnionApplicationFactory.cs index 9b264d9b4..633466ff4 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 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..1b8f39723 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 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 new file mode 100644 index 000000000..aed16baf9 --- /dev/null +++ b/tests/MagicOnion.Server.Tests/DynamicMagicOnionMethodProviderTest.cs @@ -0,0 +1,571 @@ +using Grpc.Core; +using MagicOnion.Internal; +using MagicOnion.Server.Binder; +using MagicOnion.Server.Binder.Internal; +using System.Reflection.Metadata; +using MagicOnion.Serialization; +using MagicOnion.Server.Diagnostics; +using MagicOnion.Server.Hubs; +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 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 serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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 serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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 serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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 serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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 serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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 serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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() + { + // 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 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; + UnaryResult Unary_ParameterZero_ReturnValueValueType() => UnaryResult.FromResult(12345); + UnaryResult Unary_ParameterZero_ReturnValueRefType() => UnaryResult.FromResult("Hello"); + + 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) => 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) => 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(); + 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(); + } + } + + [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(); + } +} 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 new file mode 100644 index 000000000..6dab752bb --- /dev/null +++ b/tests/MagicOnion.Server.Tests/HandCraftedMagicOnionMethodProviderTest.cs @@ -0,0 +1,216 @@ +using System.Runtime.CompilerServices; +using Grpc.Net.Client; +using MagicOnion.Client; +using MagicOnion.Internal; +using MagicOnion.Server.Binder; +using MagicOnion.Server.Hubs; +using NSubstitute; + +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(); + } + + [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); + } +} + + +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) + { + return Task.CompletedTask; + } + + public ValueTask SendMessageAsync(string message) + { + return default; + } + + public ValueTask> GetMembersAsync() + { + return new(["User-1", "User-2"]); + } +} + +class HandCraftedMagicOnionMethodProviderTest_MyFilter : MagicOnionFilterAttribute +{ + public override ValueTask Invoke(ServiceContext context, Func next) + { + return next(context); + } +} + +internal class HandCraftedMagicOnionMethodProviderTest_GeneratedMagicOnionMethodProvider : IMagicOnionGrpcMethodProvider +{ + public void MapAllSupportedServiceTypes(MagicOnionGrpcServiceMappingContext context) + { + context.Map(); + context.Map(); + context.Map(); + } + + public IReadOnlyList GetGrpcMethods() where TService : class + { + if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterService)) + { + 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)) + { + 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)) + { + return + [ + new MagicOnionStreamingHubConnectMethod(nameof(IHandCraftedMagicOnionMethodProviderTest_GreeterHub)), + ]; + } + + return []; + } + + public IReadOnlyList GetStreamingHubMethods() where TService : class + { + if (typeof(TService) == typeof(HandCraftedMagicOnionMethodProviderTest_GreeterHub)) + { + 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( + // 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))!); + } + + return []; + } +} diff --git a/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs index bdfda7046..d0bc829d6 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs @@ -3,11 +3,20 @@ using Microsoft.Extensions.DependencyInjection; using System.Collections.Concurrent; using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Builder; 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 +35,20 @@ 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([..GetServiceImplementationTypes()]); + }); }); } + protected abstract IEnumerable GetServiceImplementationTypes(); + public WebApplicationFactory WithMagicOnionOptions(Action configure) { return this.WithWebHostBuilder(x => 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 diff --git a/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs new file mode 100644 index 000000000..ca26ff543 --- /dev/null +++ b/tests/MagicOnion.Server.Tests/MagicOnionGrpcMethodTest.cs @@ -0,0 +1,257 @@ +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 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>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary), (instance, context, _) => + { + called = true; + invokerArgInstance = instance; + return default; + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary_Parameterless_Int), (instance, context, _) => + { + called = true; + invokerArgInstance = instance; + return UnaryResult.FromResult(12345); + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary_Parameterless_String), (instance, context, _) => + { + called = true; + invokerArgInstance = instance; + return UnaryResult.FromResult("Hello"); + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary_Int), (instance, context, request) => + { + called = true; + invokerArgInstance = instance; + invokerArgRequest = request; + return default; + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary_Int_Int), (instance, context, request) => + { + called = true; + invokerArgInstance = instance; + invokerArgRequest = request; + return UnaryResult.FromResult(request * 2); + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Unary_Int_String), (instance, context, request) => + { + called = true; + invokerArgInstance = instance; + invokerArgRequest = request; + return UnaryResult.FromResult(request.ToString()); + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + var serializer = Substitute.For(); + var serviceProvider = Substitute.For(); + var metrics = new MagicOnionMetrics(new TestMeterFactory()); + var serviceContext = new ServiceContext(instance, method, 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>(nameof(ServiceImpl.IMyService), nameof(ServiceImpl.IMyService.Duplex), 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; + }); + var instance = new ServiceImpl(); + var serverCallContext = Substitute.For(); + 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, 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 : 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 new file mode 100644 index 000000000..ca52eaafd --- /dev/null +++ b/tests/MagicOnion.Server.Tests/MagicOnionGrpcServiceMappingContextTest.cs @@ -0,0 +1,174 @@ +using MagicOnion.Server.Hubs; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +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() + { + // 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); + } + + [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 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})!"); + } +} diff --git a/tests/MagicOnion.Server.Tests/MethodHandlerMetadataFactoryTest.cs b/tests/MagicOnion.Server.Tests/MethodHandlerMetadataFactoryTest.cs index e8f64c92b..6bdd247c2 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,10 +282,9 @@ 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(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } @@ -302,10 +301,9 @@ 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(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } @@ -322,10 +320,9 @@ 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(); metadata.RequestType.Should().Be>(); metadata.ResponseType.Should().Be(); } @@ -342,10 +339,9 @@ 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(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } @@ -377,10 +373,9 @@ 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(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } @@ -412,10 +407,9 @@ 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(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } @@ -447,10 +441,9 @@ 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(); metadata.RequestType.Should().Be(); metadata.ResponseType.Should().Be(); } diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs index a66945b7a..1ca6be52b 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,12 +20,15 @@ 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); + 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); @@ -54,12 +58,15 @@ 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); + 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); @@ -88,12 +95,15 @@ 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); + 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); @@ -123,12 +133,15 @@ 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); + 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); @@ -158,12 +171,15 @@ 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); + 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); @@ -193,12 +209,15 @@ 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); + 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); @@ -227,12 +246,15 @@ 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); + 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); @@ -261,12 +283,15 @@ 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); + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); for (var i = 0; i < 3; i++) { var ctx = new StreamingHubContext(); @@ -303,12 +328,15 @@ 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); + 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); @@ -338,12 +366,15 @@ 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); + 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); @@ -373,12 +404,15 @@ 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); + 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); @@ -408,12 +442,15 @@ 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); + 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); @@ -430,15 +467,18 @@ 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)); // Act - var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions() + var handler = new StreamingHubHandler(hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions() { MessageSerializer = XorMessagePackMagicOnionSerializerProvider.Instance, }), serviceProvider); @@ -470,10 +510,13 @@ 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); + 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() { 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/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); }); } } 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 +}