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 15c15ea82..85854e909 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 @@ -113,7 +113,6 @@ public static MagicOnionMethod C } } - // 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. @@ -124,7 +123,7 @@ public static MagicOnionMethod C var writer = ctx.GetBufferWriter(); var buffer = writer.GetSpan(unsafeNilBytes.Length); // Write `Nil` as `byte[]` to the buffer. - MagicOnionMarshallers.UnsafeNilBytes.CopyTo(buffer); + unsafeNilBytes.CopyTo(buffer); writer.Advance(unsafeNilBytes.Length); ctx.Complete(); diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/MagicOnionMarshallers.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/MagicOnionMarshallers.cs index 7dcdf30d3..c557fbac1 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/MagicOnionMarshallers.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Shared/MagicOnionMarshallers.cs @@ -24,8 +24,6 @@ public static class MagicOnionMarshallers .OrderBy(x => x.GetGenericArguments().Length) .ToArray(); - public static readonly byte[] UnsafeNilBytes = new byte[] { MessagePackCode.Nil }; - public static readonly Marshaller ThroughMarshaller = new Marshaller(x => x, x => x); internal static Type CreateRequestType(ParameterInfo[] parameters) diff --git a/src/MagicOnion.GeneratorCore/CodeAnalysis/MagicOnionTypeInfo.cs b/src/MagicOnion.GeneratorCore/CodeAnalysis/MagicOnionTypeInfo.cs index c1d6ca445..70dde03ae 100644 --- a/src/MagicOnion.GeneratorCore/CodeAnalysis/MagicOnionTypeInfo.cs +++ b/src/MagicOnion.GeneratorCore/CodeAnalysis/MagicOnionTypeInfo.cs @@ -14,6 +14,7 @@ public static class KnownTypes public static MagicOnionTypeInfo System_Boolean { get; } = new MagicOnionTypeInfo("System", "Boolean", SubType.ValueType); public static MagicOnionTypeInfo MessagePack_Nil { get; } = new MagicOnionTypeInfo("MessagePack", "Nil", SubType.ValueType); public static MagicOnionTypeInfo System_Threading_Tasks_Task { get; } = new MagicOnionTypeInfo("System.Threading.Tasks", "Task"); + public static MagicOnionTypeInfo System_Threading_Tasks_ValueTask { get; } = new MagicOnionTypeInfo("System.Threading.Tasks", "ValueTask", SubType.ValueType); public static MagicOnionTypeInfo MagicOnion_UnaryResult { get; } = new MagicOnionTypeInfo("MagicOnion", "UnaryResult", SubType.ValueType); // ReSharper restore InconsistentNaming } @@ -26,6 +27,12 @@ public static class KnownTypes public IReadOnlyList GenericArguments { get; } public bool HasGenericArguments => GenericArguments.Any(); + public MagicOnionTypeInfo GetGenericTypeDefinition() + { + if (!HasGenericArguments) throw new InvalidOperationException("The type is not constructed generic type."); + return MagicOnionTypeInfo.Create(Namespace, Name, Array.Empty(), IsValueType); + } + public bool IsArray => _subType == SubType.Array; public int ArrayRank { get; } public MagicOnionTypeInfo ElementType { get; } @@ -120,6 +127,7 @@ public static MagicOnionTypeInfo Create(string @namespace, string name, MagicOni if (@namespace == "System" && name == "String") return KnownTypes.System_String; if (@namespace == "System" && name == "Boolean") return KnownTypes.System_Boolean; if (@namespace == "System.Threading.Tasks" && name == "Task" && genericArguments.Length == 0) return KnownTypes.System_Threading_Tasks_Task; + if (@namespace == "System.Threading.Tasks" && name == "ValueTask" && genericArguments.Length == 0) return KnownTypes.System_Threading_Tasks_ValueTask; if (@namespace == "MagicOnion" && name == "UnaryResult" && genericArguments.Length == 0) return KnownTypes.MagicOnion_UnaryResult; return new MagicOnionTypeInfo(@namespace, name, isValueType ? SubType.ValueType : SubType.None, arrayRank:0, genericArguments); diff --git a/src/MagicOnion.GeneratorCore/CodeAnalysis/MethodCollector.cs b/src/MagicOnion.GeneratorCore/CodeAnalysis/MethodCollector.cs index cc161400f..5ac5c664b 100644 --- a/src/MagicOnion.GeneratorCore/CodeAnalysis/MethodCollector.cs +++ b/src/MagicOnion.GeneratorCore/CodeAnalysis/MethodCollector.cs @@ -85,9 +85,11 @@ MagicOnionStreamingHubInfo.MagicOnionHubMethodInfo CreateHubMethodInfoFromMethod switch (methodReturnType.FullNameOpenType) { case "global::System.Threading.Tasks.Task": + case "global::System.Threading.Tasks.ValueTask": //responseType = MagicOnionTypeInfo.KnownTypes.MessagePack_Nil; break; case "global::System.Threading.Tasks.Task<>": + case "global::System.Threading.Tasks.ValueTask<>": responseType = methodReturnType.GenericArguments[0]; break; default: diff --git a/src/MagicOnion.GeneratorCore/CodeGen/StaticStreamingHubClientGenerator.cs b/src/MagicOnion.GeneratorCore/CodeGen/StaticStreamingHubClientGenerator.cs index 391cafba0..6f2fc9e5d 100644 --- a/src/MagicOnion.GeneratorCore/CodeGen/StaticStreamingHubClientGenerator.cs +++ b/src/MagicOnion.GeneratorCore/CodeGen/StaticStreamingHubClientGenerator.cs @@ -173,10 +173,30 @@ static void EmitHubMethods(StreamingHubClientBuildContext ctx, bool isFireAndFor _ => $", {method.Parameters.ToNewDynamicArgumentTuple()}", }; - ctx.TextWriter.WriteLines($""" - public {method.MethodReturnType.FullName} {method.MethodName}({method.Parameters.ToMethodSignaturize()}) - => {(isFireAndForget ? "parent.WriteMessageFireAndForgetAsync" : "base.WriteMessageWithResponseAsync")}<{method.RequestType.FullName}, {method.ResponseType.FullName}>({method.HubId}{writeMessageParameters}); - """); + if (method.MethodReturnType == MagicOnionTypeInfo.KnownTypes.System_Threading_Tasks_ValueTask) + { + // ValueTask + ctx.TextWriter.WriteLines($""" + public {method.MethodReturnType.FullName} {method.MethodName}({method.Parameters.ToMethodSignaturize()}) + => new global::System.Threading.Tasks.ValueTask({(isFireAndForget ? "parent.WriteMessageFireAndForgetAsync" : "base.WriteMessageWithResponseAsync")}<{method.RequestType.FullName}, {method.ResponseType.FullName}>({method.HubId}{writeMessageParameters})); + """); + } + else if (method.MethodReturnType.HasGenericArguments && method.MethodReturnType.GetGenericTypeDefinition() == MagicOnionTypeInfo.KnownTypes.System_Threading_Tasks_ValueTask) + { + // ValueTask + ctx.TextWriter.WriteLines($""" + public {method.MethodReturnType.FullName} {method.MethodName}({method.Parameters.ToMethodSignaturize()}) + => new global::System.Threading.Tasks.ValueTask<{method.ResponseType.FullName}>({(isFireAndForget ? "parent.WriteMessageFireAndForgetAsync" : "base.WriteMessageWithResponseAsync")}<{method.RequestType.FullName}, {method.ResponseType.FullName}>({method.HubId}{writeMessageParameters})); + """); + } + else + { + // Task, Task + ctx.TextWriter.WriteLines($""" + public {method.MethodReturnType.FullName} {method.MethodName}({method.Parameters.ToMethodSignaturize()}) + => {(isFireAndForget ? "parent.WriteMessageFireAndForgetAsync" : "base.WriteMessageWithResponseAsync")}<{method.RequestType.FullName}, {method.ResponseType.FullName}>({method.HubId}{writeMessageParameters}); + """); + } } // #endif } diff --git a/src/MagicOnion.Server/Hubs/StreamingHubContext.cs b/src/MagicOnion.Server/Hubs/StreamingHubContext.cs index c2eaf64f9..9f0aa0798 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubContext.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubContext.cs @@ -39,7 +39,7 @@ public ConcurrentDictionary Items internal Type? responseType; // helper for reflection - internal async ValueTask WriteResponseMessageNil(Task value) + internal async ValueTask WriteResponseMessageNil(ValueTask value) { if (MessageId == -1) // don't write. { @@ -70,7 +70,7 @@ byte[] BuildMessage() responseType = typeof(Nil); } - internal async ValueTask WriteResponseMessage(Task value) + internal async ValueTask WriteResponseMessage(ValueTask value) { if (MessageId == -1) // don't write. { diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs index 2d060b7ab..8cd78e584 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs @@ -2,6 +2,7 @@ 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; @@ -59,9 +60,7 @@ public StreamingHubHandler(Type classType, MethodInfo methodInfo, StreamingHubHa var invokeHubMethodFunc = Expression.Lambda(callHubMethod, contextArg, requestArg).Compile(); // Create a StreamingHub method invoker and a wrapped-invoke method. - Type invokerType = metadata.ResponseType is null - ? typeof(StreamingHubMethodInvoker<>).MakeGenericType(metadata.RequestType) - : typeof(StreamingHubMethodInvoker<,>).MakeGenericType(metadata.RequestType, metadata.ResponseType); + Type invokerType = StreamingHubMethodInvoker.CreateInvokerTypeFromMetadata(metadata); StreamingHubMethodInvoker invoker = (StreamingHubMethodInvoker)Activator.CreateInstance(invokerType, messageSerializer, invokeHubMethodFunc)!; var filters = FilterHelper.GetFilters(handlerOptions.GlobalStreamingHubFilters, classType, methodInfo); @@ -73,23 +72,63 @@ public StreamingHubHandler(Type classType, MethodInfo methodInfo, StreamingHubHa } } - abstract class StreamingHubMethodInvoker + public override string ToString() + => toStringCache; + + public override int GetHashCode() + => getHashCodeCache; + + public bool Equals(StreamingHubHandler? other) + => other != null && HubName.Equals(other.HubName) && MethodInfo.Name.Equals(other.MethodInfo.Name); +} + +/// +/// Options for StreamingHubHandler construction. +/// +public class StreamingHubHandlerOptions +{ + public IList GlobalStreamingHubFilters { get; } + + public IMagicOnionMessageSerializerProvider MessageSerializer { get; } + + public StreamingHubHandlerOptions(MagicOnionOptions options) { - protected IMagicOnionMessageSerializer MessageSerializer { get; } + GlobalStreamingHubFilters = options.GlobalStreamingHubFilters; + MessageSerializer = options.MessageSerializer; + } +} - protected StreamingHubMethodInvoker(IMagicOnionMessageSerializer messageSerializer) - { - MessageSerializer = messageSerializer; - } +internal abstract class StreamingHubMethodInvoker +{ + protected IMagicOnionMessageSerializer MessageSerializer { get; } - public abstract ValueTask InvokeAsync(StreamingHubContext context); + protected StreamingHubMethodInvoker(IMagicOnionMessageSerializer messageSerializer) + { + MessageSerializer = messageSerializer; + } + + public abstract ValueTask InvokeAsync(StreamingHubContext context); + + public static Type CreateInvokerTypeFromMetadata(in StreamingHubMethodHandlerMetadata metadata) + { + var isTaskOrTaskOfT = metadata.InterfaceMethod.ReturnType == typeof(Task) || + (metadata.InterfaceMethod.ReturnType is { IsGenericType: true } t && t.BaseType == typeof(Task)); + return 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 StreamingHubMethodInvoker : StreamingHubMethodInvoker + sealed class StreamingHubMethodInvokerTask : StreamingHubMethodInvoker { readonly Func> hubMethodFunc; - public StreamingHubMethodInvoker(IMagicOnionMessageSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) + public StreamingHubMethodInvokerTask(IMagicOnionMessageSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) { this.hubMethodFunc = (Func>)hubMethodFunc; } @@ -99,15 +138,15 @@ 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(response); + return context.WriteResponseMessage(new ValueTask(response)); } } - sealed class StreamingHubMethodInvoker : StreamingHubMethodInvoker + sealed class StreamingHubMethodInvokerTask : StreamingHubMethodInvoker { readonly Func hubMethodFunc; - public StreamingHubMethodInvoker(IMagicOnionMessageSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) + public StreamingHubMethodInvokerTask(IMagicOnionMessageSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) { this.hubMethodFunc = (Func)hubMethodFunc; } @@ -117,32 +156,44 @@ 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(response); + return context.WriteResponseMessageNil(new ValueTask(response)); } } - public override string ToString() - => toStringCache; + sealed class StreamingHubMethodInvokerValueTask : StreamingHubMethodInvoker + { + readonly Func> hubMethodFunc; - public override int GetHashCode() - => getHashCodeCache; + public StreamingHubMethodInvokerValueTask(IMagicOnionMessageSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) + { + this.hubMethodFunc = (Func>)hubMethodFunc; + } - public bool Equals(StreamingHubHandler? other) - => other != null && HubName.Equals(other.HubName) && MethodInfo.Name.Equals(other.MethodInfo.Name); -} + 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); + } + } -/// -/// Options for StreamingHubHandler construction. -/// -public class StreamingHubHandlerOptions -{ - public IList GlobalStreamingHubFilters { get; } + sealed class StreamingHubMethodInvokerValueTask : StreamingHubMethodInvoker + { + readonly Func hubMethodFunc; - public IMagicOnionMessageSerializerProvider MessageSerializer { get; } + public StreamingHubMethodInvokerValueTask(IMagicOnionMessageSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) + { + this.hubMethodFunc = (Func)hubMethodFunc; + } - public StreamingHubHandlerOptions(MagicOnionOptions options) - { - GlobalStreamingHubFilters = options.GlobalStreamingHubFilters; - MessageSerializer = options.MessageSerializer; + 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/Internal/MethodHandlerMetadata.cs b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs index 25279e85d..99c4ff6f5 100644 --- a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs +++ b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs @@ -95,7 +95,7 @@ public static StreamingHubMethodHandlerMetadata CreateStreamingHubMethodHandlerM { var hubInterface = serviceClass.GetInterfaces().First(x => x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == typeof(IStreamingHub<,>)).GetGenericArguments()[0]; var parameters = methodInfo.GetParameters(); - var responseType = UnwrapStreamingHubResponseType(methodInfo, out var responseIsTask); + var responseType = UnwrapStreamingHubResponseType(methodInfo, out var responseIsTaskOrValueTask); var requestType = GetRequestTypeFromMethod(methodInfo, parameters); var attributeLookup = serviceClass.GetCustomAttributes(true) @@ -105,9 +105,9 @@ public static StreamingHubMethodHandlerMetadata CreateStreamingHubMethodHandlerM var interfaceMethodInfo = ResolveInterfaceMethod(serviceClass, hubInterface, methodInfo.Name); - if (!responseIsTask) + if (!responseIsTaskOrValueTask) { - throw new InvalidOperationException($"A type of the StreamingHub method must be Task or Task. (Member:{serviceClass.Name}.{methodInfo.Name})"); + throw new InvalidOperationException($"A type of the StreamingHub method must be Task, Task, ValueTask or ValueTask. (Member:{serviceClass.Name}.{methodInfo.Name})"); } var methodId = interfaceMethodInfo.GetCustomAttribute()?.MethodId ?? FNV1A32.GetHashCode(interfaceMethodInfo.Name); @@ -187,19 +187,19 @@ static Type UnwrapUnaryResponseType(MethodInfo methodInfo, out MethodType method throw new InvalidOperationException($"The method '{methodInfo.Name}' has invalid return type. path:{methodInfo.DeclaringType!.Name + "/" + methodInfo.Name} type:{methodInfo.ReturnType.Name}"); } - static Type? UnwrapStreamingHubResponseType(MethodInfo methodInfo, out bool responseIsTask) + static Type? UnwrapStreamingHubResponseType(MethodInfo methodInfo, out bool responseIsTaskOrValueTask) { var t = methodInfo.ReturnType; // Task - if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Task<>)) + if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(Task<>) || t.GetGenericTypeDefinition() == typeof(ValueTask<>))) { - responseIsTask = true; + responseIsTaskOrValueTask = true; return t.GetGenericArguments()[0]; } - else if (t == typeof(Task)) + else if (t == typeof(Task) || t == typeof(ValueTask)) { - responseIsTask = true; + responseIsTaskOrValueTask = true; return null; } diff --git a/tests/MagicOnion.Generator.Tests/Collector/MagicOnionTypeInfoTest.cs b/tests/MagicOnion.Generator.Tests/Collector/MagicOnionTypeInfoTest.cs index ee40aca58..193b4d728 100644 --- a/tests/MagicOnion.Generator.Tests/Collector/MagicOnionTypeInfoTest.cs +++ b/tests/MagicOnion.Generator.Tests/Collector/MagicOnionTypeInfoTest.cs @@ -698,4 +698,15 @@ public void EnumerateDependentTypes_Generics_Nested() MagicOnionTypeInfo.CreateFromType(), MagicOnionTypeInfo.CreateFromType()); } + + [Fact] + public void GetGenericTypeDefinition() + { + // Arrange + var typeInfo = MagicOnionTypeInfo.CreateFromType>(); + // Act + var genericDefinition = typeInfo.GetGenericTypeDefinition(); + // Assert + genericDefinition.Should().Be(MagicOnionTypeInfo.Create("System", "ValueTuple", Array.Empty(), true)); + } } diff --git a/tests/MagicOnion.Generator.Tests/Collector/MethodCollectorStreamingHubsTest.cs b/tests/MagicOnion.Generator.Tests/Collector/MethodCollectorStreamingHubsTest.cs index cdb4f64e3..232275d9d 100644 --- a/tests/MagicOnion.Generator.Tests/Collector/MethodCollectorStreamingHubsTest.cs +++ b/tests/MagicOnion.Generator.Tests/Collector/MethodCollectorStreamingHubsTest.cs @@ -451,7 +451,7 @@ public interface IMyHubReceiver var collector = new MethodCollector(); var ex = Assert.Throws(() => collector.Collect(compilation)); } - + [Fact] public void ReturnType_Task() { @@ -487,7 +487,7 @@ public interface IMyHubReceiver serviceCollection.Hubs[0].Methods[0].ResponseType.Should().Be(MagicOnionTypeInfo.KnownTypes.MessagePack_Nil); serviceCollection.Hubs[0].Methods[0].MethodReturnType.Should().Be(MagicOnionTypeInfo.KnownTypes.System_Threading_Tasks_Task); } - + [Fact] public void ReturnType_TaskOfT() { @@ -524,6 +524,78 @@ public interface IMyHubReceiver serviceCollection.Hubs[0].Methods[0].MethodReturnType.Should().Be(MagicOnionTypeInfo.CreateFromType>()); } + [Fact] + public void ReturnType_ValueTask() + { + // Arrange + var source = @" +using System; +using System.Threading.Tasks; +using MagicOnion; +using MessagePack; + +namespace MyNamespace; + +public interface IMyHub : IStreamingHub +{ + ValueTask MethodA(); +} + +public interface IMyHubReceiver +{ + void EventA(); +} +"; + using var tempWorkspace = TemporaryProjectWorkarea.Create(); + tempWorkspace.AddFileToProject("IMyHub.cs", source); + var compilation = tempWorkspace.GetOutputCompilation().Compilation; + + // Act + var collector = new MethodCollector(); + var serviceCollection = collector.Collect(compilation); + + // Assert + serviceCollection.Hubs[0].Methods[0].MethodName.Should().Be("MethodA"); + serviceCollection.Hubs[0].Methods[0].ResponseType.Should().Be(MagicOnionTypeInfo.KnownTypes.MessagePack_Nil); + serviceCollection.Hubs[0].Methods[0].MethodReturnType.Should().Be(MagicOnionTypeInfo.KnownTypes.System_Threading_Tasks_ValueTask); + } + + [Fact] + public void ReturnType_ValueTaskOfT() + { + // Arrange + var source = @" +using System; +using System.Threading.Tasks; +using MagicOnion; +using MessagePack; + +namespace MyNamespace; + +public interface IMyHub : IStreamingHub +{ + ValueTask MethodA(); +} + +public interface IMyHubReceiver +{ + void EventA(); +} +"; + using var tempWorkspace = TemporaryProjectWorkarea.Create(); + tempWorkspace.AddFileToProject("IMyHub.cs", source); + var compilation = tempWorkspace.GetOutputCompilation().Compilation; + + // Act + var collector = new MethodCollector(); + var serviceCollection = collector.Collect(compilation); + + // Assert + serviceCollection.Hubs[0].Methods[0].MethodName.Should().Be("MethodA"); + serviceCollection.Hubs[0].Methods[0].ResponseType.Should().Be(MagicOnionTypeInfo.KnownTypes.System_String); + serviceCollection.Hubs[0].Methods[0].MethodReturnType.Should().Be(MagicOnionTypeInfo.CreateFromType>()); + } + [Fact] public void Receiver() { diff --git a/tests/MagicOnion.Generator.Tests/GenerateStreamingHub.cs b/tests/MagicOnion.Generator.Tests/GenerateStreamingHub.cs index b6c9078f8..4868113f5 100644 --- a/tests/MagicOnion.Generator.Tests/GenerateStreamingHub.cs +++ b/tests/MagicOnion.Generator.Tests/GenerateStreamingHub.cs @@ -264,6 +264,84 @@ await compiler.GenerateFileAsync( compilation.GetCompilationErrors().Should().BeEmpty(); } + [Fact] + public async Task Return_ValueTask() + { + using var tempWorkspace = TemporaryProjectWorkarea.Create(); + tempWorkspace.AddFileToProject("IMyService.cs", @" +using System; +using System.Threading.Tasks; +using MessagePack; +using MagicOnion; + +namespace TempProject +{ + public interface IMyHubReceiver { } + public interface IMyHub : IStreamingHub + { + ValueTask A(MyObject a); + } + + [MessagePackObject] + public class MyObject + { + } +} + "); + + var compiler = new MagicOnionCompiler(new MagicOnionGeneratorTestOutputLogger(testOutputHelper), CancellationToken.None); + await compiler.GenerateFileAsync( + tempWorkspace.CsProjectPath, + Path.Combine(tempWorkspace.OutputDirectory, "Generated.cs"), + true, + "TempProject.Generated", + "", + "MessagePack.Formatters" + ); + + var compilation = tempWorkspace.GetOutputCompilation(); + compilation.GetCompilationErrors().Should().BeEmpty(); + } + + [Fact] + public async Task Return_ValueTaskOfT() + { + using var tempWorkspace = TemporaryProjectWorkarea.Create(); + tempWorkspace.AddFileToProject("IMyService.cs", @" +using System; +using System.Threading.Tasks; +using MessagePack; +using MagicOnion; + +namespace TempProject +{ + public interface IMyHubReceiver { } + public interface IMyHub : IStreamingHub + { + ValueTask A(MyObject a); + } + + [MessagePackObject] + public class MyObject + { + } +} + "); + + var compiler = new MagicOnionCompiler(new MagicOnionGeneratorTestOutputLogger(testOutputHelper), CancellationToken.None); + await compiler.GenerateFileAsync( + tempWorkspace.CsProjectPath, + Path.Combine(tempWorkspace.OutputDirectory, "Generated.cs"), + true, + "TempProject.Generated", + "", + "MessagePack.Formatters" + ); + + var compilation = tempWorkspace.GetOutputCompilation(); + compilation.GetCompilationErrors().Should().BeEmpty(); + } + [Fact] public async Task Invalid_Return_Void() { diff --git a/tests/MagicOnion.Generator.Tests/MagicOnionGeneratorTestOutputLogger.cs b/tests/MagicOnion.Generator.Tests/MagicOnionGeneratorTestOutputLogger.cs index 589dce611..e00b813d2 100644 --- a/tests/MagicOnion.Generator.Tests/MagicOnionGeneratorTestOutputLogger.cs +++ b/tests/MagicOnion.Generator.Tests/MagicOnionGeneratorTestOutputLogger.cs @@ -12,7 +12,7 @@ public MagicOnionGeneratorTestOutputLogger(ITestOutputHelper outputHelper) this.outputHelper = outputHelper; } -#if TRUE +#if FALSE public void Trace(string message) => outputHelper.WriteLine(message); #else public void Trace(string message) {} diff --git a/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs b/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs index 439677d8f..1e2a11b3e 100644 --- a/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs +++ b/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs @@ -127,6 +127,106 @@ public async Task Parameter_Many(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = new Mock(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver.Object); + + // Act & Assert + await client.ValueTask_NoReturn_Parameter_Zero(); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task ValueTask_NoReturn_Parameter_One(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = new Mock(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver.Object); + + // Act & Assert + await client.ValueTask_NoReturn_Parameter_One(12345); + } + + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task ValueTask_NoReturn_Parameter_Many(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = new Mock(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver.Object); + + // Act & Assert + await client.ValueTask_NoReturn_Parameter_Many(12345, "Hello✨", true); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task ValueTask_Parameter_Zero(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = new Mock(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver.Object); + + // Act + var result = await client.ValueTask_Parameter_Zero(); + + // Assert + result.Should().Be(67890); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task ValueTask_Parameter_One(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = new Mock(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver.Object); + + // Act + var result = await client.ValueTask_Parameter_One(12345); + + // Assert + result.Should().Be(67890); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task ValueTask_Parameter_Many(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = new Mock(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver.Object); + + // Act + var result = await client.ValueTask_Parameter_Many(12345, "Hello✨", true); + + // Assert + result.Should().Be(67890); + } + [Theory] [MemberData(nameof(EnumerateStreamingHubClientFactory))] public async Task Receiver_Parameter_Zero(TestStreamingHubClientFactory clientFactory) @@ -294,7 +394,44 @@ public Task Never_With_Return() { return new TaskCompletionSource().Task.WaitAsync(TimeSpan.FromMilliseconds(100)); } + + public ValueTask ValueTask_NoReturn_Parameter_Zero() + { + return default; + } + + public ValueTask ValueTask_NoReturn_Parameter_One(int arg0) + { + Debug.Assert(arg0 == 12345); + return default; + } + public ValueTask ValueTask_NoReturn_Parameter_Many(int arg0, string arg1, bool arg2) + { + Debug.Assert(arg0 == 12345); + Debug.Assert(arg1 == "Hello✨"); + Debug.Assert(arg2 == true); + return default; + } + + public ValueTask ValueTask_Parameter_Zero() + { + return ValueTask.FromResult(67890); + } + + public ValueTask ValueTask_Parameter_One(int arg0) + { + Debug.Assert(arg0 == 12345); + return ValueTask.FromResult(67890); + } + + public ValueTask ValueTask_Parameter_Many(int arg0, string arg1, bool arg2) + { + Debug.Assert(arg0 == 12345); + Debug.Assert(arg1 == "Hello✨"); + Debug.Assert(arg2 == true); + return ValueTask.FromResult(67890); + } } public interface IStreamingHubTestHubReceiver @@ -320,4 +457,13 @@ public interface IStreamingHubTestHub : IStreamingHub Never_With_Return(); + + ValueTask ValueTask_NoReturn_Parameter_Zero(); + ValueTask ValueTask_NoReturn_Parameter_One(int arg0); + ValueTask ValueTask_NoReturn_Parameter_Many(int arg0, string arg1, bool arg2); + + ValueTask ValueTask_Parameter_Zero(); + ValueTask ValueTask_Parameter_One(int arg0); + ValueTask ValueTask_Parameter_Many(int arg0, string arg1, bool arg2); + } diff --git a/tests/MagicOnion.Integration.Tests/_GeneratedClient.cs b/tests/MagicOnion.Integration.Tests/_GeneratedClient.cs index b1a705e0a..5b225ff33 100644 --- a/tests/MagicOnion.Integration.Tests/_GeneratedClient.cs +++ b/tests/MagicOnion.Integration.Tests/_GeneratedClient.cs @@ -684,6 +684,18 @@ public StreamingHubTestHubClient(global::Grpc.Core.CallInvoker callInvoker, glob => base.WriteMessageWithResponseAsync(-1291900119, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task Never_With_Return() => base.WriteMessageWithResponseAsync(2074829953, global::MessagePack.Nil.Default); + public global::System.Threading.Tasks.ValueTask ValueTask_NoReturn_Parameter_Zero() + => new global::System.Threading.Tasks.ValueTask(base.WriteMessageWithResponseAsync(-1145997568, global::MessagePack.Nil.Default)); + public global::System.Threading.Tasks.ValueTask ValueTask_NoReturn_Parameter_One(global::System.Int32 arg0) + => new global::System.Threading.Tasks.ValueTask(base.WriteMessageWithResponseAsync(928334602, arg0)); + public global::System.Threading.Tasks.ValueTask ValueTask_NoReturn_Parameter_Many(global::System.Int32 arg0, global::System.String arg1, global::System.Boolean arg2) + => new global::System.Threading.Tasks.ValueTask(base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-1965648219, new global::MagicOnion.DynamicArgumentTuple(arg0, arg1, arg2))); + public global::System.Threading.Tasks.ValueTask ValueTask_Parameter_Zero() + => new global::System.Threading.Tasks.ValueTask(base.WriteMessageWithResponseAsync(658969434, global::MessagePack.Nil.Default)); + public global::System.Threading.Tasks.ValueTask ValueTask_Parameter_One(global::System.Int32 arg0) + => new global::System.Threading.Tasks.ValueTask(base.WriteMessageWithResponseAsync(-435674772, arg0)); + public global::System.Threading.Tasks.ValueTask ValueTask_Parameter_Many(global::System.Int32 arg0, global::System.String arg1, global::System.Boolean arg2) + => new global::System.Threading.Tasks.ValueTask(base.WriteMessageWithResponseAsync, global::System.Int32>(-52442641, new global::MagicOnion.DynamicArgumentTuple(arg0, arg1, arg2))); public global::MagicOnion.Integration.Tests.IStreamingHubTestHub FireAndForget() => new FireAndForgetClient(this); @@ -722,6 +734,18 @@ public FireAndForgetClient(StreamingHubTestHubClient parent) => parent.WriteMessageFireAndForgetAsync(-1291900119, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task Never_With_Return() => parent.WriteMessageFireAndForgetAsync(2074829953, global::MessagePack.Nil.Default); + public global::System.Threading.Tasks.ValueTask ValueTask_NoReturn_Parameter_Zero() + => new global::System.Threading.Tasks.ValueTask(parent.WriteMessageFireAndForgetAsync(-1145997568, global::MessagePack.Nil.Default)); + public global::System.Threading.Tasks.ValueTask ValueTask_NoReturn_Parameter_One(global::System.Int32 arg0) + => new global::System.Threading.Tasks.ValueTask(parent.WriteMessageFireAndForgetAsync(928334602, arg0)); + public global::System.Threading.Tasks.ValueTask ValueTask_NoReturn_Parameter_Many(global::System.Int32 arg0, global::System.String arg1, global::System.Boolean arg2) + => new global::System.Threading.Tasks.ValueTask(parent.WriteMessageFireAndForgetAsync, global::MessagePack.Nil>(-1965648219, new global::MagicOnion.DynamicArgumentTuple(arg0, arg1, arg2))); + public global::System.Threading.Tasks.ValueTask ValueTask_Parameter_Zero() + => new global::System.Threading.Tasks.ValueTask(parent.WriteMessageFireAndForgetAsync(658969434, global::MessagePack.Nil.Default)); + public global::System.Threading.Tasks.ValueTask ValueTask_Parameter_One(global::System.Int32 arg0) + => new global::System.Threading.Tasks.ValueTask(parent.WriteMessageFireAndForgetAsync(-435674772, arg0)); + public global::System.Threading.Tasks.ValueTask ValueTask_Parameter_Many(global::System.Int32 arg0, global::System.String arg1, global::System.Boolean arg2) + => new global::System.Threading.Tasks.ValueTask(parent.WriteMessageFireAndForgetAsync, global::System.Int32>(-52442641, new global::MagicOnion.DynamicArgumentTuple(arg0, arg1, arg2))); } @@ -787,6 +811,24 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S case 2074829953: // Task Never_With_Return() base.SetResultForResponse(taskCompletionSource, data); break; + case -1145997568: // ValueTask ValueTask_NoReturn_Parameter_Zero() + base.SetResultForResponse(taskCompletionSource, data); + break; + case 928334602: // ValueTask ValueTask_NoReturn_Parameter_One(global::System.Int32 arg0) + base.SetResultForResponse(taskCompletionSource, data); + break; + case -1965648219: // ValueTask ValueTask_NoReturn_Parameter_Many(global::System.Int32 arg0, global::System.String arg1, global::System.Boolean arg2) + base.SetResultForResponse(taskCompletionSource, data); + break; + case 658969434: // ValueTask ValueTask_Parameter_Zero() + base.SetResultForResponse(taskCompletionSource, data); + break; + case -435674772: // ValueTask ValueTask_Parameter_One(global::System.Int32 arg0) + base.SetResultForResponse(taskCompletionSource, data); + break; + case -52442641: // ValueTask ValueTask_Parameter_Many(global::System.Int32 arg0, global::System.String arg1, global::System.Boolean arg2) + base.SetResultForResponse(taskCompletionSource, data); + break; } } diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs index f9cedc926..a60980ee4 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs @@ -86,6 +86,83 @@ byte[] BuildMessage() } fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage()); } + [Fact] + public async Task Parameterless_Returns_ValueTask() + { + // Arrange + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + var hubType = typeof(StreamingHubHandlerTestHub); + var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTask))!; + var hubInstance = new StreamingHubHandlerTestHub(); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMessageMagicOnionSerializerProvider.Instance.Create(MethodType.DuplexStreaming, null), serviceProvider); + + // Act + var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var ctx = new StreamingHubContext() + { + HubInstance = hubInstance, + ServiceContext = fakeStreamingHubContext, + Request = MessagePackSerializer.Serialize(Nil.Default), + }; + await handler.MethodBody.Invoke(ctx); + + // Assert + hubInstance.Results.Should().Contain(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTask) + " called."); + byte[] BuildMessage() + { + // [MessageId, MethodId, Nil] + var buffer = new ArrayBufferWriter(); + var writer = new MessagePackWriter(buffer); + writer.WriteArrayHeader(3); + writer.Write(ctx.MessageId); + writer.Write(ctx.MethodId); + writer.WriteNil(); + writer.Flush(); + + return buffer.WrittenMemory.ToArray(); + } + fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage()); + } + + [Fact] + public async Task Parameterless_Returns_ValueTaskOfInt32() + { + // Arrange + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + var hubType = typeof(StreamingHubHandlerTestHub); + var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTaskOfInt32))!; + var hubInstance = new StreamingHubHandlerTestHub(); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMessageMagicOnionSerializerProvider.Instance.Create(MethodType.DuplexStreaming, null), serviceProvider); + + // Act + var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var ctx = new StreamingHubContext() + { + HubInstance = hubInstance, + ServiceContext = fakeStreamingHubContext, + Request = MessagePackSerializer.Serialize(Nil.Default), + }; + await handler.MethodBody.Invoke(ctx); + + // Assert + hubInstance.Results.Should().Contain(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTaskOfInt32) + " called."); + byte[] BuildMessage() + { + // [MessageId, MethodId, {Int32:12345}] + var buffer = new ArrayBufferWriter(); + var writer = new MessagePackWriter(buffer); + writer.WriteArrayHeader(3); + writer.Write(ctx.MessageId); + writer.Write(ctx.MethodId); + MessagePackSerializer.Serialize(ref writer, 12345); + writer.Flush(); + + return buffer.WrittenMemory.ToArray(); + } + fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage()); + } [Fact] public async Task Parameter_Single_Returns_Task() @@ -306,6 +383,10 @@ interface IStreamingHubHandlerTestHub : IStreamingHub Method_Parameter_Multiple_Returns_TaskOfInt32(int arg0, string arg1, bool arg2); + + ValueTask Method_Parameterless_Returns_ValueTask(); + ValueTask Method_Parameterless_Returns_ValueTaskOfInt32(); + } class StreamingHubHandlerTestHub : IStreamingHubHandlerTestHub { @@ -344,5 +425,18 @@ public Task Method_Parameter_Multiple_Returns_TaskOfInt32(int arg0, string Results.Add(nameof(Method_Parameter_Multiple_Returns_TaskOfInt32) + $"({arg0},{arg1},{arg2}) called."); return Task.FromResult(arg0); } + + public ValueTask Method_Parameterless_Returns_ValueTask() + { + Results.Add(nameof(Method_Parameterless_Returns_ValueTask) + " called."); + return ValueTask.CompletedTask; + } + + public ValueTask Method_Parameterless_Returns_ValueTaskOfInt32() + { + Results.Add(nameof(Method_Parameterless_Returns_ValueTaskOfInt32) + " called."); + return ValueTask.FromResult(12345); + } + } } diff --git a/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs index 690391b5b..83d1a234b 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs @@ -1,4 +1,4 @@ -using MagicOnion.Server.Hubs; +using MagicOnion.Server.Hubs; using MagicOnion.Server.Internal; using MagicOnion.Utils; using MessagePack; @@ -308,6 +308,46 @@ public void AttributeLookup_Class_Many_Multiple() metadata.AttributeLookup[typeof(MySecondAttribute)].Should().BeEquivalentTo(new MySecondAttribute(0), new MySecondAttribute(1), new MySecondAttribute(2)); } + [Fact] + public void ValueTask() + { + // Arrange + var type = typeof(MyHub); + var methodInfo = type.GetMethod(nameof(MyHub.Method_ValueTask))!; + + // Act + var metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(type, methodInfo); + + // Assert + metadata.StreamingHubImplementationType.Should().Be(); + metadata.StreamingHubInterfaceType.Should().Be(); + metadata.InterfaceMethod.Should().BeSameAs(typeof(IMyHub).GetMethod(nameof(IMyHub.Method_ValueTask))); + metadata.ImplementationMethod.Should().BeSameAs(methodInfo); + metadata.Parameters.Should().BeEmpty(); + metadata.RequestType.Should().Be(); + metadata.ResponseType.Should().BeNull(); + } + + [Fact] + public void ValueTaskOfT() + { + // Arrange + var type = typeof(MyHub); + var methodInfo = type.GetMethod(nameof(MyHub.Method_ValueTaskOfValue))!; + + // Act + var metadata = MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(type, methodInfo); + + // Assert + metadata.StreamingHubImplementationType.Should().Be(); + metadata.StreamingHubInterfaceType.Should().Be(); + metadata.InterfaceMethod.Should().BeSameAs(typeof(IMyHub).GetMethod(nameof(IMyHub.Method_ValueTaskOfValue))); + metadata.ImplementationMethod.Should().BeSameAs(methodInfo); + metadata.Parameters.Should().BeEmpty(); + metadata.RequestType.Should().Be(); + metadata.ResponseType.Should().Be(); + } + interface IMyHubReceiver {} @@ -315,6 +355,8 @@ interface IMyHub : IStreamingHub { Task Method_Task(); Task Method_TaskOfValue(); + ValueTask Method_ValueTask(); + ValueTask Method_ValueTaskOfValue(); Task Method_Parameterless(); Task Method_OneParameter(int arg0); @@ -328,6 +370,8 @@ class MyHub : IMyHub { public Task Method_Task() => throw new NotImplementedException(); public Task Method_TaskOfValue() => throw new NotImplementedException(); + public ValueTask Method_ValueTask() => throw new NotImplementedException(); + public ValueTask Method_ValueTaskOfValue() => throw new NotImplementedException(); public Task Method_Parameterless() => throw new NotImplementedException(); public Task Method_OneParameter(int arg0) => throw new NotImplementedException(); public Task Method_TwoParameters(int arg0, string arg1) => throw new NotImplementedException();