From 526005eb311e4d26bad4593c6beb928fc0b0d2cd Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Mon, 12 Dec 2022 18:25:58 +0900 Subject: [PATCH 1/2] Non-Generic UnaryResult --- .../AsyncUnaryResultMethodBuilder.cs | 76 +++++++- src/MagicOnion.Abstractions/UnaryResult.cs | 163 +++++++++++++++--- .../AsyncUnaryResultMethodBuilder.cs | 76 +++++++- .../MagicOnion.Abstractions/UnaryResult.cs | 163 +++++++++++++++--- .../UnaryResultNonGenericTest.cs | 128 ++++++++++++++ .../UnaryResultTest.cs | 2 +- 6 files changed, 555 insertions(+), 53 deletions(-) create mode 100644 tests/MagicOnion.Shared.Tests/UnaryResultNonGenericTest.cs diff --git a/src/MagicOnion.Abstractions/CompilerServices/AsyncUnaryResultMethodBuilder.cs b/src/MagicOnion.Abstractions/CompilerServices/AsyncUnaryResultMethodBuilder.cs index c48747975..d26fde78b 100644 --- a/src/MagicOnion.Abstractions/CompilerServices/AsyncUnaryResultMethodBuilder.cs +++ b/src/MagicOnion.Abstractions/CompilerServices/AsyncUnaryResultMethodBuilder.cs @@ -1,15 +1,79 @@ -using System; +using System; using System.Runtime.CompilerServices; using System.Security; +using System.Threading.Tasks; +using MessagePack; namespace MagicOnion.CompilerServices { + public struct AsyncUnaryResultMethodBuilder + { + AsyncTaskMethodBuilder methodBuilder; + bool hasResult; + bool useBuilder; + + public static AsyncUnaryResultMethodBuilder Create() + => new AsyncUnaryResultMethodBuilder() { methodBuilder = AsyncTaskMethodBuilder.Create() }; + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => methodBuilder.Start(ref stateMachine); + + public void SetStateMachine(IAsyncStateMachine stateMachine) + => methodBuilder.SetStateMachine(stateMachine); + + public void SetResult() + { + if (useBuilder) + { + methodBuilder.SetResult(Nil.Default); + } + else + { + hasResult = true; + } + } + + public void SetException(Exception ex) + => methodBuilder.SetException(ex); + + public UnaryResult Task + { + get + { + if (hasResult) + { + return new UnaryResult(Nil.Default); + } + + useBuilder = true; + return new UnaryResult(methodBuilder.Task); + } + } + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + useBuilder = true; + methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + { + useBuilder = true; + methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + } + } + public struct AsyncUnaryResultMethodBuilder { - private AsyncTaskMethodBuilder methodBuilder; - private T result; - private bool haveResult; - private bool useBuilder; + AsyncTaskMethodBuilder methodBuilder; + T result; + bool haveResult; + bool useBuilder; public static AsyncUnaryResultMethodBuilder Create() { @@ -77,4 +141,4 @@ public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); } } -} \ No newline at end of file +} diff --git a/src/MagicOnion.Abstractions/UnaryResult.cs b/src/MagicOnion.Abstractions/UnaryResult.cs index 58b1b574c..016ea0585 100644 --- a/src/MagicOnion.Abstractions/UnaryResult.cs +++ b/src/MagicOnion.Abstractions/UnaryResult.cs @@ -11,8 +11,145 @@ namespace MagicOnion /// /// Represents the result of a Unary call that wraps AsyncUnaryCall as Task-like. /// - public static class UnaryResult + [AsyncMethodBuilder(typeof(AsyncUnaryResultMethodBuilder))] + public readonly struct UnaryResult { + internal readonly bool hasRawValue; + internal readonly Task rawTaskValue; + internal readonly Task> response; + + public UnaryResult(Nil nil) + { + this.hasRawValue = true; + this.rawTaskValue = default; + this.response = null; + } + + public UnaryResult(Task rawTaskValue) + { + this.hasRawValue = true; + this.rawTaskValue = rawTaskValue ?? throw new ArgumentNullException(nameof(rawTaskValue)); + this.response = null; + } + + public UnaryResult(Task> response) + { + this.hasRawValue = false; + this.rawTaskValue = default; + this.response = response ?? throw new ArgumentNullException(nameof(response)); + } + + /// + /// Asynchronous call result. + /// + public Task ResponseAsync + { + get + { + if (hasRawValue) + { + if (rawTaskValue != null) + { + return rawTaskValue; + } + } + else + { + // If the UnaryResult has no raw-value and no response, it is the default value of UnaryResult. + // So, we will return the default value of TResponse as Task. + if (response is null) + { + return Task.CompletedTask; + } + + return UnwrapResponse(); + } + + return Task.CompletedTask; + } + } + + /// + /// Asynchronous access to response headers. + /// + public Task ResponseHeadersAsync => UnwrapResponseHeaders(); + + async Task UnwrapResponse() + { + var ctx = await response.ConfigureAwait(false); + await ctx.ResponseAsync.ConfigureAwait(false); + } + + async Task UnwrapResponseHeaders() + { + var ctx = await response.ConfigureAwait(false); + return await ctx.ResponseHeadersAsync.ConfigureAwait(false); + } + + IResponseContext TryUnwrap() + { + if (!response.IsCompleted) + { + throw new InvalidOperationException("UnaryResult request is not yet completed, please await before call this."); + } + + return response.Result; + } + + /// + /// Allows awaiting this object directly. + /// + public TaskAwaiter GetAwaiter() + => ResponseAsync.GetAwaiter(); + + /// + /// Gets the call status if the call has already finished. + /// Throws InvalidOperationException otherwise. + /// + public Status GetStatus() + => TryUnwrap().GetStatus(); + + /// + /// Gets the call trailing metadata if the call has already finished. + /// Throws InvalidOperationException otherwise. + /// + public Metadata GetTrailers() + => TryUnwrap().GetTrailers(); + + /// + /// Provides means to cleanup after the call. + /// If the call has already finished normally (request stream has been completed and call result has been received), doesn't do anything. + /// Otherwise, requests cancellation of the call which should terminate all pending async operations associated with the call. + /// As a result, all resources being used by the call should be released eventually. + /// + /// + /// Normally, there is no need for you to dispose the call unless you want to utilize the + /// "Cancel" semantics of invoking Dispose. + /// + public void Dispose() + { + if (!response.IsCompleted) + { + UnwrapDispose(); + } + else + { + response.Result.Dispose(); + } + } + + async void UnwrapDispose() + { + try + { + var ctx = await response.ConfigureAwait(false); + ctx.Dispose(); + } + catch + { + } + } + /// /// Creates a with the specified result. /// @@ -35,9 +172,7 @@ public static UnaryResult Nil /// /// Represents the result of a Unary call that wraps AsyncUnaryCall as Task-like. /// -#if NON_UNITY || (CSHARP_7_OR_LATER || (UNITY_2018_3_OR_NEWER && (NET_STANDARD_2_0 || NET_4_6))) [AsyncMethodBuilder(typeof(AsyncUnaryResultMethodBuilder<>))] -#endif public readonly struct UnaryResult { internal readonly bool hasRawValue; // internal @@ -102,13 +237,7 @@ public Task ResponseAsync /// /// Asynchronous access to response headers. /// - public Task ResponseHeadersAsync - { - get - { - return UnwrapResponseHeaders(); - } - } + public Task ResponseHeadersAsync => UnwrapResponseHeaders(); async Task UnwrapResponse() { @@ -148,27 +277,21 @@ IResponseContext TryUnwrap() /// Allows awaiting this object directly. /// public TaskAwaiter GetAwaiter() - { - return ResponseAsync.GetAwaiter(); - } + => ResponseAsync.GetAwaiter(); /// /// Gets the call status if the call has already finished. /// Throws InvalidOperationException otherwise. /// public Status GetStatus() - { - return TryUnwrap().GetStatus(); - } + => TryUnwrap().GetStatus(); /// /// Gets the call trailing metadata if the call has already finished. /// Throws InvalidOperationException otherwise. /// public Metadata GetTrailers() - { - return TryUnwrap().GetTrailers(); - } + => TryUnwrap().GetTrailers(); /// /// Provides means to cleanup after the call. @@ -192,4 +315,4 @@ public void Dispose() } } } -} \ No newline at end of file +} diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/CompilerServices/AsyncUnaryResultMethodBuilder.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/CompilerServices/AsyncUnaryResultMethodBuilder.cs index c48747975..d26fde78b 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/CompilerServices/AsyncUnaryResultMethodBuilder.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/CompilerServices/AsyncUnaryResultMethodBuilder.cs @@ -1,15 +1,79 @@ -using System; +using System; using System.Runtime.CompilerServices; using System.Security; +using System.Threading.Tasks; +using MessagePack; namespace MagicOnion.CompilerServices { + public struct AsyncUnaryResultMethodBuilder + { + AsyncTaskMethodBuilder methodBuilder; + bool hasResult; + bool useBuilder; + + public static AsyncUnaryResultMethodBuilder Create() + => new AsyncUnaryResultMethodBuilder() { methodBuilder = AsyncTaskMethodBuilder.Create() }; + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + => methodBuilder.Start(ref stateMachine); + + public void SetStateMachine(IAsyncStateMachine stateMachine) + => methodBuilder.SetStateMachine(stateMachine); + + public void SetResult() + { + if (useBuilder) + { + methodBuilder.SetResult(Nil.Default); + } + else + { + hasResult = true; + } + } + + public void SetException(Exception ex) + => methodBuilder.SetException(ex); + + public UnaryResult Task + { + get + { + if (hasResult) + { + return new UnaryResult(Nil.Default); + } + + useBuilder = true; + return new UnaryResult(methodBuilder.Task); + } + } + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + useBuilder = true; + methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + { + useBuilder = true; + methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + } + } + public struct AsyncUnaryResultMethodBuilder { - private AsyncTaskMethodBuilder methodBuilder; - private T result; - private bool haveResult; - private bool useBuilder; + AsyncTaskMethodBuilder methodBuilder; + T result; + bool haveResult; + bool useBuilder; public static AsyncUnaryResultMethodBuilder Create() { @@ -77,4 +141,4 @@ public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); } } -} \ No newline at end of file +} diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/UnaryResult.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/UnaryResult.cs index 58b1b574c..016ea0585 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/UnaryResult.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/UnaryResult.cs @@ -11,8 +11,145 @@ namespace MagicOnion /// /// Represents the result of a Unary call that wraps AsyncUnaryCall as Task-like. /// - public static class UnaryResult + [AsyncMethodBuilder(typeof(AsyncUnaryResultMethodBuilder))] + public readonly struct UnaryResult { + internal readonly bool hasRawValue; + internal readonly Task rawTaskValue; + internal readonly Task> response; + + public UnaryResult(Nil nil) + { + this.hasRawValue = true; + this.rawTaskValue = default; + this.response = null; + } + + public UnaryResult(Task rawTaskValue) + { + this.hasRawValue = true; + this.rawTaskValue = rawTaskValue ?? throw new ArgumentNullException(nameof(rawTaskValue)); + this.response = null; + } + + public UnaryResult(Task> response) + { + this.hasRawValue = false; + this.rawTaskValue = default; + this.response = response ?? throw new ArgumentNullException(nameof(response)); + } + + /// + /// Asynchronous call result. + /// + public Task ResponseAsync + { + get + { + if (hasRawValue) + { + if (rawTaskValue != null) + { + return rawTaskValue; + } + } + else + { + // If the UnaryResult has no raw-value and no response, it is the default value of UnaryResult. + // So, we will return the default value of TResponse as Task. + if (response is null) + { + return Task.CompletedTask; + } + + return UnwrapResponse(); + } + + return Task.CompletedTask; + } + } + + /// + /// Asynchronous access to response headers. + /// + public Task ResponseHeadersAsync => UnwrapResponseHeaders(); + + async Task UnwrapResponse() + { + var ctx = await response.ConfigureAwait(false); + await ctx.ResponseAsync.ConfigureAwait(false); + } + + async Task UnwrapResponseHeaders() + { + var ctx = await response.ConfigureAwait(false); + return await ctx.ResponseHeadersAsync.ConfigureAwait(false); + } + + IResponseContext TryUnwrap() + { + if (!response.IsCompleted) + { + throw new InvalidOperationException("UnaryResult request is not yet completed, please await before call this."); + } + + return response.Result; + } + + /// + /// Allows awaiting this object directly. + /// + public TaskAwaiter GetAwaiter() + => ResponseAsync.GetAwaiter(); + + /// + /// Gets the call status if the call has already finished. + /// Throws InvalidOperationException otherwise. + /// + public Status GetStatus() + => TryUnwrap().GetStatus(); + + /// + /// Gets the call trailing metadata if the call has already finished. + /// Throws InvalidOperationException otherwise. + /// + public Metadata GetTrailers() + => TryUnwrap().GetTrailers(); + + /// + /// Provides means to cleanup after the call. + /// If the call has already finished normally (request stream has been completed and call result has been received), doesn't do anything. + /// Otherwise, requests cancellation of the call which should terminate all pending async operations associated with the call. + /// As a result, all resources being used by the call should be released eventually. + /// + /// + /// Normally, there is no need for you to dispose the call unless you want to utilize the + /// "Cancel" semantics of invoking Dispose. + /// + public void Dispose() + { + if (!response.IsCompleted) + { + UnwrapDispose(); + } + else + { + response.Result.Dispose(); + } + } + + async void UnwrapDispose() + { + try + { + var ctx = await response.ConfigureAwait(false); + ctx.Dispose(); + } + catch + { + } + } + /// /// Creates a with the specified result. /// @@ -35,9 +172,7 @@ public static UnaryResult Nil /// /// Represents the result of a Unary call that wraps AsyncUnaryCall as Task-like. /// -#if NON_UNITY || (CSHARP_7_OR_LATER || (UNITY_2018_3_OR_NEWER && (NET_STANDARD_2_0 || NET_4_6))) [AsyncMethodBuilder(typeof(AsyncUnaryResultMethodBuilder<>))] -#endif public readonly struct UnaryResult { internal readonly bool hasRawValue; // internal @@ -102,13 +237,7 @@ public Task ResponseAsync /// /// Asynchronous access to response headers. /// - public Task ResponseHeadersAsync - { - get - { - return UnwrapResponseHeaders(); - } - } + public Task ResponseHeadersAsync => UnwrapResponseHeaders(); async Task UnwrapResponse() { @@ -148,27 +277,21 @@ IResponseContext TryUnwrap() /// Allows awaiting this object directly. /// public TaskAwaiter GetAwaiter() - { - return ResponseAsync.GetAwaiter(); - } + => ResponseAsync.GetAwaiter(); /// /// Gets the call status if the call has already finished. /// Throws InvalidOperationException otherwise. /// public Status GetStatus() - { - return TryUnwrap().GetStatus(); - } + => TryUnwrap().GetStatus(); /// /// Gets the call trailing metadata if the call has already finished. /// Throws InvalidOperationException otherwise. /// public Metadata GetTrailers() - { - return TryUnwrap().GetTrailers(); - } + => TryUnwrap().GetTrailers(); /// /// Provides means to cleanup after the call. @@ -192,4 +315,4 @@ public void Dispose() } } } -} \ No newline at end of file +} diff --git a/tests/MagicOnion.Shared.Tests/UnaryResultNonGenericTest.cs b/tests/MagicOnion.Shared.Tests/UnaryResultNonGenericTest.cs new file mode 100644 index 000000000..cebfed2e2 --- /dev/null +++ b/tests/MagicOnion.Shared.Tests/UnaryResultNonGenericTest.cs @@ -0,0 +1,128 @@ +using MagicOnion.Client; + +namespace MagicOnion.Shared.Tests; + +public class UnaryResultNonGenericTest +{ + [Fact] + public async Task Ctor_Default() + { + var result = default(UnaryResult); + await result; + } + + [Fact] + public async Task Ctor_RawValue() + { + var result = new UnaryResult(Nil.Default); + await result; + } + + [Fact] + public async Task Ctor_RawTask() + { + var result = new UnaryResult(Task.FromResult(Nil.Default)); + await result; + } + + [Fact] + public void Ctor_RawTask_Null() + { + // Arrange + var value = default(Task); + // Act + var result = Record.Exception(() => new UnaryResult(value)); + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task Ctor_TaskOfResponseContext() + { + var result = new UnaryResult(Task.FromResult(DummyResponseContext.Create(Nil.Default))); + await result; + } + + [Fact] + public void Ctor_TaskOfResponseContext_Null() + { + // Arrange + var value = Task.FromResult(DummyResponseContext.Create(Nil.Default)); + // Act + var result = Record.Exception(() => new UnaryResult(value)); + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task ResponseHeadersAsync_Ctor_ResponseContext() + { + var result = new UnaryResult(Task.FromResult>(new DummyResponseContext(Nil.Default))); + (await result.ResponseHeadersAsync).Should().Contain(x => x.Key == "x-foo-bar" && x.Value == "baz"); + } + + [Fact] + public async Task ResponseHeadersAsync_Never_Ctor_ResponseContext() + { + var result = new UnaryResult(Task.FromResult>(new DummyResponseContext(new TaskCompletionSource()))); + await Assert.ThrowsAsync(async () => await result.ResponseHeadersAsync.WaitAsync(TimeSpan.FromMilliseconds(250))); + } + + [Fact] + public async Task ResponseAsync_Ctor_Task() + { + var result = new UnaryResult(Task.FromResult(Nil.Default)); + await result.ResponseAsync; + } + + [Fact] + public async Task ResponseAsync_Ctor_ResponseContext() + { + var result = new UnaryResult(Task.FromResult>(new DummyResponseContext(Nil.Default))); + await result.ResponseAsync; + } + + [Fact] + public async Task ResponseAsync_Never_Ctor_Task() + { + var result = new UnaryResult(new TaskCompletionSource().Task); + await Assert.ThrowsAsync(async () => await result.ResponseAsync.WaitAsync(TimeSpan.FromMilliseconds(250))); + } + + [Fact] + public async Task ResponseAsync_Never_Ctor_ResponseContext() + { + var result = new UnaryResult(Task.FromResult>(new DummyResponseContext(new TaskCompletionSource()))); + await Assert.ThrowsAsync(async () => await result.ResponseAsync.WaitAsync(TimeSpan.FromMilliseconds(250))); + } + + static class DummyResponseContext + { + public static IResponseContext Create(T value) + => new DummyResponseContext(value); + } + + class DummyResponseContext : IResponseContext + { + readonly Task task; + + public DummyResponseContext(T value) + { + this.task = Task.FromResult(value); + } + public DummyResponseContext(TaskCompletionSource taskCompletionSource) + { + this.task = taskCompletionSource.Task; + } + + public Task ResponseHeadersAsync => task.ContinueWith(_ => new Metadata() { {"x-foo-bar", "baz"} }); + public Status GetStatus() => Status.DefaultSuccess; + public Metadata GetTrailers() => Metadata.Empty; + + public Type ResponseType => typeof(T); + public Task ResponseAsync => task; + + public void Dispose() { } + + } +} diff --git a/tests/MagicOnion.Shared.Tests/UnaryResultTest.cs b/tests/MagicOnion.Shared.Tests/UnaryResultTest.cs index acb8f5df3..bb0c3e177 100644 --- a/tests/MagicOnion.Shared.Tests/UnaryResultTest.cs +++ b/tests/MagicOnion.Shared.Tests/UnaryResultTest.cs @@ -85,4 +85,4 @@ public async Task Ctor_Default() var result2 = default(UnaryResult); (await result2).Should().Be(null); } -} \ No newline at end of file +} From 6e30a775a068958e019e7fc8f6dfdb9c05a03dd1 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Tue, 13 Dec 2022 18:53:57 +0900 Subject: [PATCH 2/2] Make to handle Non-Generic UnaryResult --- .../Scripts/Generated/MagicOnion.Generated.cs | 30 ++++++++ src/MagicOnion.Abstractions/UnaryResult.cs | 5 ++ .../MagicOnion.Abstractions/UnaryResult.cs | 5 ++ .../DynamicClient/DynamicClientBuilder.cs | 5 +- .../DynamicClient/ServiceClientDefinition.cs | 21 ++++-- .../Internal/MagicOnionMethodInvoker.cs | 9 ++- .../DynamicClient/DynamicClientBuilder.cs | 5 +- .../DynamicClient/ServiceClientDefinition.cs | 21 ++++-- .../Internal/MagicOnionMethodInvoker.cs | 9 ++- .../CodeAnalysis/MagicOnionTypeInfo.cs | 2 + .../CodeAnalysis/MethodCollector.cs | 4 ++ .../StaticMagicOnionClientGenerator.cs | 5 +- .../Utils/PseudoCompilation.cs | 47 ++++++++++-- .../Internal/MethodHandlerMetadata.cs | 27 ++++--- src/MagicOnion.Server/MethodHandler.cs | 18 ++++- .../ServiceClientDefinitionTest.cs | 54 +++++++++++++- .../Collector/MethodCollectorServicesTest.cs | 33 +++++++++ .../GenerateServiceTest.cs | 33 +++++++++ .../GenerateTest.cs | 32 ++++----- .../MagicOnionGeneratorTestOutputLogger.cs | 2 +- .../UnaryServiceTest.cs | 71 +++++++++++++++++++ .../_GeneratedClient.cs | 45 ++++++++++++ .../UnaryServiceTest.cs | 27 +++++++ .../UnaryResultNonGenericTest.cs | 2 +- .../Services/BasicUnaryService.cs | 15 +++- 25 files changed, 480 insertions(+), 47 deletions(-) create mode 100644 tests/MagicOnion.Integration.Tests/UnaryServiceTest.cs diff --git a/samples/ChatApp/ChatApp.Unity/Assets/Scripts/Generated/MagicOnion.Generated.cs b/samples/ChatApp/ChatApp.Unity/Assets/Scripts/Generated/MagicOnion.Generated.cs index 0285ee30d..c49731770 100644 --- a/samples/ChatApp/ChatApp.Unity/Assets/Scripts/Generated/MagicOnion.Generated.cs +++ b/samples/ChatApp/ChatApp.Unity/Assets/Scripts/Generated/MagicOnion.Generated.cs @@ -5,6 +5,12 @@ #pragma warning disable 219 #pragma warning disable 168 +// NOTE: Disable warnings for nullable reference types. +// `#nullable disable` causes compile error on old C# compilers (-7.3) +#pragma warning disable 8603 // Possible null reference return. +#pragma warning disable 8618 // Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable. +#pragma warning disable 8625 // Cannot convert null literal to non-nullable reference type. + namespace MagicOnion { using global::System; @@ -17,7 +23,11 @@ public static partial class MagicOnionInitializer { static bool isRegistered = false; +#if UNITY_2019_4_OR_NEWER [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] +#elif NET5_0_OR_GREATER + [System.Runtime.CompilerServices.ModuleInitializer] +#endif public static void Register() { if(isRegistered) return; @@ -35,12 +45,19 @@ public static void Register() #pragma warning restore 414 #pragma warning restore 612 #pragma warning restore 618 + #pragma warning disable 618 #pragma warning disable 612 #pragma warning disable 414 #pragma warning disable 219 #pragma warning disable 168 +// NOTE: Disable warnings for nullable reference types. +// `#nullable disable` causes compile error on old C# compilers (-7.3) +#pragma warning disable 8603 // Possible null reference return. +#pragma warning disable 8618 // Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable. +#pragma warning disable 8625 // Cannot convert null literal to non-nullable reference type. + namespace MagicOnion.Resolvers { using System; @@ -113,6 +130,7 @@ internal static object GetFormatter(Type t) #pragma warning restore 414 #pragma warning restore 612 #pragma warning restore 618 + /// #pragma warning disable 618 #pragma warning disable 612 @@ -120,6 +138,12 @@ internal static object GetFormatter(Type t) #pragma warning disable 219 #pragma warning disable 168 +// NOTE: Disable warnings for nullable reference types. +// `#nullable disable` causes compile error on old C# compilers (-7.3) +#pragma warning disable 8603 // Possible null reference return. +#pragma warning disable 8618 // Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable. +#pragma warning disable 8625 // Cannot convert null literal to non-nullable reference type. + namespace ChatApp.Shared.Services { using global::System; @@ -172,6 +196,12 @@ private ChatServiceClient(MagicOnionClientOptions options, ClientCore core) : ba #pragma warning disable 219 #pragma warning disable 168 +// NOTE: Disable warnings for nullable reference types. +// `#nullable disable` causes compile error on old C# compilers (-7.3) +#pragma warning disable 8603 // Possible null reference return. +#pragma warning disable 8618 // Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable. +#pragma warning disable 8625 // Cannot convert null literal to non-nullable reference type. + namespace ChatApp.Shared.Hubs { using global::System; diff --git a/src/MagicOnion.Abstractions/UnaryResult.cs b/src/MagicOnion.Abstractions/UnaryResult.cs index 016ea0585..f2d0fb14c 100644 --- a/src/MagicOnion.Abstractions/UnaryResult.cs +++ b/src/MagicOnion.Abstractions/UnaryResult.cs @@ -150,6 +150,11 @@ async void UnwrapDispose() } } + /// + /// Gets a completed with the void result. + /// + public static UnaryResult CompletedResult => default; + /// /// Creates a with the specified result. /// diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/UnaryResult.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/UnaryResult.cs index 016ea0585..f2d0fb14c 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/UnaryResult.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Abstractions/UnaryResult.cs @@ -150,6 +150,11 @@ async void UnwrapDispose() } } + /// + /// Gets a completed with the void result. + /// + public static UnaryResult CompletedResult => default; + /// /// Creates a with the specified result. /// diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicClientBuilder.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicClientBuilder.cs index 4ed2ff9f4..6af115431 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicClientBuilder.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicClientBuilder.cs @@ -135,6 +135,8 @@ private static void EmitServiceMethods(ServiceClientBuildContext ctx) // => this.core.MethodName.InvokeUnary(this, "ServiceName/MethodName", request); // public UnaryResult MethodName() // => this.core.MethodName.InvokeUnary(this, "ServiceName/MethodName", Nil.Default); + // public UnaryResult MethodName() + // => this.core.MethodName.InvokeUnaryNonGeneric(this, "ServiceName/MethodName", Nil.Default); // public Task> MethodName(TArg1 arg1, TArg2 arg2, ...) // => this.core.MethodName.InvokeServerStreaming(this, "ServiceName/MethodName", new DynamicArgumentTuple(arg1, arg2, ...)); // public Task> MethodName() @@ -143,7 +145,8 @@ private static void EmitServiceMethods(ServiceClientBuildContext ctx) // => this.core.MethodName.InvokeDuplexStreaming(this, "ServiceName/MethodName"); foreach (var method in ctx.Definition.Methods) { - var methodInvokerInvokeMethod = ctx.FieldAndMethodInvokerTypeByMethod[method.MethodName].MethodInvokerType.GetMethod($"Invoke{method.MethodType}"); + var hasNonGenericUnaryResult = method.MethodReturnType == typeof(UnaryResult); + var methodInvokerInvokeMethod = ctx.FieldAndMethodInvokerTypeByMethod[method.MethodName].MethodInvokerType.GetMethod($"Invoke{method.MethodType}{(hasNonGenericUnaryResult ? "NonGeneric" : "")}"); var methodBuilder = ctx.ServiceClientType.DefineMethod(method.MethodName, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual, methodInvokerInvokeMethod.ReturnType, method.ParameterTypes.ToArray()); var il = methodBuilder.GetILGenerator(); diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/ServiceClientDefinition.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/ServiceClientDefinition.cs index c8f7dd447..ee925532c 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/ServiceClientDefinition.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/ServiceClientDefinition.cs @@ -74,7 +74,8 @@ private void Verify() switch (MethodType) { case MethodType.Unary: - if (!MethodReturnType.IsGenericType || MethodReturnType.GetGenericTypeDefinition() != typeof(UnaryResult<>)) + if ((MethodReturnType != typeof(UnaryResult)) && + (MethodReturnType.IsGenericType && MethodReturnType.GetGenericTypeDefinition() != typeof(UnaryResult<>))) { throw new InvalidOperationException($"The return type of Unary method must be UnaryResult. (Service: {ServiceName}, Method: {MethodName})"); } @@ -96,6 +97,10 @@ private static (MethodType MethodType, Type RequestType, Type ResponseType) GetM "The method of a service must return 'UnaryResult', 'Task>', 'Task>' or 'DuplexStreamingResult'."; var returnType = methodInfo.ReturnType; + if (returnType == typeof(UnaryResult)) + { + return (MethodType.Unary, null, typeof(Nil)); + } if (!returnType.IsGenericType) { throw new InvalidOperationException($"{UnsupportedReturnTypeErrorMessage} (Method: {methodInfo.DeclaringType.Name}.{methodInfo.Name})"); @@ -114,11 +119,19 @@ private static (MethodType MethodType, Type RequestType, Type ResponseType) GetM returnTypeOpen = returnType.GetGenericTypeDefinition(); } - if (returnTypeOpen == typeof(UnaryResult<>)) + if (returnTypeOpen == typeof(UnaryResult)) + { + if (isTaskOfT) + { + throw new InvalidOperationException($"The return type of an Unary method must be 'UnaryResult' or 'UnaryResult'. (Method: {methodInfo.DeclaringType.Name}.{methodInfo.Name})"); + } + return (MethodType.Unary, null, typeof(Nil)); + } + else if (returnTypeOpen == typeof(UnaryResult<>)) { if (isTaskOfT) { - throw new InvalidOperationException($"The return type of an Unary method must be 'UnaryResult'. (Method: {methodInfo.DeclaringType.Name}.{methodInfo.Name})"); + throw new InvalidOperationException($"The return type of an Unary method must be 'UnaryResult' or 'UnaryResult'. (Method: {methodInfo.DeclaringType.Name}.{methodInfo.Name})"); } return (MethodType.Unary, null, returnType.GetGenericArguments()[0]); } @@ -221,4 +234,4 @@ private static Type GetRequestTypeFromMethod(MethodInfo methodInfo) } } } -} \ No newline at end of file +} diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal/MagicOnionMethodInvoker.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal/MagicOnionMethodInvoker.cs index 2b58c42f7..979518ae3 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal/MagicOnionMethodInvoker.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal/MagicOnionMethodInvoker.cs @@ -33,6 +33,7 @@ public static RawMethodInvoker Create_ValueType_ValueType { public abstract UnaryResult InvokeUnary(MagicOnionClientBase client, string path, TRequest request); + public abstract UnaryResult InvokeUnaryNonGeneric(MagicOnionClientBase client, string path, TRequest request); public abstract Task> InvokeServerStreaming(MagicOnionClientBase client, string path, TRequest request); public abstract Task> InvokeClientStreaming(MagicOnionClientBase client, string path); public abstract Task> InvokeDuplexStreaming(MagicOnionClientBase client, string path); @@ -56,9 +57,15 @@ public override UnaryResult InvokeUnary(MagicOnionClientBase client, { var future = InvokeUnaryCore(client, path, request, createUnaryResponseContext); return new UnaryResult(future); + } + public override UnaryResult InvokeUnaryNonGeneric(MagicOnionClientBase client, string path, TRequest request) + { + var future = (Task>)(object)InvokeUnaryCore(client, path, request, createUnaryResponseContext); + return new UnaryResult(future); } - private async Task> InvokeUnaryCore(MagicOnionClientBase client, string path, TRequest request, Func requestMethod) + + async Task> InvokeUnaryCore(MagicOnionClientBase client, string path, TRequest request, Func requestMethod) { var requestContext = new RequestContext(request, client, path, client.Options.CallOptions, typeof(TResponse), client.Options.Filters, requestMethod); var response = await InterceptInvokeHelper.InvokeWithFilter(requestContext); diff --git a/src/MagicOnion.Client/DynamicClient/DynamicClientBuilder.cs b/src/MagicOnion.Client/DynamicClient/DynamicClientBuilder.cs index 4ed2ff9f4..6af115431 100644 --- a/src/MagicOnion.Client/DynamicClient/DynamicClientBuilder.cs +++ b/src/MagicOnion.Client/DynamicClient/DynamicClientBuilder.cs @@ -135,6 +135,8 @@ private static void EmitServiceMethods(ServiceClientBuildContext ctx) // => this.core.MethodName.InvokeUnary(this, "ServiceName/MethodName", request); // public UnaryResult MethodName() // => this.core.MethodName.InvokeUnary(this, "ServiceName/MethodName", Nil.Default); + // public UnaryResult MethodName() + // => this.core.MethodName.InvokeUnaryNonGeneric(this, "ServiceName/MethodName", Nil.Default); // public Task> MethodName(TArg1 arg1, TArg2 arg2, ...) // => this.core.MethodName.InvokeServerStreaming(this, "ServiceName/MethodName", new DynamicArgumentTuple(arg1, arg2, ...)); // public Task> MethodName() @@ -143,7 +145,8 @@ private static void EmitServiceMethods(ServiceClientBuildContext ctx) // => this.core.MethodName.InvokeDuplexStreaming(this, "ServiceName/MethodName"); foreach (var method in ctx.Definition.Methods) { - var methodInvokerInvokeMethod = ctx.FieldAndMethodInvokerTypeByMethod[method.MethodName].MethodInvokerType.GetMethod($"Invoke{method.MethodType}"); + var hasNonGenericUnaryResult = method.MethodReturnType == typeof(UnaryResult); + var methodInvokerInvokeMethod = ctx.FieldAndMethodInvokerTypeByMethod[method.MethodName].MethodInvokerType.GetMethod($"Invoke{method.MethodType}{(hasNonGenericUnaryResult ? "NonGeneric" : "")}"); var methodBuilder = ctx.ServiceClientType.DefineMethod(method.MethodName, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual, methodInvokerInvokeMethod.ReturnType, method.ParameterTypes.ToArray()); var il = methodBuilder.GetILGenerator(); diff --git a/src/MagicOnion.Client/DynamicClient/ServiceClientDefinition.cs b/src/MagicOnion.Client/DynamicClient/ServiceClientDefinition.cs index c8f7dd447..ee925532c 100644 --- a/src/MagicOnion.Client/DynamicClient/ServiceClientDefinition.cs +++ b/src/MagicOnion.Client/DynamicClient/ServiceClientDefinition.cs @@ -74,7 +74,8 @@ private void Verify() switch (MethodType) { case MethodType.Unary: - if (!MethodReturnType.IsGenericType || MethodReturnType.GetGenericTypeDefinition() != typeof(UnaryResult<>)) + if ((MethodReturnType != typeof(UnaryResult)) && + (MethodReturnType.IsGenericType && MethodReturnType.GetGenericTypeDefinition() != typeof(UnaryResult<>))) { throw new InvalidOperationException($"The return type of Unary method must be UnaryResult. (Service: {ServiceName}, Method: {MethodName})"); } @@ -96,6 +97,10 @@ private static (MethodType MethodType, Type RequestType, Type ResponseType) GetM "The method of a service must return 'UnaryResult', 'Task>', 'Task>' or 'DuplexStreamingResult'."; var returnType = methodInfo.ReturnType; + if (returnType == typeof(UnaryResult)) + { + return (MethodType.Unary, null, typeof(Nil)); + } if (!returnType.IsGenericType) { throw new InvalidOperationException($"{UnsupportedReturnTypeErrorMessage} (Method: {methodInfo.DeclaringType.Name}.{methodInfo.Name})"); @@ -114,11 +119,19 @@ private static (MethodType MethodType, Type RequestType, Type ResponseType) GetM returnTypeOpen = returnType.GetGenericTypeDefinition(); } - if (returnTypeOpen == typeof(UnaryResult<>)) + if (returnTypeOpen == typeof(UnaryResult)) + { + if (isTaskOfT) + { + throw new InvalidOperationException($"The return type of an Unary method must be 'UnaryResult' or 'UnaryResult'. (Method: {methodInfo.DeclaringType.Name}.{methodInfo.Name})"); + } + return (MethodType.Unary, null, typeof(Nil)); + } + else if (returnTypeOpen == typeof(UnaryResult<>)) { if (isTaskOfT) { - throw new InvalidOperationException($"The return type of an Unary method must be 'UnaryResult'. (Method: {methodInfo.DeclaringType.Name}.{methodInfo.Name})"); + throw new InvalidOperationException($"The return type of an Unary method must be 'UnaryResult' or 'UnaryResult'. (Method: {methodInfo.DeclaringType.Name}.{methodInfo.Name})"); } return (MethodType.Unary, null, returnType.GetGenericArguments()[0]); } @@ -221,4 +234,4 @@ private static Type GetRequestTypeFromMethod(MethodInfo methodInfo) } } } -} \ No newline at end of file +} diff --git a/src/MagicOnion.Client/Internal/MagicOnionMethodInvoker.cs b/src/MagicOnion.Client/Internal/MagicOnionMethodInvoker.cs index 2b58c42f7..979518ae3 100644 --- a/src/MagicOnion.Client/Internal/MagicOnionMethodInvoker.cs +++ b/src/MagicOnion.Client/Internal/MagicOnionMethodInvoker.cs @@ -33,6 +33,7 @@ public static RawMethodInvoker Create_ValueType_ValueType { public abstract UnaryResult InvokeUnary(MagicOnionClientBase client, string path, TRequest request); + public abstract UnaryResult InvokeUnaryNonGeneric(MagicOnionClientBase client, string path, TRequest request); public abstract Task> InvokeServerStreaming(MagicOnionClientBase client, string path, TRequest request); public abstract Task> InvokeClientStreaming(MagicOnionClientBase client, string path); public abstract Task> InvokeDuplexStreaming(MagicOnionClientBase client, string path); @@ -56,9 +57,15 @@ public override UnaryResult InvokeUnary(MagicOnionClientBase client, { var future = InvokeUnaryCore(client, path, request, createUnaryResponseContext); return new UnaryResult(future); + } + public override UnaryResult InvokeUnaryNonGeneric(MagicOnionClientBase client, string path, TRequest request) + { + var future = (Task>)(object)InvokeUnaryCore(client, path, request, createUnaryResponseContext); + return new UnaryResult(future); } - private async Task> InvokeUnaryCore(MagicOnionClientBase client, string path, TRequest request, Func requestMethod) + + async Task> InvokeUnaryCore(MagicOnionClientBase client, string path, TRequest request, Func requestMethod) { var requestContext = new RequestContext(request, client, path, client.Options.CallOptions, typeof(TResponse), client.Options.Filters, requestMethod); var response = await InterceptInvokeHelper.InvokeWithFilter(requestContext); diff --git a/src/MagicOnion.GeneratorCore/CodeAnalysis/MagicOnionTypeInfo.cs b/src/MagicOnion.GeneratorCore/CodeAnalysis/MagicOnionTypeInfo.cs index c951d6886..c1d6ca445 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 MagicOnion_UnaryResult { get; } = new MagicOnionTypeInfo("MagicOnion", "UnaryResult", SubType.ValueType); // ReSharper restore InconsistentNaming } @@ -119,6 +120,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 == "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 f1889a356..cc161400f 100644 --- a/src/MagicOnion.GeneratorCore/CodeAnalysis/MethodCollector.cs +++ b/src/MagicOnion.GeneratorCore/CodeAnalysis/MethodCollector.cs @@ -177,6 +177,10 @@ MagicOnionServiceInfo.MagicOnionServiceMethodInfo CreateServiceMethodInfoFromMet var responseType = MagicOnionTypeInfo.KnownTypes.System_Void; switch (methodReturnType.FullNameOpenType) { + case "global::MagicOnion.UnaryResult": + methodType = MethodType.Unary; + responseType = MagicOnionTypeInfo.KnownTypes.MessagePack_Nil; + break; case "global::MagicOnion.UnaryResult<>": methodType = MethodType.Unary; responseType = methodReturnType.GenericArguments[0]; diff --git a/src/MagicOnion.GeneratorCore/CodeGen/StaticMagicOnionClientGenerator.cs b/src/MagicOnion.GeneratorCore/CodeGen/StaticMagicOnionClientGenerator.cs index d7d862748..4ccd20f72 100644 --- a/src/MagicOnion.GeneratorCore/CodeGen/StaticMagicOnionClientGenerator.cs +++ b/src/MagicOnion.GeneratorCore/CodeGen/StaticMagicOnionClientGenerator.cs @@ -153,6 +153,8 @@ static void EmitServiceMethods(ServiceClientBuildContext ctx) // => this.core.MethodName.InvokeUnary(this, "ServiceName/MethodName", request); // public UnaryResult MethodName() // => this.core.MethodName.InvokeUnary(this, "ServiceName/MethodName", Nil.Default); + // public UnaryResult MethodName() + // => this.core.MethodName.InvokeUnaryNonGeneric(this, "ServiceName/MethodName", Nil.Default); // public Task> MethodName(TArg1 arg1, TArg2 arg2, ...) // => this.core.MethodName.InvokeServerStreaming(this, "ServiceName/MethodName", new DynamicArgumentTuple(arg1, arg2, ...)); // public Task> MethodName() @@ -174,10 +176,11 @@ _ when (method.MethodType != MethodType.Unary && method.MethodType != MethodType // new DynamicArgumentTuple(arg1, arg2, ...) _ => $", {method.Parameters.ToNewDynamicArgumentTuple()}", }; + var hasNonGenericUnaryResult = method.MethodReturnType == MagicOnionTypeInfo.KnownTypes.MagicOnion_UnaryResult; ctx.TextWriter.WriteLines($""" public {method.MethodReturnType.FullName} {method.MethodName}({method.Parameters.ToMethodSignaturize()}) - => this.core.{method.MethodName}.Invoke{method.MethodType}(this, "{method.Path}"{invokeRequestParameters}); + => this.core.{method.MethodName}.Invoke{method.MethodType}{(hasNonGenericUnaryResult ? "NonGeneric" : "")}(this, "{method.Path}"{invokeRequestParameters}); """); } // #endif } diff --git a/src/MagicOnion.GeneratorCore/Utils/PseudoCompilation.cs b/src/MagicOnion.GeneratorCore/Utils/PseudoCompilation.cs index ef92fb6f5..ceb93fdb3 100644 --- a/src/MagicOnion.GeneratorCore/Utils/PseudoCompilation.cs +++ b/src/MagicOnion.GeneratorCore/Utils/PseudoCompilation.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Runtime.Serialization; using System.Text; using System.Xml; @@ -39,14 +40,49 @@ public static Task CreateFromProjectAsync(string[] csprojs, s // ignore Unity's default metadatas(to avoid conflict .NET Core runtime import) // MonoBleedingEdge = .NET 4.x Unity metadata // 2.0.0 = .NET Standard 2.0 Unity metadata - var metadata = new List(); - var targetMetadataLocations = locations.Select(Path.GetFullPath).Concat(GetStandardReferences()).Distinct().Where(x => !(x.Contains("MonoBleedingEdge") || x.Contains("2.0.0"))); + var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + var targetMetadataLocations = locations.Select(Path.GetFullPath) + .Concat(GetStandardReferences()) + .Distinct() + .Where(x => !(x.Contains("MonoBleedingEdge") || x.Contains("2.0.0"))) + // .NET (Core) SDK libraries are prioritized. + .OrderBy(x => x.Contains(".NETCore") ? 0 : 1) + // We expect the NuGet package path to contain the version. :) + .ThenByDescending(x => x); + + var netCoreBundledAssemblies = new[] + { + "Microsoft.Bcl.AsyncInterfaces.dll", + "System.Buffers.dll", + "System.Memory.dll", + "System.Runtime.CompilerServices.Unsafe.dll", + "System.Threading.Tasks.Extensions.dll", + }; + + var hasNetCoreLibrariesLoaded = false; foreach (var item in targetMetadataLocations) { if (File.Exists(item)) { - logger.Trace($"[{nameof(PseudoCompilation)}] Loading Metadata '{item}'"); - metadata.Add(MetadataReference.CreateFromFile(item)); + var assemblyFileName = Path.GetFileName(item); + if (metadata.ContainsKey(assemblyFileName)) + { + logger.Trace($"[{nameof(PseudoCompilation)}] Loading Metadata '{item}' (Skipped. Different version of the library is already loaded.)"); + } + else if (hasNetCoreLibrariesLoaded && netCoreBundledAssemblies.Contains(assemblyFileName, StringComparer.OrdinalIgnoreCase)) + { + logger.Trace($"[{nameof(PseudoCompilation)}] Loading Metadata '{item}' (Skipped. Use .NET Runtime assembly instead.)"); + } + else + { + logger.Trace($"[{nameof(PseudoCompilation)}] Loading Metadata '{item}'."); + metadata.Add(assemblyFileName, MetadataReference.CreateFromFile(item)); + + if (item.Contains("Microsoft.NETCore.App")) + { + hasNetCoreLibrariesLoaded = true; + } + } } else { @@ -54,10 +90,11 @@ public static Task CreateFromProjectAsync(string[] csprojs, s } } + var compilation = CSharpCompilation.Create( "CodeGenTemp", syntaxTrees, - metadata, + metadata.Values, new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true, diff --git a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs index 5f0297636..25279e85d 100644 --- a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs +++ b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs @@ -129,10 +129,23 @@ static MethodInfo ResolveInterfaceMethod(Type targetType, Type interfaceType, st static Type UnwrapUnaryResponseType(MethodInfo methodInfo, out MethodType methodType, out bool responseIsTask, out Type? requestTypeIfExists) { var t = methodInfo.ReturnType; - if (!t.GetTypeInfo().IsGenericType) throw new InvalidOperationException($"The method '{methodInfo.Name}' has invalid return type. (Member:{methodInfo.DeclaringType!.Name}.{methodInfo.Name}, ReturnType:{methodInfo.ReturnType.Name})"); + + // UnaryResult + if (t == typeof(UnaryResult)) + { + methodType = MethodType.Unary; + requestTypeIfExists = default; + responseIsTask = false; + return typeof(MessagePack.Nil); + } + + if (!t.GetTypeInfo().IsGenericType) + { + throw new InvalidOperationException($"The method '{methodInfo.Name}' has invalid return type. (Member:{methodInfo.DeclaringType!.Name}.{methodInfo.Name}, ReturnType:{methodInfo.ReturnType.Name})"); + } - // Task> - if (t.GetGenericTypeDefinition() == typeof(Task<>)) + // Task> + if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Task<>)) { responseIsTask = true; t = t.GetGenericArguments()[0]; @@ -142,7 +155,7 @@ static Type UnwrapUnaryResponseType(MethodInfo methodInfo, out MethodType method responseIsTask = false; } - // Unary + // UnaryResult, ClientStreamingResult, ServerStreamingResult, DuplexStreamingResult var returnType = t.GetGenericTypeDefinition(); if (returnType == typeof(UnaryResult<>)) { @@ -170,10 +183,8 @@ static Type UnwrapUnaryResponseType(MethodInfo methodInfo, out MethodType method requestTypeIfExists = genArgs[0]; return genArgs[1]; } - else - { - throw new InvalidOperationException($"The method '{methodInfo.Name}' has invalid return type. path:{methodInfo.DeclaringType!.Name + "/" + methodInfo.Name} type:{methodInfo.ReturnType.Name}"); - } + + 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) diff --git a/src/MagicOnion.Server/MethodHandler.cs b/src/MagicOnion.Server/MethodHandler.cs index 54db64db5..924231e5f 100644 --- a/src/MagicOnion.Server/MethodHandler.cs +++ b/src/MagicOnion.Server/MethodHandler.cs @@ -20,6 +20,7 @@ public class MethodHandler : IEquatable 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)!; @@ -115,7 +116,9 @@ public MethodHandler(Type classType, MethodInfo methodInfo, string methodName, M { var finalMethod = (metadata.IsResultTypeTask) ? Helper_SetTaskUnaryResult.MakeGenericMethod(UnwrappedResponseType) - : Helper_SetUnaryResult.MakeGenericMethod(UnwrappedResponseType); + : metadata.ServiceMethod.ReturnType == typeof(UnaryResult) + ? Helper_SetUnaryResultNonGeneric + : Helper_SetUnaryResult.MakeGenericMethod(UnwrappedResponseType); callBody = Expression.Call(finalMethod, callBody, contextArg); } @@ -524,6 +527,7 @@ public MethodHandlerOptions(MagicOnionOptions options) internal class MethodHandlerResultHelper { static readonly ValueTask CopmletedValueTask = new ValueTask(); + static readonly object BoxedNil = Nil.Default; public static ValueTask NewEmptyValueTask(T result) { @@ -537,6 +541,18 @@ public static async ValueTask TaskToEmptyValueTask(Task 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) diff --git a/tests/MagicOnion.Client.Tests/DynamicClient/ServiceClientDefinitionTest.cs b/tests/MagicOnion.Client.Tests/DynamicClient/ServiceClientDefinitionTest.cs index ea4a83706..4a41d7a34 100644 --- a/tests/MagicOnion.Client.Tests/DynamicClient/ServiceClientDefinitionTest.cs +++ b/tests/MagicOnion.Client.Tests/DynamicClient/ServiceClientDefinitionTest.cs @@ -6,6 +6,12 @@ public class ServiceClientDefinitionTest { public interface IDummyService { + UnaryResult Unary(); + UnaryResult UnaryNonGeneric(); + Task> ClientStreaming(); + Task> ServerStreaming(); + Task> DuplexStreaming(); + Task> TaskOfUnary(); ClientStreamingResult NonTaskOfClientStreamingResult(); ServerStreamingResult NonTaskOfServerStreamingResult(); @@ -13,6 +19,52 @@ public interface IDummyService void Unknown(); } + [Fact] + public void Unary() + { + var methodInfo = ServiceClientDefinition.MagicOnionServiceMethodInfo.Create(typeof(IDummyService), typeof(IDummyService).GetMethod(nameof(IDummyService.Unary))); + methodInfo.Should().NotBeNull(); + methodInfo.MethodName.Should().Be(nameof(IDummyService.Unary)); + methodInfo.MethodReturnType.Should().Be>(); + methodInfo.ResponseType.Should().Be(); + } + + [Fact] + public void Unary_NonGeneric() + { + var methodInfo = ServiceClientDefinition.MagicOnionServiceMethodInfo.Create(typeof(IDummyService), typeof(IDummyService).GetMethod(nameof(IDummyService.UnaryNonGeneric))); + methodInfo.Should().NotBeNull(); + methodInfo.MethodName.Should().Be(nameof(IDummyService.UnaryNonGeneric)); + methodInfo.MethodReturnType.Should().Be(); + methodInfo.ResponseType.Should().Be(); + } + + [Fact] + public void ClientStreaming() + { + var methodInfo = ServiceClientDefinition.MagicOnionServiceMethodInfo.Create(typeof(IDummyService), typeof(IDummyService).GetMethod(nameof(IDummyService.ClientStreaming))); + methodInfo.Should().NotBeNull(); + methodInfo.MethodName.Should().Be(nameof(IDummyService.ClientStreaming)); + methodInfo.MethodReturnType.Should().Be>>(); + } + + [Fact] + public void ServerStreaming() + { + var methodInfo = ServiceClientDefinition.MagicOnionServiceMethodInfo.Create(typeof(IDummyService), typeof(IDummyService).GetMethod(nameof(IDummyService.ServerStreaming))); + methodInfo.Should().NotBeNull(); + methodInfo.MethodName.Should().Be(nameof(IDummyService.ServerStreaming)); + methodInfo.MethodReturnType.Should().Be>>(); + } + + [Fact] + public void DuplexStreaming() + { + var methodInfo = ServiceClientDefinition.MagicOnionServiceMethodInfo.Create(typeof(IDummyService), typeof(IDummyService).GetMethod(nameof(IDummyService.DuplexStreaming))); + methodInfo.Should().NotBeNull(); + methodInfo.MethodName.Should().Be(nameof(IDummyService.DuplexStreaming)); + methodInfo.MethodReturnType.Should().Be>>(); + } [Fact] public void InvalidUnaryTaskOfUnary() { @@ -21,7 +73,7 @@ public void InvalidUnaryTaskOfUnary() ServiceClientDefinition.MagicOnionServiceMethodInfo.Create(typeof(IDummyService), typeof(IDummyService).GetMethod(nameof(IDummyService.TaskOfUnary))); }); - ex.Message.Should().Contain("The return type of an Unary method must be 'UnaryResult'"); + ex.Message.Should().Contain("The return type of an Unary method must be 'UnaryResult' or 'UnaryResult'"); } [Fact] diff --git a/tests/MagicOnion.Generator.Tests/Collector/MethodCollectorServicesTest.cs b/tests/MagicOnion.Generator.Tests/Collector/MethodCollectorServicesTest.cs index 81d024b0d..455230778 100644 --- a/tests/MagicOnion.Generator.Tests/Collector/MethodCollectorServicesTest.cs +++ b/tests/MagicOnion.Generator.Tests/Collector/MethodCollectorServicesTest.cs @@ -180,6 +180,39 @@ public interface IMyService : IService serviceCollection.Hubs.Should().BeEmpty(); serviceCollection.Services.Should().BeEmpty(); } + + [Fact] + public void Unary_NonGenericResult() + { + // Arrange + var source = @" +using System; +using System.Threading.Tasks; +using MagicOnion; +using MessagePack; + +namespace MyNamespace +{ + public interface IMyService : IService + { + UnaryResult MethodA(); + } +} +"; + using var tempWorkspace = TemporaryProjectWorkarea.Create(); + tempWorkspace.AddFileToProject("IMyService.cs", source); + var compilation = tempWorkspace.GetOutputCompilation().Compilation; + + // Act + var collector = new MethodCollector(new MagicOnionGeneratorTestOutputLogger(testOutputHelper)); + var serviceCollection = collector.Collect(compilation); + + // Assert + compilation.GetDiagnostics().Should().NotContain(x => x.Severity == DiagnosticSeverity.Error); + serviceCollection.Services[0].Methods[0].RequestType.Should().Be(MagicOnionTypeInfo.CreateFromType()); + serviceCollection.Services[0].Methods[0].ResponseType.Should().Be(MagicOnionTypeInfo.CreateFromType()); + serviceCollection.Services[0].Methods[0].MethodReturnType.Should().Be(MagicOnionTypeInfo.CreateFromType()); + } [Fact] public void Unary_Array() diff --git a/tests/MagicOnion.Generator.Tests/GenerateServiceTest.cs b/tests/MagicOnion.Generator.Tests/GenerateServiceTest.cs index cf90e2f8b..4ff7f5bf7 100644 --- a/tests/MagicOnion.Generator.Tests/GenerateServiceTest.cs +++ b/tests/MagicOnion.Generator.Tests/GenerateServiceTest.cs @@ -11,6 +11,39 @@ public GenerateServiceTest(ITestOutputHelper testOutputHelper) this.testOutputHelper = testOutputHelper; } + [Fact] + public async Task Return_UnaryResultNonGeneric() + { + using var tempWorkspace = TemporaryProjectWorkarea.Create(); + tempWorkspace.AddFileToProject("IMyService.cs", @" +using System; +using System.Threading.Tasks; +using MessagePack; +using MagicOnion; + +namespace TempProject +{ + public interface IMyService : IService + { + UnaryResult A(); + } +} + "); + + 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_UnaryResultOfT() { diff --git a/tests/MagicOnion.Generator.Tests/GenerateTest.cs b/tests/MagicOnion.Generator.Tests/GenerateTest.cs index fb586c022..7e39b7225 100644 --- a/tests/MagicOnion.Generator.Tests/GenerateTest.cs +++ b/tests/MagicOnion.Generator.Tests/GenerateTest.cs @@ -55,8 +55,8 @@ await compiler.GenerateFileAsync( "MessagePack.Formatters" ); - var compilation = tempWorkspace.GetOutputCompilation(); - compilation.GetCompilationErrors().Should().BeEmpty(); + var outputCompilation = tempWorkspace.GetOutputCompilation(); + outputCompilation.GetCompilationErrors().Should().BeEmpty(); } [Fact] @@ -83,8 +83,8 @@ await compiler.GenerateFileAsync( "MessagePack.Formatters" ); - var compilation = tempWorkspace.GetOutputCompilation(); - compilation.GetCompilationErrors().Should().BeEmpty(); + var outputCompilation = tempWorkspace.GetOutputCompilation(); + outputCompilation.GetCompilationErrors().Should().BeEmpty(); } [Fact] @@ -111,8 +111,8 @@ await compiler.GenerateFileAsync( "MessagePack.Formatters" ); - var compilation = tempWorkspace.GetOutputCompilation(); - compilation.GetCompilationErrors().Should().BeEmpty(); + var outputCompilation = tempWorkspace.GetOutputCompilation(); + outputCompilation.GetCompilationErrors().Should().BeEmpty(); } [Fact] @@ -135,8 +135,8 @@ await compiler.GenerateFileAsync( "MessagePack.Formatters" ); - var compilation = tempWorkspace.GetOutputCompilation(); - compilation.GetCompilationErrors().Should().BeEmpty(); + var outputCompilation = tempWorkspace.GetOutputCompilation(); + outputCompilation.GetCompilationErrors().Should().BeEmpty(); } [Fact] @@ -168,7 +168,7 @@ public interface IMyService : IService """); // Try create a compilation from the temporary project. - var compilation = await PseudoCompilation.CreateFromProjectAsync( + var sourceProjectCompilation = await PseudoCompilation.CreateFromProjectAsync( new[] { tempWorkspace.CsProjectPath }, Array.Empty(), new MagicOnionGeneratorTestOutputLogger(testOutputHelper), @@ -188,7 +188,7 @@ await compiler.GenerateFileAsync( var outputCompilation = tempWorkspace.GetOutputCompilation(); // Assert - compilation.GetDiagnostics().Should().NotContain(x => x.Severity == DiagnosticSeverity.Error); + sourceProjectCompilation.GetDiagnostics().Should().NotContain(x => x.Severity == DiagnosticSeverity.Error); outputCompilation.GetCompilationErrors().Should().BeEmpty(); } @@ -221,7 +221,7 @@ public interface IMyService : IService """); // Try create a compilation from the temporary project. - var compilation = await PseudoCompilation.CreateFromProjectAsync( + var sourceProjectCompilation = await PseudoCompilation.CreateFromProjectAsync( new[] { tempWorkspace.CsProjectPath }, Array.Empty(), new MagicOnionGeneratorTestOutputLogger(testOutputHelper), @@ -241,7 +241,7 @@ await compiler.GenerateFileAsync( var outputCompilation = tempWorkspace.GetOutputCompilation(); // Assert - compilation.GetDiagnostics().Should().Contain(x => x.Severity == DiagnosticSeverity.Error); // Failed to compile the project. + sourceProjectCompilation.GetDiagnostics().Should().Contain(x => x.Severity == DiagnosticSeverity.Error); // Failed to compile the project. outputCompilation.GetCompilationErrors().Should().NotBeEmpty(); } @@ -284,7 +284,7 @@ public interface IMyService : IService """); // Try create a compilation from the temporary project. - var compilation = await PseudoCompilation.CreateFromProjectAsync( + var sourceProjectCompilation = await PseudoCompilation.CreateFromProjectAsync( new[] { tempWorkspace.CsProjectPath }, Array.Empty(), new MagicOnionGeneratorTestOutputLogger(testOutputHelper), @@ -304,7 +304,7 @@ await compiler.GenerateFileAsync( var outputCompilation = tempWorkspace.GetOutputCompilation(); // Assert - compilation.GetDiagnostics().Should().NotContain(x => x.Severity == DiagnosticSeverity.Error); + sourceProjectCompilation.GetDiagnostics().Should().NotContain(x => x.Severity == DiagnosticSeverity.Error); outputCompilation.GetCompilationErrors().Should().BeEmpty(); } @@ -350,7 +350,7 @@ public interface IMyService : IService """); // Try create a compilation from the temporary project. - var compilation = await PseudoCompilation.CreateFromProjectAsync( + var sourceProjectCompilation = await PseudoCompilation.CreateFromProjectAsync( new[] { tempWorkspace.CsProjectPath }, Array.Empty(), new MagicOnionGeneratorTestOutputLogger(testOutputHelper), @@ -358,6 +358,6 @@ public interface IMyService : IService ); // Assert - compilation.GetDiagnostics().Should().Contain(x => x.Severity == DiagnosticSeverity.Error); // the compilation should have some errors. + sourceProjectCompilation.GetDiagnostics().Should().Contain(x => x.Severity == DiagnosticSeverity.Error); // the compilation should have some errors. } } diff --git a/tests/MagicOnion.Generator.Tests/MagicOnionGeneratorTestOutputLogger.cs b/tests/MagicOnion.Generator.Tests/MagicOnionGeneratorTestOutputLogger.cs index e00b813d2..589dce611 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 FALSE +#if TRUE public void Trace(string message) => outputHelper.WriteLine(message); #else public void Trace(string message) {} diff --git a/tests/MagicOnion.Integration.Tests/UnaryServiceTest.cs b/tests/MagicOnion.Integration.Tests/UnaryServiceTest.cs new file mode 100644 index 000000000..3a4aa186f --- /dev/null +++ b/tests/MagicOnion.Integration.Tests/UnaryServiceTest.cs @@ -0,0 +1,71 @@ +using Grpc.Net.Client; +using MagicOnion.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MagicOnion.Client; +using MagicOnion.Server; + +namespace MagicOnion.Integration.Tests; + +public class UnaryServiceTest : IClassFixture> +{ + readonly MagicOnionApplicationFactory factory; + + public UnaryServiceTest(MagicOnionApplicationFactory factory) + { + this.factory = factory; + } + + public static IEnumerable EnumerateMagicOnionClientFactory() + { + yield return new [] { new TestMagicOnionClientFactory("Dynamic", x => MagicOnionClient.Create(x, MagicOnionMessageSerializerProvider.Default)) }; + yield return new [] { new TestMagicOnionClientFactory("Generated", x => new UnaryServiceClient(x, MagicOnionMessageSerializerProvider.Default)) }; + } + + [Theory] + [MemberData(nameof(EnumerateMagicOnionClientFactory))] + public async Task NonGeneric(TestMagicOnionClientFactory clientFactory) + { + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = factory.CreateDefaultClient() }); + var client = clientFactory.Create(channel); + var result = client.NonGeneric(123); + await result; + + result.GetTrailers().Should().Contain(x => x.Key == "x-request-arg0" && x.Value == "123"); + } + + [Theory] + [MemberData(nameof(EnumerateMagicOnionClientFactory))] + public async Task ManyParametersReturnsValueType(TestMagicOnionClientFactory clientFactory) + { + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = factory.CreateDefaultClient() }); + var client = clientFactory.Create(channel); + var result = await client.ManyParametersReturnsValueType(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); + result.Should().Be(120); + } +} + +public interface IUnaryService : IService +{ + UnaryResult NonGeneric(int arg0); + + // T0 - T14 (TypeParams = 15) + UnaryResult ManyParametersReturnsValueType(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, int arg9, int arg10, int arg11, int arg12, int arg13, int arg14); +} + +public class UnaryService : ServiceBase, IUnaryService +{ + public UnaryResult NonGeneric(int arg0) + { + Context.CallContext.ResponseTrailers.Add("x-request-arg0", "" + arg0); + return default; + } + + public UnaryResult ManyParametersReturnsValueType(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, int arg9, int arg10, int arg11, int arg12, int arg13, int arg14) + { + return UnaryResult(arg0 + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8 + arg9 + arg10 + arg11 + arg12 + arg13 + arg14); + } +} diff --git a/tests/MagicOnion.Integration.Tests/_GeneratedClient.cs b/tests/MagicOnion.Integration.Tests/_GeneratedClient.cs index b739b5b5b..b1a705e0a 100644 --- a/tests/MagicOnion.Integration.Tests/_GeneratedClient.cs +++ b/tests/MagicOnion.Integration.Tests/_GeneratedClient.cs @@ -37,6 +37,7 @@ public static void Register() MagicOnionClientRegistry.Register((x, y) => new MagicOnion.Integration.Tests.DynamicArgumentTupleServiceClient(x, y)); MagicOnionClientRegistry.Register((x, y) => new MagicOnion.Integration.Tests.MessageSerializerTestServiceClient(x, y)); MagicOnionClientRegistry.Register((x, y) => new MagicOnion.Integration.Tests.StreamingTestServiceClient(x, y)); + MagicOnionClientRegistry.Register((x, y) => new MagicOnion.Integration.Tests.UnaryServiceClient(x, y)); MagicOnionClientRegistry.Register((x, y) => new MagicOnion.Integration.Tests.MemoryPack.MemoryPackMessageSerializerTestServiceClient(x, y)); StreamingHubClientRegistry.Register((a, _, b, c, d, e) => new MagicOnion.Integration.Tests.HandCraftedStreamingHubClientTestHubClient(a, b, c, d, e)); @@ -341,6 +342,50 @@ private StreamingTestServiceClient(MagicOnionClientOptions options, ClientCore c } } +namespace MagicOnion.Integration.Tests +{ + using global::System; + using global::Grpc.Core; + using global::MagicOnion; + using global::MagicOnion.Client; + using global::MessagePack; + + [global::MagicOnion.Ignore] + public class UnaryServiceClient : global::MagicOnion.Client.MagicOnionClientBase, global::MagicOnion.Integration.Tests.IUnaryService + { + class ClientCore + { + public global::MagicOnion.Client.Internal.RawMethodInvoker NonGeneric; + public global::MagicOnion.Client.Internal.RawMethodInvoker, global::System.Int32> ManyParametersReturnsValueType; + public ClientCore(global::MagicOnion.Serialization.IMagicOnionMessageSerializerProvider messageSerializer) + { + this.NonGeneric = global::MagicOnion.Client.Internal.RawMethodInvoker.Create_ValueType_ValueType(global::Grpc.Core.MethodType.Unary, "IUnaryService", "NonGeneric", messageSerializer); + this.ManyParametersReturnsValueType = global::MagicOnion.Client.Internal.RawMethodInvoker.Create_ValueType_ValueType, global::System.Int32>(global::Grpc.Core.MethodType.Unary, "IUnaryService", "ManyParametersReturnsValueType", messageSerializer); + } + } + + readonly ClientCore core; + + public UnaryServiceClient(global::MagicOnion.Client.MagicOnionClientOptions options, global::MagicOnion.Serialization.IMagicOnionMessageSerializerProvider messageSerializer) : base(options) + { + this.core = new ClientCore(messageSerializer); + } + + private UnaryServiceClient(MagicOnionClientOptions options, ClientCore core) : base(options) + { + this.core = core; + } + + protected override global::MagicOnion.Client.MagicOnionClientBase Clone(global::MagicOnion.Client.MagicOnionClientOptions options) + => new UnaryServiceClient(options, core); + + public global::MagicOnion.UnaryResult NonGeneric(global::System.Int32 arg0) + => this.core.NonGeneric.InvokeUnaryNonGeneric(this, "IUnaryService/NonGeneric", arg0); + public global::MagicOnion.UnaryResult ManyParametersReturnsValueType(global::System.Int32 arg0, global::System.Int32 arg1, global::System.Int32 arg2, global::System.Int32 arg3, global::System.Int32 arg4, global::System.Int32 arg5, global::System.Int32 arg6, global::System.Int32 arg7, global::System.Int32 arg8, global::System.Int32 arg9, global::System.Int32 arg10, global::System.Int32 arg11, global::System.Int32 arg12, global::System.Int32 arg13, global::System.Int32 arg14) + => this.core.ManyParametersReturnsValueType.InvokeUnary(this, "IUnaryService/ManyParametersReturnsValueType", new global::MagicOnion.DynamicArgumentTuple(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); + } +} + namespace MagicOnion.Integration.Tests.MemoryPack { using global::System; diff --git a/tests/MagicOnion.Server.Tests/UnaryServiceTest.cs b/tests/MagicOnion.Server.Tests/UnaryServiceTest.cs index 21b4636d1..536dec609 100644 --- a/tests/MagicOnion.Server.Tests/UnaryServiceTest.cs +++ b/tests/MagicOnion.Server.Tests/UnaryServiceTest.cs @@ -182,4 +182,31 @@ public async Task TwoRefTypeParametersReturnRefType() (await client.TwoRefTypeParametersReturnRefTypeAsync(new BasicServerSample.Services.MyRequest(123), new BasicServerSample.Services.MyRequest(456))).Value.Should().Be((123 + 456).ToString()); } + + [Fact] + public async Task NonGeneric_NoParameter() + { + var httpClient = factory.CreateDefaultClient(); + var client = MagicOnionClient.Create(GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient })); + + await client.NonGenericNoParameterAsync(); + } + + [Fact] + public async Task NonGeneric_OneValueTypeParameter() + { + var httpClient = factory.CreateDefaultClient(); + var client = MagicOnionClient.Create(GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient })); + + await client.NonGenericOneValueTypeParameterAsync(123); + } + + [Fact] + public async Task NonGeneric_TwoValueTypeParameters() + { + var httpClient = factory.CreateDefaultClient(); + var client = MagicOnionClient.Create(GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient })); + + await client.NonGenericTwoValueTypeParameterAsync(123, 456); + } } diff --git a/tests/MagicOnion.Shared.Tests/UnaryResultNonGenericTest.cs b/tests/MagicOnion.Shared.Tests/UnaryResultNonGenericTest.cs index cebfed2e2..1b79ca64f 100644 --- a/tests/MagicOnion.Shared.Tests/UnaryResultNonGenericTest.cs +++ b/tests/MagicOnion.Shared.Tests/UnaryResultNonGenericTest.cs @@ -47,7 +47,7 @@ public async Task Ctor_TaskOfResponseContext() public void Ctor_TaskOfResponseContext_Null() { // Arrange - var value = Task.FromResult(DummyResponseContext.Create(Nil.Default)); + var value = default(Task>); // Act var result = Record.Exception(() => new UnaryResult(value)); // Assert diff --git a/tests/samples/BasicServerSample/Services/BasicUnaryService.cs b/tests/samples/BasicServerSample/Services/BasicUnaryService.cs index 1c4b861d3..e9f960c1e 100644 --- a/tests/samples/BasicServerSample/Services/BasicUnaryService.cs +++ b/tests/samples/BasicServerSample/Services/BasicUnaryService.cs @@ -27,6 +27,10 @@ public interface IBasicUnaryService : IService UnaryResult TwoRefTypeParametersReturnValueTypeAsync(MyRequest a, MyRequest b); UnaryResult OneRefTypeParameterReturnRefTypeAsync(MyRequest a); UnaryResult TwoRefTypeParametersReturnRefTypeAsync(MyRequest a, MyRequest b); + + UnaryResult NonGenericNoParameterAsync(); + UnaryResult NonGenericOneValueTypeParameterAsync(int a); + UnaryResult NonGenericTwoValueTypeParameterAsync(int a, int b); } [MessagePackObject(true)] @@ -103,4 +107,13 @@ public UnaryResult OneRefTypeParameterReturnRefTypeAsync(MyRequest a public UnaryResult TwoRefTypeParametersReturnRefTypeAsync(MyRequest a, MyRequest b) => UnaryResult(new MyResponse((a.Value + b.Value).ToString())); -} \ No newline at end of file + + public UnaryResult NonGenericNoParameterAsync() + => MagicOnion.UnaryResult.CompletedResult; + + public UnaryResult NonGenericOneValueTypeParameterAsync(int a) + => MagicOnion.UnaryResult.CompletedResult; + + public UnaryResult NonGenericTwoValueTypeParameterAsync(int a, int b) + => MagicOnion.UnaryResult.CompletedResult; +}