From a2e9d90bb12e8c9677c7cfb2c3c037448a449460 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Fri, 15 Sep 2023 19:14:33 +0900 Subject: [PATCH 1/4] Allows to send response messages directly as byte arrays. --- src/MagicOnion.Abstractions/RawBytesBox.cs | 15 ++ .../MagicOnion.Abstractions/RawBytesBox.cs | 15 ++ .../MagicOnion.Shared/GrpcMethodHelper.cs | 154 +++++++++--------- ...MessagePackMagicOnionSerializerProvider.cs | 1 + .../Internal/MagicOnionMethodHandlerBinder.cs | 26 ++- src/MagicOnion.Server/MagicOnionEngine.cs | 9 +- src/MagicOnion.Server/MethodHandler.cs | 6 +- src/MagicOnion.Shared/GrpcMethodHelper.cs | 154 +++++++++--------- .../MagicOnion.Server.Tests.csproj | 1 + .../MagicOnionApplicationFactory.cs | 28 ++++ .../RawBytesResponseTest.cs | 122 ++++++++++++++ 11 files changed, 373 insertions(+), 158 deletions(-) create mode 100644 src/MagicOnion.Abstractions/RawBytesBox.cs create mode 100644 src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/RawBytesBox.cs create mode 100644 tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs create mode 100644 tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs diff --git a/src/MagicOnion.Abstractions/RawBytesBox.cs b/src/MagicOnion.Abstractions/RawBytesBox.cs new file mode 100644 index 000000000..f52dce471 --- /dev/null +++ b/src/MagicOnion.Abstractions/RawBytesBox.cs @@ -0,0 +1,15 @@ +using System; + +namespace MagicOnion +{ + public sealed class RawBytesBox + { + public ReadOnlyMemory Bytes { get; } + + public RawBytesBox(ReadOnlyMemory bytes) + { + Bytes = bytes; + } + } +} + diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/RawBytesBox.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/RawBytesBox.cs new file mode 100644 index 000000000..f52dce471 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/RawBytesBox.cs @@ -0,0 +1,15 @@ +using System; + +namespace MagicOnion +{ + public sealed class RawBytesBox + { + public ReadOnlyMemory Bytes { get; } + + public RawBytesBox(ReadOnlyMemory bytes) + { + Bytes = bytes; + } + } +} + diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/GrpcMethodHelper.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/GrpcMethodHelper.cs index 32595d973..7ac93817c 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/GrpcMethodHelper.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/GrpcMethodHelper.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; using System.Reflection; -using System.Text; +using System.Runtime.CompilerServices; using Grpc.Core; using MagicOnion.Internal; using MagicOnion.Serialization; @@ -14,18 +13,39 @@ public static class GrpcMethodHelper public sealed class MagicOnionMethod { public Method Method { get; } - public Func ToRawRequest { get; } - public Func ToRawResponse { get; } - public Func FromRawRequest { get; } - public Func FromRawResponse { get; } public MagicOnionMethod(Method method) { Method = method; - ToRawRequest = ((typeof(TRawRequest) == typeof(Box)) ? (Func)(x => (TRawRequest)(object)Box.Create(x)) : x => DangerousDummyNull.GetObjectOrDummyNull((TRawRequest)(object)x)); - ToRawResponse = ((typeof(TRawResponse) == typeof(Box)) ? (Func)(x => (TRawResponse)(object)Box.Create(x)) : x => DangerousDummyNull.GetObjectOrDummyNull((TRawResponse)(object)x)); - FromRawRequest = ((typeof(TRawRequest) == typeof(Box)) ? (Func)(x => ((Box)(object)x).Value) : x => DangerousDummyNull.GetObjectOrDefault(x)); - FromRawResponse = ((typeof(TRawResponse) == typeof(Box)) ? (Func)(x => ((Box)(object)x).Value) : x => DangerousDummyNull.GetObjectOrDefault(x)); + } + + public TRawRequest ToRawRequest(TRequest obj) => ToRaw(obj); + public TRawResponse ToRawResponse(TResponse obj) => ToRaw(obj); + public TRequest FromRawRequest(TRawRequest obj) => FromRaw(obj); + public TResponse FromRawResponse(TRawResponse obj) => FromRaw(obj); + + static TRaw ToRaw(T obj) + { + if (typeof(TRaw) == typeof(Box)) + { + return (TRaw)(object)Box.Create(obj); + } + else + { + return DangerousDummyNull.GetObjectOrDummyNull(Unsafe.As(ref obj)); + } + } + + static T FromRaw(TRaw obj) + { + if (typeof(TRaw) == typeof(Box)) + { + return ((Box)(object)obj).Value; + } + else + { + return DangerousDummyNull.GetObjectOrDefault(obj); + } } } @@ -39,27 +59,17 @@ public static MagicOnionMethod, TRawResponse> CreateMet // 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 isMethodResponseTypeBoxed = typeof(TResponse).IsValueType; + var responseMarshaller = isMethodResponseTypeBoxed + ? (object)CreateBoxedMarshaller(messageSerializer) + : (object)CreateMarshaller(messageSerializer); - if (isMethodResponseTypeBoxed) - { - return new MagicOnionMethod, TRawResponse>(new Method, TRawResponse>( - methodType, - serviceName, - name, - IgnoreNilMarshaller, - (Marshaller)(object)CreateBoxedMarshaller(messageSerializer, methodType, methodInfo) - )); - } - else - { - return new MagicOnionMethod, TRawResponse>(new Method, TRawResponse>( - methodType, - serviceName, - name, - IgnoreNilMarshaller, - (Marshaller)(object)CreateMarshaller(messageSerializer, methodType, methodInfo) - )); - } + return new MagicOnionMethod, TRawResponse>(new Method, TRawResponse>( + methodType, + serviceName, + name, + IgnoreNilMarshaller, + (Marshaller)responseMarshaller + )); } public static MagicOnionMethod CreateMethod(MethodType methodType, string serviceName, string name, IMagicOnionSerializer messageSerializer) @@ -71,46 +81,20 @@ public static MagicOnionMethod C var isMethodRequestTypeBoxed = typeof(TRequest).IsValueType; var isMethodResponseTypeBoxed = typeof(TResponse).IsValueType; - if (isMethodRequestTypeBoxed && isMethodResponseTypeBoxed) - { - return new MagicOnionMethod(new Method( - methodType, - serviceName, - name, - (Marshaller)(object)CreateBoxedMarshaller(messageSerializer, methodType, methodInfo), - (Marshaller)(object)CreateBoxedMarshaller(messageSerializer, methodType, methodInfo) - )); - } - else if (isMethodRequestTypeBoxed) - { - return new MagicOnionMethod(new Method( - methodType, - serviceName, - name, - (Marshaller)(object)CreateBoxedMarshaller(messageSerializer, methodType, methodInfo), - (Marshaller)(object)CreateMarshaller(messageSerializer, methodType, methodInfo) - )); - } - else if (isMethodResponseTypeBoxed) - { - return new MagicOnionMethod(new Method( - methodType, - serviceName, - name, - (Marshaller)(object)CreateMarshaller(messageSerializer, methodType, methodInfo), - (Marshaller)(object)CreateBoxedMarshaller(messageSerializer, methodType, methodInfo) - )); - } - else - { - return new MagicOnionMethod(new Method( - methodType, - serviceName, - name, - (Marshaller)(object)CreateMarshaller(messageSerializer, methodType, methodInfo), - (Marshaller)(object)CreateMarshaller(messageSerializer, methodType, methodInfo) - )); - } + var requestMarshaller = isMethodRequestTypeBoxed + ? (object)CreateBoxedMarshaller(messageSerializer) + : (object)CreateMarshaller(messageSerializer); + var responseMarshaller = isMethodResponseTypeBoxed + ? (object)CreateBoxedMarshaller(messageSerializer) + : (object)CreateMarshaller(messageSerializer); + + return new MagicOnionMethod(new Method( + methodType, + serviceName, + name, + (Marshaller)requestMarshaller, + (Marshaller)responseMarshaller + )); } // WORKAROUND: Prior to MagicOnion 5.0, the request type for the parameter-less method was byte[]. @@ -131,23 +115,45 @@ public static MagicOnionMethod C deserializer: (ctx) => Box.Create(Nil.Default) /* Box.Create always returns cached Box */ ); - static Marshaller CreateMarshaller(IMagicOnionSerializer messageSerializer, MethodType methodType, MethodInfo methodInfo) + static Marshaller CreateMarshaller(IMagicOnionSerializer messageSerializer) { return new Marshaller( serializer: (obj, ctx) => { - messageSerializer.Serialize(ctx.GetBufferWriter(), DangerousDummyNull.GetObjectOrDefault(obj)); + if (obj.GetType() == typeof(RawBytesBox)) + { + var rawBytesBox = (RawBytesBox)(object)obj; + var writer = ctx.GetBufferWriter(); + var buffer = writer.GetSpan(rawBytesBox.Bytes.Length); + rawBytesBox.Bytes.Span.CopyTo(buffer); + writer.Advance(rawBytesBox.Bytes.Length); + } + else + { + messageSerializer.Serialize(ctx.GetBufferWriter(), DangerousDummyNull.GetObjectOrDefault(obj)); + } ctx.Complete(); }, deserializer: (ctx) => DangerousDummyNull.GetObjectOrDummyNull(messageSerializer.Deserialize(ctx.PayloadAsReadOnlySequence()))); } - static Marshaller> CreateBoxedMarshaller(IMagicOnionSerializer messageSerializer, MethodType methodType, MethodInfo methodInfo) + static Marshaller> CreateBoxedMarshaller(IMagicOnionSerializer messageSerializer) { return new Marshaller>( serializer: (obj, ctx) => { - messageSerializer.Serialize(ctx.GetBufferWriter(), obj.Value); + if (obj.GetType() == typeof(RawBytesBox)) + { + var rawBytesBox = (RawBytesBox)(object)obj; + var writer = ctx.GetBufferWriter(); + var buffer = writer.GetSpan(rawBytesBox.Bytes.Length); + rawBytesBox.Bytes.Span.CopyTo(buffer); + writer.Advance(rawBytesBox.Bytes.Length); + } + else + { + messageSerializer.Serialize(ctx.GetBufferWriter(), obj.Value); + } ctx.Complete(); }, deserializer: (ctx) => Box.Create(messageSerializer.Deserialize(ctx.PayloadAsReadOnlySequence())) diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/Serialization/MessagePackMagicOnionSerializerProvider.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/Serialization/MessagePackMagicOnionSerializerProvider.cs index a6633affb..b7c8d8240 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/Serialization/MessagePackMagicOnionSerializerProvider.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/Serialization/MessagePackMagicOnionSerializerProvider.cs @@ -111,6 +111,7 @@ public MessagePackMagicOnionSerializer(MessagePackSerializerOptions serializerOp public T Deserialize(in ReadOnlySequence bytes) => MessagePackSerializer.Deserialize(bytes, serializerOptions); + public void Serialize(IBufferWriter writer, in T value) => MessagePackSerializer.Serialize(writer, value, serializerOptions); } diff --git a/src/MagicOnion.Server/Internal/MagicOnionMethodHandlerBinder.cs b/src/MagicOnion.Server/Internal/MagicOnionMethodHandlerBinder.cs index 733100ccf..7fc07364b 100644 --- a/src/MagicOnion.Server/Internal/MagicOnionMethodHandlerBinder.cs +++ b/src/MagicOnion.Server/Internal/MagicOnionMethodHandlerBinder.cs @@ -1,8 +1,10 @@ +using System.Diagnostics; using Grpc.Core; using MagicOnion.Internal; using MagicOnion.Serialization; using MessagePack; using System.Reflection; +using System.Runtime.CompilerServices; namespace MagicOnion.Server.Internal; @@ -25,21 +27,37 @@ internal class MagicOnionMethodHandlerBinder Instance { get; } = new MagicOnionMethodHandlerBinder(); - public void BindUnary(ServiceBinderBase binder, Func> serverMethod, MethodHandler methodHandler, IMagicOnionSerializer messageSerializer) + public void BindUnary(ServiceBinderBase binder, Func> serverMethod, MethodHandler methodHandler, IMagicOnionSerializer messageSerializer) { var method = GrpcMethodHelper.CreateMethod(MethodType.Unary, methodHandler.ServiceName, methodHandler.MethodName, methodHandler.MethodInfo, messageSerializer); binder.AddMethod(new MagicOnionServerMethod(method.Method, methodHandler), - async (request, context) => method.ToRawResponse(await serverMethod(method.FromRawRequest(request), context))); + async (request, context) => + { + var response = await serverMethod(method.FromRawRequest(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 method.ToRawResponse((TResponse?)response); + }); } - public void BindUnaryParameterless(ServiceBinderBase binder, Func> serverMethod, MethodHandler methodHandler, IMagicOnionSerializer messageSerializer) + public void BindUnaryParameterless(ServiceBinderBase binder, Func> serverMethod, MethodHandler methodHandler, IMagicOnionSerializer messageSerializer) { // 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, methodHandler.ServiceName, methodHandler.MethodName, methodHandler.MethodInfo, messageSerializer); binder.AddMethod(new MagicOnionServerMethod, TRawResponse>(method.Method, methodHandler), - async (request, context) => method.ToRawResponse(await serverMethod(method.FromRawRequest(request), context))); + async (request, context) => + { + var response = await serverMethod(method.FromRawRequest(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 method.ToRawResponse((TResponse?)response); + }); } public void BindStreamingHub(ServiceBinderBase binder, Func, IServerStreamWriter, ServerCallContext, Task> serverMethod, MethodHandler methodHandler, IMagicOnionSerializer messageSerializer) diff --git a/src/MagicOnion.Server/MagicOnionEngine.cs b/src/MagicOnion.Server/MagicOnionEngine.cs index 9fa2a19de..68c449b9c 100644 --- a/src/MagicOnion.Server/MagicOnionEngine.cs +++ b/src/MagicOnion.Server/MagicOnionEngine.cs @@ -125,6 +125,9 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP var handlers = new HashSet(); var streamingHubHandlers = new List(); + var methodHandlerOptions = new MethodHandlerOptions(options); + var streamingHubHandlerOptions = new StreamingHubHandlerOptions(options); + logger.BeginBuildServiceDefinition(); var sw = Stopwatch.StartNew(); @@ -179,7 +182,7 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP // register for StreamingHub if (isStreamingHub && methodName != "Connect") { - var streamingHandler = new StreamingHubHandler(classType, methodInfo, new StreamingHubHandlerOptions(options), serviceProvider); + var streamingHandler = new StreamingHubHandler(classType, methodInfo, streamingHubHandlerOptions, serviceProvider); if (!tempStreamingHubHandlers!.Add(streamingHandler)) { throw new InvalidOperationException($"Method does not allow overload, {className}.{methodName}"); @@ -189,7 +192,7 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP else { // create handler - var handler = new MethodHandler(classType, methodInfo, methodName, new MethodHandlerOptions(options), serviceProvider, logger, isStreamingHub: false); + var handler = new MethodHandler(classType, methodInfo, methodName, methodHandlerOptions, serviceProvider, logger, isStreamingHub: false); if (!handlers.Add(handler)) { throw new InvalidOperationException($"Method does not allow overload, {className}.{methodName}"); @@ -199,7 +202,7 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP if (isStreamingHub) { - var connectHandler = new MethodHandler(classType, classType.GetMethod("Connect")!, "Connect", new MethodHandlerOptions(options), serviceProvider, logger, isStreamingHub: true); + var connectHandler = new MethodHandler(classType, classType.GetMethod("Connect")!, "Connect", methodHandlerOptions, serviceProvider, logger, isStreamingHub: true); if (!handlers.Add(connectHandler)) { throw new InvalidOperationException($"Method does not allow overload, {className}.Connect"); diff --git a/src/MagicOnion.Server/MethodHandler.cs b/src/MagicOnion.Server/MethodHandler.cs index 9734e3823..d45cfb664 100644 --- a/src/MagicOnion.Server/MethodHandler.cs +++ b/src/MagicOnion.Server/MethodHandler.cs @@ -224,13 +224,13 @@ void BindHandlerTyped(ServiceBin } } - async Task UnaryServerMethod(TRequest request, ServerCallContext context) + async Task 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); - TResponse? response = default; + object? response = default(TResponse?); try { Logger.BeginInvokeMethod(serviceContext, typeof(TRequest)); @@ -241,7 +241,7 @@ void BindHandlerTyped(ServiceBin await this.methodBody(serviceContext).ConfigureAwait(false); if (serviceContext.Result is not null) { - response = (TResponse?)serviceContext.Result; + response = serviceContext.Result; } } catch (ReturnStatusException ex) diff --git a/src/MagicOnion.Shared/GrpcMethodHelper.cs b/src/MagicOnion.Shared/GrpcMethodHelper.cs index 32595d973..7ac93817c 100644 --- a/src/MagicOnion.Shared/GrpcMethodHelper.cs +++ b/src/MagicOnion.Shared/GrpcMethodHelper.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; using System.Reflection; -using System.Text; +using System.Runtime.CompilerServices; using Grpc.Core; using MagicOnion.Internal; using MagicOnion.Serialization; @@ -14,18 +13,39 @@ public static class GrpcMethodHelper public sealed class MagicOnionMethod { public Method Method { get; } - public Func ToRawRequest { get; } - public Func ToRawResponse { get; } - public Func FromRawRequest { get; } - public Func FromRawResponse { get; } public MagicOnionMethod(Method method) { Method = method; - ToRawRequest = ((typeof(TRawRequest) == typeof(Box)) ? (Func)(x => (TRawRequest)(object)Box.Create(x)) : x => DangerousDummyNull.GetObjectOrDummyNull((TRawRequest)(object)x)); - ToRawResponse = ((typeof(TRawResponse) == typeof(Box)) ? (Func)(x => (TRawResponse)(object)Box.Create(x)) : x => DangerousDummyNull.GetObjectOrDummyNull((TRawResponse)(object)x)); - FromRawRequest = ((typeof(TRawRequest) == typeof(Box)) ? (Func)(x => ((Box)(object)x).Value) : x => DangerousDummyNull.GetObjectOrDefault(x)); - FromRawResponse = ((typeof(TRawResponse) == typeof(Box)) ? (Func)(x => ((Box)(object)x).Value) : x => DangerousDummyNull.GetObjectOrDefault(x)); + } + + public TRawRequest ToRawRequest(TRequest obj) => ToRaw(obj); + public TRawResponse ToRawResponse(TResponse obj) => ToRaw(obj); + public TRequest FromRawRequest(TRawRequest obj) => FromRaw(obj); + public TResponse FromRawResponse(TRawResponse obj) => FromRaw(obj); + + static TRaw ToRaw(T obj) + { + if (typeof(TRaw) == typeof(Box)) + { + return (TRaw)(object)Box.Create(obj); + } + else + { + return DangerousDummyNull.GetObjectOrDummyNull(Unsafe.As(ref obj)); + } + } + + static T FromRaw(TRaw obj) + { + if (typeof(TRaw) == typeof(Box)) + { + return ((Box)(object)obj).Value; + } + else + { + return DangerousDummyNull.GetObjectOrDefault(obj); + } } } @@ -39,27 +59,17 @@ public static MagicOnionMethod, TRawResponse> CreateMet // 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 isMethodResponseTypeBoxed = typeof(TResponse).IsValueType; + var responseMarshaller = isMethodResponseTypeBoxed + ? (object)CreateBoxedMarshaller(messageSerializer) + : (object)CreateMarshaller(messageSerializer); - if (isMethodResponseTypeBoxed) - { - return new MagicOnionMethod, TRawResponse>(new Method, TRawResponse>( - methodType, - serviceName, - name, - IgnoreNilMarshaller, - (Marshaller)(object)CreateBoxedMarshaller(messageSerializer, methodType, methodInfo) - )); - } - else - { - return new MagicOnionMethod, TRawResponse>(new Method, TRawResponse>( - methodType, - serviceName, - name, - IgnoreNilMarshaller, - (Marshaller)(object)CreateMarshaller(messageSerializer, methodType, methodInfo) - )); - } + return new MagicOnionMethod, TRawResponse>(new Method, TRawResponse>( + methodType, + serviceName, + name, + IgnoreNilMarshaller, + (Marshaller)responseMarshaller + )); } public static MagicOnionMethod CreateMethod(MethodType methodType, string serviceName, string name, IMagicOnionSerializer messageSerializer) @@ -71,46 +81,20 @@ public static MagicOnionMethod C var isMethodRequestTypeBoxed = typeof(TRequest).IsValueType; var isMethodResponseTypeBoxed = typeof(TResponse).IsValueType; - if (isMethodRequestTypeBoxed && isMethodResponseTypeBoxed) - { - return new MagicOnionMethod(new Method( - methodType, - serviceName, - name, - (Marshaller)(object)CreateBoxedMarshaller(messageSerializer, methodType, methodInfo), - (Marshaller)(object)CreateBoxedMarshaller(messageSerializer, methodType, methodInfo) - )); - } - else if (isMethodRequestTypeBoxed) - { - return new MagicOnionMethod(new Method( - methodType, - serviceName, - name, - (Marshaller)(object)CreateBoxedMarshaller(messageSerializer, methodType, methodInfo), - (Marshaller)(object)CreateMarshaller(messageSerializer, methodType, methodInfo) - )); - } - else if (isMethodResponseTypeBoxed) - { - return new MagicOnionMethod(new Method( - methodType, - serviceName, - name, - (Marshaller)(object)CreateMarshaller(messageSerializer, methodType, methodInfo), - (Marshaller)(object)CreateBoxedMarshaller(messageSerializer, methodType, methodInfo) - )); - } - else - { - return new MagicOnionMethod(new Method( - methodType, - serviceName, - name, - (Marshaller)(object)CreateMarshaller(messageSerializer, methodType, methodInfo), - (Marshaller)(object)CreateMarshaller(messageSerializer, methodType, methodInfo) - )); - } + var requestMarshaller = isMethodRequestTypeBoxed + ? (object)CreateBoxedMarshaller(messageSerializer) + : (object)CreateMarshaller(messageSerializer); + var responseMarshaller = isMethodResponseTypeBoxed + ? (object)CreateBoxedMarshaller(messageSerializer) + : (object)CreateMarshaller(messageSerializer); + + return new MagicOnionMethod(new Method( + methodType, + serviceName, + name, + (Marshaller)requestMarshaller, + (Marshaller)responseMarshaller + )); } // WORKAROUND: Prior to MagicOnion 5.0, the request type for the parameter-less method was byte[]. @@ -131,23 +115,45 @@ public static MagicOnionMethod C deserializer: (ctx) => Box.Create(Nil.Default) /* Box.Create always returns cached Box */ ); - static Marshaller CreateMarshaller(IMagicOnionSerializer messageSerializer, MethodType methodType, MethodInfo methodInfo) + static Marshaller CreateMarshaller(IMagicOnionSerializer messageSerializer) { return new Marshaller( serializer: (obj, ctx) => { - messageSerializer.Serialize(ctx.GetBufferWriter(), DangerousDummyNull.GetObjectOrDefault(obj)); + if (obj.GetType() == typeof(RawBytesBox)) + { + var rawBytesBox = (RawBytesBox)(object)obj; + var writer = ctx.GetBufferWriter(); + var buffer = writer.GetSpan(rawBytesBox.Bytes.Length); + rawBytesBox.Bytes.Span.CopyTo(buffer); + writer.Advance(rawBytesBox.Bytes.Length); + } + else + { + messageSerializer.Serialize(ctx.GetBufferWriter(), DangerousDummyNull.GetObjectOrDefault(obj)); + } ctx.Complete(); }, deserializer: (ctx) => DangerousDummyNull.GetObjectOrDummyNull(messageSerializer.Deserialize(ctx.PayloadAsReadOnlySequence()))); } - static Marshaller> CreateBoxedMarshaller(IMagicOnionSerializer messageSerializer, MethodType methodType, MethodInfo methodInfo) + static Marshaller> CreateBoxedMarshaller(IMagicOnionSerializer messageSerializer) { return new Marshaller>( serializer: (obj, ctx) => { - messageSerializer.Serialize(ctx.GetBufferWriter(), obj.Value); + if (obj.GetType() == typeof(RawBytesBox)) + { + var rawBytesBox = (RawBytesBox)(object)obj; + var writer = ctx.GetBufferWriter(); + var buffer = writer.GetSpan(rawBytesBox.Bytes.Length); + rawBytesBox.Bytes.Span.CopyTo(buffer); + writer.Advance(rawBytesBox.Bytes.Length); + } + else + { + messageSerializer.Serialize(ctx.GetBufferWriter(), obj.Value); + } ctx.Complete(); }, deserializer: (ctx) => Box.Create(messageSerializer.Deserialize(ctx.PayloadAsReadOnlySequence())) diff --git a/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj b/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj index ddd2773f9..7368ee9b1 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj +++ b/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj @@ -28,6 +28,7 @@ + diff --git a/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs new file mode 100644 index 000000000..b0515fca4 --- /dev/null +++ b/tests/MagicOnion.Server.Tests/MagicOnionApplicationFactory.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; + +namespace MagicOnion.Server.Tests; + +#pragma warning disable CS1998 +public class MagicOnionApplicationFactory : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(services => + { + services.AddMagicOnion(new[] { typeof(TServiceImplementation) }); + }); + } + + public WebApplicationFactory WithMagicOnionOptions(Action configure) + { + return this.WithWebHostBuilder(x => + { + x.ConfigureServices(services => + { + services.Configure(configure); + }); + }); + } +} diff --git a/tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs b/tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs new file mode 100644 index 000000000..6bad2b026 --- /dev/null +++ b/tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs @@ -0,0 +1,122 @@ +using System.Collections.Concurrent; +using Grpc.Net.Client; +using MagicOnion.Client; +using MagicOnion.Serialization; +using MessagePack; +using Microsoft.AspNetCore.Mvc.Testing; + +namespace MagicOnion.Server.Tests; + +public class RawBytesResponseTest : IClassFixture> +{ + readonly WebApplicationFactory factory; + + public RawBytesResponseTest(MagicOnionApplicationFactory factory) + { + this.factory = factory.WithMagicOnionOptions(options => {}); + } + + [Fact] + public async Task RefType() + { + // Arrange + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = factory.CreateDefaultClient() }); + var client = MagicOnionClient.Create(channel, MessagePackMagicOnionSerializerProvider.Default); + + // Act + var result = await client.RefType(); + var result2 = await client.RefType(); + + // Assert + var expected = (RawBytesResponseTestService_RefTypeResponse)FixedResponseFilterAttribute.ResponseCache["/IRawBytesResponseTestService/RefType"]; + Assert.Equal(expected.Value1, result.Value1); + Assert.Equal(expected.Value2, result.Value2); + Assert.Equal(expected.Value3, result.Value3); + Assert.Equal(expected.Value1, result2.Value1); + Assert.Equal(expected.Value2, result2.Value2); + Assert.Equal(expected.Value3, result2.Value3); + } + + [Fact] + public async Task ValueType() + { + // Arrange + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = factory.CreateDefaultClient() }); + var client = MagicOnionClient.Create(channel, MessagePackMagicOnionSerializerProvider.Default); + + // Act + var result = await client.ValueType(); + var result2 = await client.ValueType(); + + // Assert + var expected = (RawBytesResponseTestService_ValueTypeResponse)FixedResponseFilterAttribute.ResponseCache["/IRawBytesResponseTestService/ValueType"]; + Assert.Equal(expected.Value1, result.Value1); + Assert.Equal(expected.Value2, result.Value2); + Assert.Equal(expected.Value3, result.Value3); + Assert.Equal(expected.Value1, result2.Value1); + Assert.Equal(expected.Value2, result2.Value2); + Assert.Equal(expected.Value3, result2.Value3); + } +} + +file class FixedResponseFilterAttribute : MagicOnionFilterAttribute +{ + public static readonly RawBytesResponseTestService_RefTypeResponse ResponseRefType; + public static readonly RawBytesResponseTestService_ValueTypeResponse ResponseValueType; + + public static readonly ConcurrentDictionary ResponseCache = new(); + public static readonly ConcurrentDictionary ResponseBytesCache = new(); + + static FixedResponseFilterAttribute() + { + ResponseRefType = new () { Value1 = Guid.NewGuid().ToString(), Value2 = Random.Shared.Next(), Value3 = Random.Shared.NextInt64() }; + ResponseValueType = new () { Value1 = Guid.NewGuid().ToString(), Value2 = Random.Shared.Next(), Value3 = Random.Shared.NextInt64() }; + } + + public override async ValueTask Invoke(ServiceContext context, Func next) + { + if (ResponseBytesCache.TryGetValue(context.CallContext.Method, out var cachedBytes)) + { + context.SetRawResponse(new RawBytesBox(cachedBytes)); + return; + } + + await next(context); + + ResponseCache[context.CallContext.Method] = context.Result; + ResponseBytesCache[context.CallContext.Method] = MessagePackSerializer.Serialize(context.Result); + } +} + +public interface IRawBytesResponseTestService : IService +{ + UnaryResult RefType(); + UnaryResult ValueType(); + +} + +[FixedResponseFilter] +public class RawBytesResponseTestService : ServiceBase, IRawBytesResponseTestService +{ + public UnaryResult RefType() + => UnaryResult.FromResult(new RawBytesResponseTestService_RefTypeResponse() { Value1 = Guid.NewGuid().ToString(), Value2 = Random.Shared.Next(), Value3 = Random.Shared.NextInt64() }); + + public UnaryResult ValueType() + => UnaryResult.FromResult(new RawBytesResponseTestService_ValueTypeResponse(){ Value1 = Guid.NewGuid().ToString(), Value2 = Random.Shared.Next(), Value3 = Random.Shared.NextInt64() }); +} + +[MessagePackObject(true)] +public class RawBytesResponseTestService_RefTypeResponse +{ + public string Value1 { get; set; } + public int Value2 { get; set; } + public long Value3 { get; set; } +} + +[MessagePackObject(true)] +public struct RawBytesResponseTestService_ValueTypeResponse +{ + public string Value1 { get; set; } + public int Value2 { get; set; } + public long Value3 { get; set; } +} From 4ca00f72040c5fe23665e2f90b04bf42c46507f9 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Tue, 19 Sep 2023 12:33:34 +0900 Subject: [PATCH 2/4] Introduce ServiceContext.SetRawBytesResponse --- .../Internal}/RawBytesBox.cs | 6 ++++-- .../Internal}/RawBytesBox.cs | 6 ++++-- .../Internal/RawBytesBox.cs.meta | 11 +++++++++++ src/MagicOnion.Server/ServiceContext.cs | 18 ++++++++++++++---- .../RawBytesResponseTest.cs | 2 +- 5 files changed, 34 insertions(+), 9 deletions(-) rename src/{MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions => MagicOnion.Abstractions/Internal}/RawBytesBox.cs (62%) rename src/{MagicOnion.Abstractions => MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal}/RawBytesBox.cs (63%) create mode 100644 src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs.meta diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/RawBytesBox.cs b/src/MagicOnion.Abstractions/Internal/RawBytesBox.cs similarity index 62% rename from src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/RawBytesBox.cs rename to src/MagicOnion.Abstractions/Internal/RawBytesBox.cs index f52dce471..00b4a2e5f 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/RawBytesBox.cs +++ b/src/MagicOnion.Abstractions/Internal/RawBytesBox.cs @@ -1,7 +1,10 @@ using System; +using System.ComponentModel; -namespace MagicOnion +namespace MagicOnion.Internal { + // Pubternal API + [EditorBrowsable(EditorBrowsableState.Never)] public sealed class RawBytesBox { public ReadOnlyMemory Bytes { get; } @@ -12,4 +15,3 @@ public RawBytesBox(ReadOnlyMemory bytes) } } } - diff --git a/src/MagicOnion.Abstractions/RawBytesBox.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs similarity index 63% rename from src/MagicOnion.Abstractions/RawBytesBox.cs rename to src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs index f52dce471..132df4c60 100644 --- a/src/MagicOnion.Abstractions/RawBytesBox.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs @@ -1,7 +1,10 @@ using System; +using System.ComponentModel; -namespace MagicOnion +namespace MagicOnion.Internal { + // Pubternal + [EditorBrowsable(EditorBrowsableState.Never)] public sealed class RawBytesBox { public ReadOnlyMemory Bytes { get; } @@ -12,4 +15,3 @@ public RawBytesBox(ReadOnlyMemory bytes) } } } - diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs.meta b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs.meta new file mode 100644 index 000000000..0fb354b7b --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e38ae70e650f974458dc1814576f4747 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Server/ServiceContext.cs b/src/MagicOnion.Server/ServiceContext.cs index fd574061e..a67c3cc12 100644 --- a/src/MagicOnion.Server/ServiceContext.cs +++ b/src/MagicOnion.Server/ServiceContext.cs @@ -4,6 +4,7 @@ using MessagePack; using System.Collections.Concurrent; using System.Reflection; +using MagicOnion.Internal; namespace MagicOnion.Server; @@ -107,27 +108,36 @@ IServiceProvider serviceProvider this.ServiceProvider = serviceProvider; } - /// Get Raw Request. + /// Gets a request object. public object? GetRawRequest() { return request; } - /// Set Raw Request, you can set before method body was called. + /// Sets a request object. public void SetRawRequest(object? request) { this.request = request; } - /// Can get after method body was finished. + /// Gets a response object. The object is available after the service method has completed. public object? GetRawResponse() { return Result; } - /// Can set after method body was finished. + /// Sets a response object. This can overwrite the result of the service method. public void SetRawResponse(object? response) { Result = response; } + + /// + /// Sets a raw bytes response. The response will not be serialized and the bytes will be sent directly. + /// This can overwrite the result of the service method. + /// + public void SetRawBytesResponse(ReadOnlyMemory bytes) + { + Result = new RawBytesBox(bytes); + } } diff --git a/tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs b/tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs index 6bad2b026..e2fefe30a 100644 --- a/tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs +++ b/tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs @@ -77,7 +77,7 @@ public override async ValueTask Invoke(ServiceContext context, Func Date: Tue, 19 Sep 2023 12:34:53 +0900 Subject: [PATCH 3/4] Sync --- .../MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs | 2 +- .../Serialization/MessagePackMagicOnionSerializerProvider.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs index 132df4c60..00b4a2e5f 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/Internal/RawBytesBox.cs @@ -3,7 +3,7 @@ namespace MagicOnion.Internal { - // Pubternal + // Pubternal API [EditorBrowsable(EditorBrowsableState.Never)] public sealed class RawBytesBox { diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/Serialization/MessagePackMagicOnionSerializerProvider.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/Serialization/MessagePackMagicOnionSerializerProvider.cs index b7c8d8240..a6633affb 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/Serialization/MessagePackMagicOnionSerializerProvider.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/Serialization/MessagePackMagicOnionSerializerProvider.cs @@ -111,7 +111,6 @@ public MessagePackMagicOnionSerializer(MessagePackSerializerOptions serializerOp public T Deserialize(in ReadOnlySequence bytes) => MessagePackSerializer.Deserialize(bytes, serializerOptions); - public void Serialize(IBufferWriter writer, in T value) => MessagePackSerializer.Serialize(writer, value, serializerOptions); } From e60ecf12406ee8c5c41eec37eac8fce6da560467 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Tue, 19 Sep 2023 12:54:43 +0900 Subject: [PATCH 4/4] Fix warnings --- tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs b/tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs index e2fefe30a..0d1046aec 100644 --- a/tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs +++ b/tests/MagicOnion.Server.Tests/RawBytesResponseTest.cs @@ -64,7 +64,7 @@ public async Task ValueType() public static readonly RawBytesResponseTestService_RefTypeResponse ResponseRefType; public static readonly RawBytesResponseTestService_ValueTypeResponse ResponseValueType; - public static readonly ConcurrentDictionary ResponseCache = new(); + public static readonly ConcurrentDictionary ResponseCache = new(); public static readonly ConcurrentDictionary ResponseBytesCache = new(); static FixedResponseFilterAttribute()