diff --git a/.github/workflows/build-canary.yml b/.github/workflows/build-canary.yml index 3a071cc98..f94bc5c16 100644 --- a/.github/workflows/build-canary.yml +++ b/.github/workflows/build-canary.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - vNext tags: - "!*" # not a tag push paths-ignore: diff --git a/.github/workflows/build-debug.yml b/.github/workflows/build-debug.yml index a296c2e63..1e66d10e6 100644 --- a/.github/workflows/build-debug.yml +++ b/.github/workflows/build-debug.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - vNext tags: - "!*" # not a tag push paths-ignore: @@ -13,6 +14,7 @@ on: pull_request: branches: - main + - vNext paths-ignore: - '**.md' - .github/** diff --git a/Directory.Build.props b/Directory.Build.props index 0f05ca832..26e638cd2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 6.1.3 + 7.0.0 diff --git a/Directory.Packages.props b/Directory.Packages.props index bb5de49c0..cac34b822 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,8 +9,10 @@ 2.5.129 4.3.1 3.9.0 + 0.1.11 + @@ -20,7 +22,11 @@ + + + + @@ -46,4 +52,4 @@ - \ No newline at end of file + diff --git a/MagicOnion.sln b/MagicOnion.sln index d03c2bfb0..41869b4d8 100644 --- a/MagicOnion.sln +++ b/MagicOnion.sln @@ -120,6 +120,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MagicOnion.Serialization.Me EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MagicOnion.Abstractions.Tests", "tests\MagicOnion.Abstractions.Tests\MagicOnion.Abstractions.Tests.csproj", "{D340EFB8-128A-4B49-A47A-F00A905D10AC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatApp.Console", "samples\ChatApp\ChatApp.Console\ChatApp.Console.csproj", "{AF21B7BD-7399-41B7-B0D4-08ACDC952E50}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microbenchmark", "Microbenchmark", "{F1FD52DD-E8A4-4CF0-A857-1A22443A0324}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microbenchmark.Client", "perf\Microbenchmark\Microbenchmark.Client\Microbenchmark.Client.csproj", "{CDBB141A-E0A9-4FD8-8260-1FB1E95C4E80}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -266,6 +272,14 @@ Global {D340EFB8-128A-4B49-A47A-F00A905D10AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {D340EFB8-128A-4B49-A47A-F00A905D10AC}.Release|Any CPU.ActiveCfg = Release|Any CPU {D340EFB8-128A-4B49-A47A-F00A905D10AC}.Release|Any CPU.Build.0 = Release|Any CPU + {AF21B7BD-7399-41B7-B0D4-08ACDC952E50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF21B7BD-7399-41B7-B0D4-08ACDC952E50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF21B7BD-7399-41B7-B0D4-08ACDC952E50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF21B7BD-7399-41B7-B0D4-08ACDC952E50}.Release|Any CPU.Build.0 = Release|Any CPU + {CDBB141A-E0A9-4FD8-8260-1FB1E95C4E80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDBB141A-E0A9-4FD8-8260-1FB1E95C4E80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDBB141A-E0A9-4FD8-8260-1FB1E95C4E80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDBB141A-E0A9-4FD8-8260-1FB1E95C4E80}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -312,6 +326,9 @@ Global {2996029B-D329-499F-8525-69614A820135} = {1987061F-8970-4018-8D58-6932961C9EB4} {701E193F-587D-4C20-8970-6E215B0634F8} = {7ACC27E8-8FBE-4807-B91F-B89AF3CFF7E0} {D340EFB8-128A-4B49-A47A-F00A905D10AC} = {7ACC27E8-8FBE-4807-B91F-B89AF3CFF7E0} + {AF21B7BD-7399-41B7-B0D4-08ACDC952E50} = {FEE2B9AB-A1D0-41BA-A172-FC95935542DF} + {F1FD52DD-E8A4-4CF0-A857-1A22443A0324} = {A0CED9FB-5B18-4EE3-859F-CE3A6F90A82A} + {CDBB141A-E0A9-4FD8-8260-1FB1E95C4E80} = {F1FD52DD-E8A4-4CF0-A857-1A22443A0324} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D5B2E7E3-B727-40A1-BE68-7BAC9B9DE2FE} diff --git a/perf/BenchmarkApp/PerformanceTest.Client/Program.cs b/perf/BenchmarkApp/PerformanceTest.Client/Program.cs index 284ce3c6e..c946bb662 100644 --- a/perf/BenchmarkApp/PerformanceTest.Client/Program.cs +++ b/perf/BenchmarkApp/PerformanceTest.Client/Program.cs @@ -52,7 +52,7 @@ async Task Main( // Create a control channel using var channelControl = GrpcChannel.ForAddress(config.Url); var controlServiceClient = MagicOnionClient.Create(channelControl); - controlServiceClient.SetMemoryProfilerCollectAllocations(true); + await controlServiceClient.SetMemoryProfilerCollectAllocationsAsync(true); ServerInformation serverInfo; WriteLog("Gathering the server information..."); diff --git a/perf/BenchmarkApp/PerformanceTest.Server/PerfTestControlService.cs b/perf/BenchmarkApp/PerformanceTest.Server/PerfTestControlService.cs index d8d62d59b..004b66d3c 100644 --- a/perf/BenchmarkApp/PerformanceTest.Server/PerfTestControlService.cs +++ b/perf/BenchmarkApp/PerformanceTest.Server/PerfTestControlService.cs @@ -25,7 +25,7 @@ public UnaryResult GetServerInformationAsync() ApplicationInformation.Current.IsAttached)); } - public UnaryResult SetMemoryProfilerCollectAllocations(bool enable) + public UnaryResult SetMemoryProfilerCollectAllocationsAsync(bool enable) { MemoryProfiler.CollectAllocations(enable); return UnaryResult.CompletedResult; diff --git a/perf/BenchmarkApp/PerformanceTest.Shared/IPerfTestControlService.cs b/perf/BenchmarkApp/PerformanceTest.Shared/IPerfTestControlService.cs index e747c92e4..e4d621fed 100644 --- a/perf/BenchmarkApp/PerformanceTest.Shared/IPerfTestControlService.cs +++ b/perf/BenchmarkApp/PerformanceTest.Shared/IPerfTestControlService.cs @@ -9,7 +9,7 @@ public interface IPerfTestControlService : IService { UnaryResult GetServerInformationAsync(); - UnaryResult SetMemoryProfilerCollectAllocations(bool enable); + UnaryResult SetMemoryProfilerCollectAllocationsAsync(bool enable); UnaryResult CreateMemoryProfilerSnapshotAsync(string name); } diff --git a/perf/Microbenchmark/Microbenchmark.Client/.gitignore b/perf/Microbenchmark/Microbenchmark.Client/.gitignore new file mode 100644 index 000000000..1c2dac683 --- /dev/null +++ b/perf/Microbenchmark/Microbenchmark.Client/.gitignore @@ -0,0 +1 @@ +BenchmarkDotNet.Artifacts \ No newline at end of file diff --git a/perf/Microbenchmark/Microbenchmark.Client/ChannelAsyncStreamReader.cs b/perf/Microbenchmark/Microbenchmark.Client/ChannelAsyncStreamReader.cs new file mode 100644 index 000000000..bfa5bf7bb --- /dev/null +++ b/perf/Microbenchmark/Microbenchmark.Client/ChannelAsyncStreamReader.cs @@ -0,0 +1,37 @@ +using System.Runtime.CompilerServices; +using System.Threading.Channels; +using Grpc.Core; + +namespace Microbenchmark.Client; + +class ChannelAsyncStreamReader : IAsyncStreamReader +{ + readonly ChannelReader reader; + + public T Current { get; private set; } = default!; + + public ChannelAsyncStreamReader(Channel channel) + { + reader = channel.Reader; + } + + public Task MoveNext(CancellationToken cancellationToken) + { + return MoveNextCore(cancellationToken).AsTask(); + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + async ValueTask MoveNextCore(CancellationToken cancellationToken) + { + if (await reader.WaitToReadAsync()) + { + if (reader.TryRead(out var item)) + { + Current = item; + return true; + } + } + + return false; + } +} diff --git a/perf/Microbenchmark/Microbenchmark.Client/ChannelClientStreamWriter.cs b/perf/Microbenchmark/Microbenchmark.Client/ChannelClientStreamWriter.cs new file mode 100644 index 000000000..7136ed34b --- /dev/null +++ b/perf/Microbenchmark/Microbenchmark.Client/ChannelClientStreamWriter.cs @@ -0,0 +1,28 @@ +using System.Threading.Channels; +using Grpc.Core; + +namespace Microbenchmark.Client; + +class ChannelClientStreamWriter : IClientStreamWriter +{ + readonly ChannelWriter writer; + + public WriteOptions? WriteOptions { get; set; } + + public ChannelClientStreamWriter(ChannelWriter writer) + { + this.writer = writer; + } + + public Task CompleteAsync() + { + writer.Complete(); + return Task.CompletedTask; + } + + public Task WriteAsync(T message) + { + writer.TryWrite(message); + return Task.CompletedTask; + } +} diff --git a/perf/Microbenchmark/Microbenchmark.Client/HubMethodBenchmarks.cs b/perf/Microbenchmark/Microbenchmark.Client/HubMethodBenchmarks.cs new file mode 100644 index 000000000..f4eaeaf88 --- /dev/null +++ b/perf/Microbenchmark/Microbenchmark.Client/HubMethodBenchmarks.cs @@ -0,0 +1,82 @@ +using BenchmarkDotNet.Attributes; +using MagicOnion.Client.DynamicClient; + +namespace Microbenchmark.Client; + +[MemoryDiagnoser, RankColumn] +[ShortRunJob] +public class HubMethodBenchmarks +{ + readonly StreamingHubClientTestHelper helper; + readonly Task responseTask; + readonly ITestHub client; + + static ReadOnlySpan IntResponse => [0xcd, 0x30, 0x39 /* 12345 (int) */]; + static ReadOnlySpan NilResponse => [0xc0 /* Nil */]; + static ReadOnlySpan StringResponse => [0xa6, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21 /* "Hello!" (string) */]; + + public HubMethodBenchmarks() + { + this.helper = new StreamingHubClientTestHelper(new TestHubReceiver(), DynamicStreamingHubClientFactoryProvider.Instance); + this.responseTask = Task.Run(async () => + { + while (true) + { + var req = await helper.ReadRequestNoDeserializeAsync(); + if (req.MethodId is -2087943100 or 1273874383) + { + // Parameter_Zero_Return_ValueType, Parameter_Many_Return_ValueType + helper.WriteResponse(req.MessageId, req.MethodId, IntResponse); + } + else if (req.MethodId is -1841486598) + { + // ValueTask_Parameter_Zero_NoReturn + helper.WriteResponse(req.MessageId, req.MethodId, NilResponse); + } + else if (req.MethodId is -440496944 or -1110031569) + { + // Parameter_Zero_Return_RefType, Parameter_Many_Return_RefType + helper.WriteResponse(req.MessageId, req.MethodId, StringResponse); + } + } + }); + this.client = helper.ConnectAsync().GetAwaiter().GetResult(); + } + + [Benchmark] + public Task Void_Parameter_Zero_NoReturn() + { + client.Void_Parameter_Zero_NoReturn(); + return Task.CompletedTask; + } + + [Benchmark] + public async Task ValueTask_Parameter_Zero_NoReturn() + { + await client.ValueTask_Parameter_Zero_NoReturn(); + } + + [Benchmark] + public async Task Parameter_Zero_Return_ValueType() + { + var value = await client.Parameter_Zero_Return_ValueType(); + } + + [Benchmark] + public async Task Parameter_Many_Return_ValueType() + { + var value = await client.Parameter_Many_Return_ValueType("Hello", 12345, true); + } + + [Benchmark] + public async Task Parameter_Zero_Return_RefType() + { + var value = await client.Parameter_Zero_Return_RefType(); + } + + [Benchmark] + public async Task Parameter_Many_Return_RefType() + { + var value = await client.Parameter_Many_Return_RefType("Hello", 12345, true); + } +} diff --git a/perf/Microbenchmark/Microbenchmark.Client/HubReceiverBroadcastBenchmarks.cs b/perf/Microbenchmark/Microbenchmark.Client/HubReceiverBroadcastBenchmarks.cs new file mode 100644 index 000000000..b8c198142 --- /dev/null +++ b/perf/Microbenchmark/Microbenchmark.Client/HubReceiverBroadcastBenchmarks.cs @@ -0,0 +1,65 @@ +using BenchmarkDotNet.Attributes; +using MagicOnion.Client.DynamicClient; + +namespace Microbenchmark.Client; + +[MemoryDiagnoser, RankColumn] +[ShortRunJob] +public class HubReceiverBroadcastBenchmarks +{ + StreamingHubClientTestHelper helper = default!; + ITestHub client = default!; + TestHubReceiver receiver = default!; + + static ReadOnlySpan BroadcastMessage_Parameter_Zero => [0x92, 0xce, 0x76, 0xe4, 0x37, 0x1b /* 1994667803 */, 0xc0 /* Nil */]; // [MethodId(int), SerializedArgument] + static ReadOnlySpan BroadcastMessage_Parameter_Many => [0x92, 0xce, 0x4c, 0xb8, 0x83, 0xca /* 1287160778 */, 0x93, 0xa6, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0xcd, 0x30, 0x39, 0xc3 /* [ "Hello", 12345, true ] */]; // [MethodId(int), SerializedArgument] + + void Setup() + { + this.receiver = new TestHubReceiver(); + this.helper = new StreamingHubClientTestHelper(receiver, DynamicStreamingHubClientFactoryProvider.Instance); + this.client = helper.ConnectAsync().GetAwaiter().GetResult(); + } + + [GlobalSetup(Targets = [nameof(Parameter_Zero), nameof(Parameter_Many)])] + public void UnsetSynchronizationContext() + { + SynchronizationContext.SetSynchronizationContext(null); + Setup(); + } + + [GlobalSetup(Targets = [nameof(Parameter_Zero_With_SynchronizationContext), nameof(Parameter_Many_With_SynchronizationContext)])] + public void SetSynchronizationContext() + { + SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext()); + Setup(); + } + + [Benchmark] + public void Parameter_Zero() + { + helper.WriteResponseRaw(BroadcastMessage_Parameter_Zero); + receiver.Received.Wait(); + } + + [Benchmark] + public void Parameter_Many() + { + helper.WriteResponseRaw(BroadcastMessage_Parameter_Many); + receiver.Received.Wait(); + } + + [Benchmark] + public void Parameter_Zero_With_SynchronizationContext() + { + helper.WriteResponseRaw(BroadcastMessage_Parameter_Zero); + receiver.Received.Wait(); + } + + [Benchmark] + public void Parameter_Many_With_SynchronizationContext() + { + helper.WriteResponseRaw(BroadcastMessage_Parameter_Many); + receiver.Received.Wait(); + } +} diff --git a/perf/Microbenchmark/Microbenchmark.Client/Microbenchmark.Client.csproj b/perf/Microbenchmark/Microbenchmark.Client/Microbenchmark.Client.csproj new file mode 100644 index 000000000..64dab9f3c --- /dev/null +++ b/perf/Microbenchmark/Microbenchmark.Client/Microbenchmark.Client.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + + true + ..\..\..\src\MagicOnion\opensource.snk + + + + + + + + + + + diff --git a/perf/Microbenchmark/Microbenchmark.Client/Program.cs b/perf/Microbenchmark/Microbenchmark.Client/Program.cs new file mode 100644 index 000000000..60e603a1b --- /dev/null +++ b/perf/Microbenchmark/Microbenchmark.Client/Program.cs @@ -0,0 +1,46 @@ +using BenchmarkDotNet.Running; +using MagicOnion; +using Microbenchmark.Client; + +//BenchmarkRunner.Run(); +BenchmarkRunner.Run(); + +#if FALSE +var b = new HubMethodBenchmarks(); +for (var i = 0; i < 1000000; i++) + await b.Parameter_Zero_Return_ValueType(); +#endif + +class MySynchronizationContext : SynchronizationContext; + +class TestHubReceiver : ITestHubReceiver +{ + public ManualResetEventSlim Received { get; } = new(); + + public void Parameter_Zero() + { + Received.Set(); + } + + public void Parameter_Many(string arg0, int arg1, bool arg2) + { + Received.Set(); + } +} + +public interface ITestHub : IStreamingHub +{ + void Void_Parameter_Zero_NoReturn(); + ValueTask ValueTask_Parameter_Zero_NoReturn(); + + Task Parameter_Zero_Return_ValueType(); + Task Parameter_Many_Return_ValueType(string arg0, int arg1, bool arg2); + Task Parameter_Zero_Return_RefType(); + Task Parameter_Many_Return_RefType(string arg0, int arg1, bool arg2); +} + +public interface ITestHubReceiver +{ + void Parameter_Zero(); + void Parameter_Many(string arg0, int arg1, bool arg2); +} diff --git a/perf/Microbenchmark/Microbenchmark.Client/StreamingHubClientTestHelper.cs b/perf/Microbenchmark/Microbenchmark.Client/StreamingHubClientTestHelper.cs new file mode 100644 index 000000000..c6490db6f --- /dev/null +++ b/perf/Microbenchmark/Microbenchmark.Client/StreamingHubClientTestHelper.cs @@ -0,0 +1,204 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Threading.Channels; +using Grpc.Core; +using MagicOnion; +using MagicOnion.Client; +using MagicOnion.Internal; +using MagicOnion.Internal.Buffers; +using MessagePack; + +namespace Microbenchmark.Client; + +class StreamingHubClientTestHelper + where TStreamingHub : IStreamingHub + where TReceiver : class +{ + readonly Channel requestChannel; + readonly ChannelClientStreamWriter requestStream; + readonly Channel responseChannel; + readonly ChannelAsyncStreamReader responseStream; + + readonly CallInvoker callInvokerMock; + readonly TReceiver receiver; + readonly IStreamingHubClientFactoryProvider? factoryProvider; + + public TReceiver Receiver => receiver; + public CallInvoker CallInvoker => callInvokerMock; + + public StreamingHubClientTestHelper(TReceiver receiver, IStreamingHubClientFactoryProvider? factoryProvider = null) + { + this.receiver = receiver; + this.requestChannel = Channel.CreateUnbounded(new() { SingleReader = true, SingleWriter = true }); + this.requestStream = new ChannelClientStreamWriter(requestChannel); + this.responseChannel = Channel.CreateUnbounded(new() { SingleReader = true, SingleWriter = true }); + this.responseStream = new ChannelAsyncStreamReader(responseChannel); + + this.factoryProvider = factoryProvider; + + callInvokerMock = new MockCallInvoker(this); + } + + class MockCallInvoker(StreamingHubClientTestHelper parent) : CallInvoker + { + public override TResponse BlockingUnaryCall(Method method, string? host, CallOptions options, TRequest request) + => throw new NotImplementedException(); + + public override AsyncUnaryCall AsyncUnaryCall(Method method, string? host, CallOptions options, TRequest request) + => throw new NotImplementedException(); + + public override AsyncServerStreamingCall AsyncServerStreamingCall(Method method, string? host, CallOptions options, TRequest request) + => throw new NotImplementedException(); + + public override AsyncClientStreamingCall AsyncClientStreamingCall(Method method, string? host, CallOptions options) + => throw new NotImplementedException(); + + public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall(Method method, string? host, CallOptions options) + => new( + (IClientStreamWriter)parent.requestStream, + (IAsyncStreamReader)parent.responseStream, + _ => Task.FromResult(new Metadata { { "x-magiconion-streaminghub-version", "2" } }), + _ => Status.DefaultSuccess, + _ => Metadata.Empty, + _ => { }, + new object()); + } + + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + return await StreamingHubClient.ConnectAsync( + callInvokerMock, + receiver, + cancellationToken: cancellationToken, + factoryProvider: factoryProvider + ); + } + + public async Task ConnectAsync(StreamingHubClientOptions options, CancellationToken cancellationToken = default) + { + return await StreamingHubClient.ConnectAsync( + callInvokerMock, + receiver, + options, + cancellationToken: cancellationToken, + factoryProvider: factoryProvider + ); + } + + public async ValueTask> ReadRequestRawAsync() + { + var requestPayload = await requestChannel.Reader.ReadAsync(); + return requestPayload.Memory; + } + + public async ValueTask<(int MessageId, int MethodId, T Request)> ReadRequestAsync() + { + var requestPayload = await requestChannel.Reader.ReadAsync(); + try + { + return ReadRequestPayload(requestPayload.Memory); + } + finally + { + StreamingHubPayloadPool.Shared.Return(requestPayload); + } + } + + public async ValueTask<(int MessageId, int MethodId, ReadOnlyMemory Request)> ReadRequestNoDeserializeAsync() + { + var requestPayload = await requestChannel.Reader.ReadAsync(); + try + { + return ReadRequestPayload(requestPayload.Memory); + } + finally + { + StreamingHubPayloadPool.Shared.Return(requestPayload); + } + } + + public async ValueTask<(int MethodId, T Request)> ReadFireAndForgetRequestAsync() + { + var requestPayload = await requestChannel.Reader.ReadAsync(); + try + { + return ReadFireAndForgetRequestPayload(requestPayload.Memory); + } + finally + { + StreamingHubPayloadPool.Shared.Return(requestPayload); + } + } + + public void WriteResponseRaw(ReadOnlySpan data) + { + responseChannel.Writer.TryWrite(StreamingHubPayloadPool.Shared.RentOrCreate(data)); + } + + public void WriteResponse(int messageId, int methodId, T response) + { + responseChannel.Writer.TryWrite(BuildResponsePayload(messageId, methodId, response)); + } + + public void WriteResponse(int messageId, int methodId, ReadOnlySpan response) + { + responseChannel.Writer.TryWrite(BuildResponsePayload(messageId, methodId, response)); + } + + static StreamingHubPayload BuildResponsePayload(int messageId, int methodId, T response) + { + using var bufferWriter = ArrayPoolBufferWriter.RentThreadStaticWriter(); + var messagePackWriter = new MessagePackWriter(bufferWriter); + messagePackWriter.WriteArrayHeader(3); + messagePackWriter.Write(messageId); + messagePackWriter.Write(methodId); + MessagePackSerializer.Serialize(ref messagePackWriter, response); + messagePackWriter.Flush(); + return StreamingHubPayloadPool.Shared.RentOrCreate(bufferWriter.WrittenSpan); + } + + static StreamingHubPayload BuildResponsePayload(int messageId, int methodId, ReadOnlySpan response) + { + using var bufferWriter = ArrayPoolBufferWriter.RentThreadStaticWriter(); + var messagePackWriter = new MessagePackWriter(bufferWriter); + messagePackWriter.WriteArrayHeader(3); + messagePackWriter.Write(messageId); + messagePackWriter.Write(methodId); + messagePackWriter.Flush(); + bufferWriter.Write(response); + return StreamingHubPayloadPool.Shared.RentOrCreate(bufferWriter.WrittenSpan); + } + + static (int MessageId, int MethodId, T Body) ReadRequestPayload(ReadOnlyMemory payload) + { + // Array[3][messageId (int), methodId (int), request body...] + var messagePackReader = new MessagePackReader(payload); + var arraySize = messagePackReader.ReadArrayHeader(); + Debug.Assert(arraySize == 3); + var messageId = messagePackReader.ReadInt32(); + var methodId = messagePackReader.ReadInt32(); + return (messageId, methodId, MessagePackSerializer.Deserialize(ref messagePackReader)); + } + + static (int MessageId, int MethodId, ReadOnlyMemory Body) ReadRequestPayload(ReadOnlyMemory payload) + { + // Array[3][messageId (int), methodId (int), request body...] + var messagePackReader = new MessagePackReader(payload); + var arraySize = messagePackReader.ReadArrayHeader(); + Debug.Assert(arraySize == 3); + var messageId = messagePackReader.ReadInt32(); + var methodId = messagePackReader.ReadInt32(); + return (messageId, methodId, payload.Slice((int)messagePackReader.Consumed)); + } + + static (int MethodId, T Body) ReadFireAndForgetRequestPayload(ReadOnlyMemory payload) + { + // Array[2][methodId (int), request body...] + var messagePackReader = new MessagePackReader(payload); + var arraySize = messagePackReader.ReadArrayHeader(); + Debug.Assert(arraySize == 2); + var methodId = messagePackReader.ReadInt32(); + return (methodId, MessagePackSerializer.Deserialize(ref messagePackReader)); + } +} diff --git a/samples/ChatApp/ChatApp.Console/ChatApp.Console.csproj b/samples/ChatApp/ChatApp.Console/ChatApp.Console.csproj new file mode 100644 index 000000000..aafa3e639 --- /dev/null +++ b/samples/ChatApp/ChatApp.Console/ChatApp.Console.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/samples/ChatApp/ChatApp.Console/Program.cs b/samples/ChatApp/ChatApp.Console/Program.cs new file mode 100644 index 000000000..71c1c4429 --- /dev/null +++ b/samples/ChatApp/ChatApp.Console/Program.cs @@ -0,0 +1,57 @@ +// See https://aka.ms/new-console-template for more information + +using ChatApp.Shared.Hubs; +using ChatApp.Shared.MessagePackObjects; +using Grpc.Net.Client; +using MagicOnion.Client; + +var channel = GrpcChannel.ForAddress("http://localhost:5000"); + +var sessionId = Guid.NewGuid(); +Console.WriteLine("Connecting..."); +var hub = await StreamingHubClient.ConnectAsync(channel, new ChatHubReceiver(sessionId)); +Console.WriteLine($"Connected: {sessionId}"); + +Console.Write("UserName: "); +var userName = Console.ReadLine(); +Console.Write("RoomName: "); +var roomName = Console.ReadLine(); + +Console.WriteLine($"Join: RoomName={roomName}; UserName={userName}"); +await hub.JoinAsync(new JoinRequest() { RoomName = roomName, UserName = userName }); +Console.WriteLine($"Joined"); + +while (true) +{ + var message = Console.ReadLine(); + await hub.SendMessageAsync(message); +} + +[MagicOnionClientGeneration(typeof(IChatHub))] +partial class MagicOnionGeneratedClientInitializer; + + +class ChatHubReceiver(Guid sessionId) : IChatHubReceiver +{ + public void OnJoin(string name) + { + Console.WriteLine($" Join: {name}"); + } + + public void OnLeave(string name) + { + Console.WriteLine($" Leave: {name}"); + } + + public void OnSendMessage(MessageResponse message) + { + Console.WriteLine($"{message.UserName}> {message.Message}"); + } + + public async Task HelloAsync(string name, int age) + { + Console.WriteLine("HelloAsync called"); + await Task.Delay(100); + return $"Hello {name} ({age})!; {sessionId}"; + } +} diff --git a/samples/ChatApp/ChatApp.Server/ChatHub.cs b/samples/ChatApp/ChatApp.Server/ChatHub.cs index d6bf64d3a..31ee72ef0 100644 --- a/samples/ChatApp/ChatApp.Server/ChatHub.cs +++ b/samples/ChatApp/ChatApp.Server/ChatHub.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Cysharp.Runtime.Multicast; namespace ChatApp.Server; @@ -13,8 +14,14 @@ namespace ChatApp.Server; /// public class ChatHub : StreamingHubBase, IChatHub { - private IGroup room; + private IGroup room; private string myName; + private readonly IMulticastSyncGroup roomForAll; + + public ChatHub(IMulticastGroupProvider groupProvider) + { + roomForAll = groupProvider.GetOrAddSynchronousGroup("All"); + } public async Task JoinAsync(JoinRequest request) { @@ -22,7 +29,7 @@ public async Task JoinAsync(JoinRequest request) this.myName = request.UserName; - this.Broadcast(this.room).OnJoin(request.UserName); + this.room.All.OnJoin(request.UserName); } @@ -31,7 +38,7 @@ public async Task LeaveAsync() if (this.room is not null) { await this.room.RemoveAsync(this.Context); - this.Broadcast(this.room).OnLeave(this.myName); + this.room.All.OnLeave(this.myName); } } @@ -39,8 +46,16 @@ public async Task SendMessageAsync(string message) { if (this.room is not null) { - var response = new MessageResponse { UserName = this.myName, Message = message }; - this.Broadcast(this.room).OnSendMessage(response); + if (message.StartsWith("/global ", StringComparison.InvariantCultureIgnoreCase)) + { + var response = new MessageResponse { UserName = this.myName, Message = message.Substring("/global ".Length) }; + this.roomForAll.All.OnSendMessage(response); + } + else + { + var response = new MessageResponse { UserName = this.myName, Message = message }; + this.room.All.OnSendMessage(response); + } } await Task.CompletedTask; @@ -61,6 +76,7 @@ protected override ValueTask OnConnecting() { // handle connection if needed. Console.WriteLine($"client connected {this.Context.ContextId}"); + roomForAll.Add(ConnectionId, Client); return CompletedTask; } @@ -68,6 +84,7 @@ protected override ValueTask OnDisconnected() { // handle disconnection if needed. // on disconnecting, if automatically removed this connection from group. + roomForAll.Remove(ConnectionId); return CompletedTask; } } diff --git a/samples/ChatApp/ChatApp.Shared/Hubs/IChatHubReceiver.cs b/samples/ChatApp/ChatApp.Shared/Hubs/IChatHubReceiver.cs index b8b21eeb0..0bca9910d 100644 --- a/samples/ChatApp/ChatApp.Shared/Hubs/IChatHubReceiver.cs +++ b/samples/ChatApp/ChatApp.Shared/Hubs/IChatHubReceiver.cs @@ -1,4 +1,5 @@ -using ChatApp.Shared.MessagePackObjects; +using System.Threading.Tasks; +using ChatApp.Shared.MessagePackObjects; namespace ChatApp.Shared.Hubs { @@ -12,5 +13,8 @@ public interface IChatHubReceiver void OnLeave(string name); void OnSendMessage(MessageResponse message); + + + Task HelloAsync(string name, int age); } } diff --git a/samples/JwtAuthentication/JwtAuthApp.Server/Hubs/TimerHub.cs b/samples/JwtAuthentication/JwtAuthApp.Server/Hubs/TimerHub.cs index 384465cd7..7fb5a02d1 100644 --- a/samples/JwtAuthentication/JwtAuthApp.Server/Hubs/TimerHub.cs +++ b/samples/JwtAuthentication/JwtAuthApp.Server/Hubs/TimerHub.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Grpc.Core; using JwtAuthApp.Shared; -using MagicOnion.Server; using MagicOnion.Server.Hubs; using Microsoft.AspNetCore.Authorization; @@ -18,7 +17,7 @@ public class TimerHub : StreamingHubBase, ITimerHu private Task _timerLoopTask; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private TimeSpan _interval = TimeSpan.FromSeconds(1); - private IGroup _group; + private IGroup _group; public async Task SetAsync(TimeSpan interval) { @@ -33,7 +32,7 @@ public async Task SetAsync(TimeSpan interval) await Task.Delay(_interval, _cancellationTokenSource.Token); var userPrincipal = Context.CallContext.GetHttpContext().User; - BroadcastToSelf(_group).OnTick($"UserId={userPrincipal.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value}; Name={userPrincipal.Identity?.Name}"); + Client.OnTick($"UserId={userPrincipal.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value}; Name={userPrincipal.Identity?.Name}"); } }); } diff --git a/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MagicOnionStreamingHubInfo.cs b/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MagicOnionStreamingHubInfo.cs index a8a04f9f0..4e09ed00b 100644 --- a/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MagicOnionStreamingHubInfo.cs +++ b/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MagicOnionStreamingHubInfo.cs @@ -77,13 +77,25 @@ public MagicOnionHubMethodInfo(int hubId, string methodName, IReadOnlyList parameters, MagicOnionTypeInfo methodReturnType, MagicOnionTypeInfo requestType, MagicOnionTypeInfo responseType) + : base(hubId, methodName, parameters, methodReturnType, requestType, responseType) + { + IsClientResult = methodReturnType != MagicOnionTypeInfo.KnownTypes.System_Void; + } + } + [DebuggerDisplay("StreamingHubReceiver: {ReceiverType,nq}; Methods={Methods.Count,nq}")] public class MagicOnionStreamingHubReceiverInfo { public MagicOnionTypeInfo ReceiverType { get; } - public IReadOnlyList Methods { get; } + public IReadOnlyList Methods { get; } - public MagicOnionStreamingHubReceiverInfo(MagicOnionTypeInfo receiverType, IReadOnlyList methods) + public MagicOnionStreamingHubReceiverInfo(MagicOnionTypeInfo receiverType, IReadOnlyList methods) { ReceiverType = receiverType; Methods = methods; diff --git a/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MagicOnionTypeInfo.cs b/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MagicOnionTypeInfo.cs index 34da33848..e931feb60 100644 --- a/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MagicOnionTypeInfo.cs +++ b/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MagicOnionTypeInfo.cs @@ -13,6 +13,7 @@ public static class KnownTypes public static MagicOnionTypeInfo System_String { get; } = new MagicOnionTypeInfo("System", "String"); 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_CancellationToken { get; } = new MagicOnionTypeInfo("System.Threading", "CancellationToken", 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); diff --git a/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MethodCollector.cs b/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MethodCollector.cs index e80e4bb0c..55dac16ee 100644 --- a/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MethodCollector.cs +++ b/src/MagicOnion.Client.SourceGenerator/CodeAnalysis/MethodCollector.cs @@ -58,7 +58,7 @@ static IReadOnlyList GetStreamingHubs(MethodCollecto var receiverInterfaceSymbol = x.AllInterfaces.First(y => y.ConstructedFrom.ApproximatelyEqual(ctx.ReferenceSymbols.IStreamingHub)).TypeArguments[1]; var receiverType = ctx.GetOrCreateTypeInfoFromSymbol(receiverInterfaceSymbol); - var receiverMethods = new List(); + var receiverMethods = new List(); foreach (var methodSymbol in GetAllMethods(receiverInterfaceSymbol, ctx.ReferenceSymbols)) { if (TryCreateHubReceiverMethodInfoFromMethodSymbol(ctx, serviceType, receiverType, methodSymbol, out var methodInfo, out var diagnostic)) @@ -122,6 +122,7 @@ static bool TryCreateHubMethodInfoFromMethodSymbol(MethodCollectorContext ctx, M { case "global::System.Threading.Tasks.Task": case "global::System.Threading.Tasks.ValueTask": + case "global::System.Void": //responseType = MagicOnionTypeInfo.KnownTypes.MessagePack_Nil; break; case "global::System.Threading.Tasks.Task<>": @@ -148,14 +149,19 @@ static bool TryCreateHubMethodInfoFromMethodSymbol(MethodCollectorContext ctx, M diagnostic = null; return true; } - static bool TryCreateHubReceiverMethodInfoFromMethodSymbol(MethodCollectorContext ctx, MagicOnionTypeInfo interfaceType, MagicOnionTypeInfo receiverType, IMethodSymbol methodSymbol, [NotNullWhen(true)] out MagicOnionStreamingHubInfo.MagicOnionHubMethodInfo? methodInfo, out Diagnostic? diagnostic) + static bool TryCreateHubReceiverMethodInfoFromMethodSymbol(MethodCollectorContext ctx, MagicOnionTypeInfo interfaceType, MagicOnionTypeInfo receiverType, IMethodSymbol methodSymbol, [NotNullWhen(true)] out MagicOnionStreamingHubInfo.MagicOnionHubReceiverMethodInfo? methodInfo, out Diagnostic? diagnostic) { var hubId = GetHubMethodIdFromMethodSymbol(methodSymbol); var methodReturnType = ctx.GetOrCreateTypeInfoFromSymbol(methodSymbol.ReturnType); var methodParameters = CreateParameterInfoListFromMethodSymbol(ctx, methodSymbol); - var requestType = CreateRequestTypeFromMethodParameters(methodParameters); + var requestType = CreateRequestTypeFromMethodParameters(methodParameters.Where(x => x.Type != MagicOnionTypeInfo.KnownTypes.System_Threading_CancellationToken).ToArray()); var responseType = MagicOnionTypeInfo.KnownTypes.MessagePack_Nil; - if (methodReturnType != MagicOnionTypeInfo.KnownTypes.System_Void) + if (methodReturnType != MagicOnionTypeInfo.KnownTypes.System_Void && + methodReturnType != MagicOnionTypeInfo.KnownTypes.System_Threading_Tasks_Task && + methodReturnType != MagicOnionTypeInfo.KnownTypes.System_Threading_Tasks_ValueTask && + (!methodReturnType.HasGenericArguments || + (methodReturnType.GetGenericTypeDefinition() != MagicOnionTypeInfo.KnownTypes.System_Threading_Tasks_Task && + methodReturnType.GetGenericTypeDefinition() != MagicOnionTypeInfo.KnownTypes.System_Threading_Tasks_ValueTask))) { methodInfo = null; diagnostic = Diagnostic.Create( @@ -164,8 +170,12 @@ static bool TryCreateHubReceiverMethodInfoFromMethodSymbol(MethodCollectorContex $"{receiverType.ToDisplayName(MagicOnionTypeInfo.DisplayNameFormat.Namespace)}.{methodSymbol.Name}", methodReturnType.ToDisplayName(MagicOnionTypeInfo.DisplayNameFormat.Namespace)); return false; } + else if (methodReturnType.HasGenericArguments) + { + responseType = methodReturnType.GenericArguments[0]; + } - methodInfo = new MagicOnionStreamingHubInfo.MagicOnionHubMethodInfo( + methodInfo = new MagicOnionStreamingHubInfo.MagicOnionHubReceiverMethodInfo( hubId, methodSymbol.Name, methodParameters, diff --git a/src/MagicOnion.Client.SourceGenerator/CodeGen/MagicOnionInitializerGenerator.cs b/src/MagicOnion.Client.SourceGenerator/CodeGen/MagicOnionInitializerGenerator.cs index 2bec960b0..de067ffb9 100644 --- a/src/MagicOnion.Client.SourceGenerator/CodeGen/MagicOnionInitializerGenerator.cs +++ b/src/MagicOnion.Client.SourceGenerator/CodeGen/MagicOnionInitializerGenerator.cs @@ -142,7 +142,7 @@ static StreamingHubClientFactoryCache() writer.AppendLineWithFormat($$""" if (typeof(TStreamingHub) == typeof({{hubInfo.ServiceType.FullName}}) && typeof(TReceiver) == typeof({{hubInfo.Receiver.ReceiverType.FullName}})) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate<{{hubInfo.ServiceType.FullName}}, {{hubInfo.Receiver.ReceiverType.FullName}}>)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.{{hubInfo.GetClientFullName()}}(a, b, c, d, e, StreamingHubDiagnosticHandler))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate<{{hubInfo.ServiceType.FullName}}, {{hubInfo.Receiver.ReceiverType.FullName}}>)((a, b, c) => new MagicOnionGeneratedClient.{{hubInfo.GetClientFullName()}}(a, b, c, StreamingHubDiagnosticHandler))); } """); } @@ -151,7 +151,7 @@ static StreamingHubClientFactoryCache() writer.AppendLineWithFormat($$""" if (typeof(TStreamingHub) == typeof({{hubInfo.ServiceType.FullName}}) && typeof(TReceiver) == typeof({{hubInfo.Receiver.ReceiverType.FullName}})) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate<{{hubInfo.ServiceType.FullName}}, {{hubInfo.Receiver.ReceiverType.FullName}}>)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.{{hubInfo.GetClientFullName()}}(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate<{{hubInfo.ServiceType.FullName}}, {{hubInfo.Receiver.ReceiverType.FullName}}>)((a, b, c) => new MagicOnionGeneratedClient.{{hubInfo.GetClientFullName()}}(a, b, c))); } """); } diff --git a/src/MagicOnion.Client.SourceGenerator/CodeGen/StaticStreamingHubClientGenerator.cs b/src/MagicOnion.Client.SourceGenerator/CodeGen/StaticStreamingHubClientGenerator.cs index 410d07b07..6544756eb 100644 --- a/src/MagicOnion.Client.SourceGenerator/CodeGen/StaticStreamingHubClientGenerator.cs +++ b/src/MagicOnion.Client.SourceGenerator/CodeGen/StaticStreamingHubClientGenerator.cs @@ -46,6 +46,7 @@ static void EmitHeader(GenerationContext generationContext, StringBuilder writer #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block + #pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. """); } @@ -107,6 +108,7 @@ public class {{ctx.Hub.GetClientFullName()}} : global::MagicOnion.Client.Streami EmitFireAndForget(ctx); EmitOnBroadcastEvent(ctx); EmitOnResponseEvent(ctx); + EmitOnClientResultEvent(ctx); ctx.Writer.AppendLine(""" } """); @@ -147,8 +149,8 @@ static void EmitConstructor(StreamingHubClientBuildContext ctx) if (ctx.EnableStreamingHubDiagnosticHandler) { ctx.Writer.AppendLineWithFormat($$""" - public {{ctx.Hub.GetClientFullName()}}(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger, global::MagicOnion.Client.IStreamingHubDiagnosticHandler diagnosticHandler) - : base("{{ctx.Hub.ServiceType.Name}}", callInvoker, host, options, serializerProvider, logger) + public {{ctx.Hub.GetClientFullName()}}({{ctx.Hub.Receiver.ReceiverType.FullName}} receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options, global::MagicOnion.Client.IStreamingHubDiagnosticHandler diagnosticHandler) + : base("{{ctx.Hub.ServiceType.Name}}", receiver, callInvoker, options) { this.diagnosticHandler = diagnosticHandler; } @@ -157,8 +159,8 @@ static void EmitConstructor(StreamingHubClientBuildContext ctx) else { ctx.Writer.AppendLineWithFormat($$""" - public {{ctx.Hub.GetClientFullName()}}(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("{{ctx.Hub.ServiceType.Name}}", callInvoker, host, options, serializerProvider, logger) + public {{ctx.Hub.GetClientFullName()}}({{ctx.Hub.Receiver.ReceiverType.FullName}} receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("{{ctx.Hub.ServiceType.Name}}", receiver, callInvoker, options) { } """); @@ -171,7 +173,7 @@ static void EmitFireAndForget(StreamingHubClientBuildContext ctx) ctx.Writer.AppendLineWithFormat($$""" public {{ctx.Hub.ServiceType.FullName}} FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : {{ctx.Hub.ServiceType.FullName}} { @@ -213,17 +215,19 @@ static void EmitHubMethods(StreamingHubClientBuildContext ctx, bool isFireAndFor // new DynamicArgumentTuple(arg1, arg2, ...) _ => $", {method.Parameters.ToNewDynamicArgumentTuple()}", }; + var isReturnTypeVoid = method.MethodReturnType == MagicOnionTypeInfo.KnownTypes.System_Void; + var writeMessageTarget = isFireAndForget ? "parent" : "this"; var writeMessageAsync = ctx.EnableStreamingHubDiagnosticHandler - ? isFireAndForget - ? "parent.WriteMessageFireAndForgetDiagnosticAsync" - : "this.WriteMessageWithResponseDiagnosticAsync" - : isFireAndForget - ? "parent.WriteMessageFireAndForgetAsync" - : "base.WriteMessageWithResponseAsync"; + ? isFireAndForget || isReturnTypeVoid + ? $"{writeMessageTarget}.WriteMessageFireAndForgetDiagnosticAsync" + : $"{writeMessageTarget}.WriteMessageWithResponseDiagnosticAsync" + : isFireAndForget || isReturnTypeVoid + ? $"{writeMessageTarget}.WriteMessageFireAndForgetAsync" + : $"{writeMessageTarget}.WriteMessageWithResponseAsync"; if (isFireAndForget) ctx.Writer.Append(" "); ctx.Writer.AppendLineWithFormat($""" - public {method.MethodReturnType.FullName} {method.MethodName}({method.Parameters.ToMethodSignaturize()}) + public {(isReturnTypeVoid ? "void" : method.MethodReturnType.FullName)} {method.MethodName}({method.Parameters.ToMethodSignaturize()}) """); if (isFireAndForget) ctx.Writer.Append(" "); @@ -256,12 +260,12 @@ static void EmitHubMethods(StreamingHubClientBuildContext ctx, bool isFireAndFor static void EmitOnBroadcastEvent(StreamingHubClientBuildContext ctx) { ctx.Writer.AppendLine(""" - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { """); - foreach (var method in ctx.Hub.Receiver.Methods) + foreach (var method in ctx.Hub.Receiver.Methods.Where(x => x.MethodReturnType == MagicOnionTypeInfo.KnownTypes.System_Void)) { var methodArgs = method.Parameters.Count switch { @@ -270,29 +274,35 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: _ => string.Join(", ", Enumerable.Range(1, method.Parameters.Count).Select(x => $"value.Item{x}")) }; + ctx.Writer.AppendLineWithFormat($$""" + case {{method.HubId}}: // {{method.MethodReturnType.ToDisplayName()}} {{method.MethodName}}({{method.Parameters.ToMethodSignaturize()}}) + { + """); + if (ctx.EnableStreamingHubDiagnosticHandler) { ctx.Writer.AppendLineWithFormat($$""" - case {{method.HubId}}: // {{method.MethodReturnType.ToDisplayName()}} {{method.MethodName}}({{method.Parameters.ToMethodSignaturize()}}) - { var value = base.Deserialize<{{method.RequestType.FullName}}>(data); diagnosticHandler?.OnBroadcastEvent(this, "{{method.MethodName}}", value); receiver.{{method.MethodName}}({{methodArgs}}); - } - break; """); } else { - ctx.Writer.AppendLineWithFormat($$""" - case {{method.HubId}}: // {{method.MethodReturnType.ToDisplayName()}} {{method.MethodName}}({{method.Parameters.ToMethodSignaturize()}}) - { + if (method.Parameters.Count != 0) + { + ctx.Writer.AppendLineWithFormat($$""" var value = base.Deserialize<{{method.RequestType.FullName}}>(data); + """); + } + ctx.Writer.AppendLineWithFormat($$""" receiver.{{method.MethodName}}({{methodArgs}}); + """); + } + ctx.Writer.AppendLine(""" } break; """); - } } ctx.Writer.AppendLine(""" } @@ -304,7 +314,7 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: static void EmitOnResponseEvent(StreamingHubClientBuildContext ctx) { ctx.Writer.AppendLine(""" - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -323,4 +333,59 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S """); } + + static void EmitOnClientResultEvent(StreamingHubClientBuildContext ctx) + { + var clientResultMethods = ctx.Hub.Receiver.Methods.Where(x => x.IsClientResult).ToArray(); + if (clientResultMethods.Length == 0) + { + ctx.Writer.AppendLine(""" + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } + """); + return; + } + + ctx.Writer.AppendLine(""" + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + try + { + switch (methodId) + { + """); + foreach (var method in clientResultMethods) + { + var methodParameters = method.Parameters + .Select((x, i) => (Index: i, IsCancellationToken: x.Type == MagicOnionTypeInfo.KnownTypes.System_Threading_CancellationToken, Type: x.Type)) + .ToArray(); + + var methodArgs = methodParameters.Count(x => !x.IsCancellationToken) switch + { + 0 => string.Join(", ", methodParameters.Select(x => "default")), + 1 => string.Join(", ", methodParameters.Select(x => x.IsCancellationToken ? "default" : "value")), + _ => string.Join(", ", methodParameters.Select(x => x.IsCancellationToken ? "default" : $"value.Item{x.Index + 1}")), + }; + + ctx.Writer.AppendLineWithFormat($$""" + case {{method.HubId}}: // {{method.MethodReturnType.ToDisplayName()}} {{method.MethodName}}({{method.Parameters.ToMethodSignaturize()}}) + { + var value = base.Deserialize<{{method.RequestType.FullName}}>(data); + base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, receiver.{{method.MethodName}}({{methodArgs}})); + } + break; + """); + } + ctx.Writer.AppendLine(""" + } + } + catch (global::System.Exception ex) + { + base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + } + } + + """); + } } diff --git a/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels.meta b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels.meta new file mode 100644 index 000000000..5d3dc9bf0 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6f8d1cd17d285aa40aba05cb8861efa2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0.meta b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0.meta new file mode 100644 index 000000000..fa72750dc --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3fae5b6755585dc4c941b234a9e7e82b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/LICENSE.TXT b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/LICENSE.TXT new file mode 100644 index 000000000..984713a49 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/LICENSE.TXT @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/LICENSE.TXT.meta b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/LICENSE.TXT.meta new file mode 100644 index 000000000..9d58eba11 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/LICENSE.TXT.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d91ceaa4a725879459c4908f7eb8a4f9 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib.meta b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib.meta new file mode 100644 index 000000000..b7c3424bb --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d49c9170d8551e346b86aafb360e2fd1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1.meta b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1.meta new file mode 100644 index 000000000..ccc02371c --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bc8731f0410c3b1458d9b44a697880d2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.dll b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.dll new file mode 100644 index 000000000..60b45fba1 Binary files /dev/null and b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.dll differ diff --git a/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.dll.meta b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.dll.meta new file mode 100644 index 000000000..60afb28db --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.dll.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 178d59b1c5291b64d9a206e568e79b9b +labels: +- NuGetForUnity +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.xml b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.xml new file mode 100644 index 000000000..20275dc30 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.xml @@ -0,0 +1,243 @@ + + + + System.Threading.Channels + + + + Specifies the behavior to use when writing to a bounded channel that is already full. + + + Removes and ignores the newest item in the channel in order to make room for the item being written. + + + Removes and ignores the oldest item in the channel in order to make room for the item being written. + + + Drops the item being written. + + + Waits for space to be available in order to complete the write operation. + + + Provides options that control the behavior of bounded instances. + + + Initializes the options. + The maximum number of items the bounded channel may store. + + + Gets or sets the maximum number of items the bounded channel may store. + + + Gets or sets the behavior incurred by write operations when the channel is full. + + + Provides static methods for creating channels. + + + Creates a channel with the specified maximum capacity. + The maximum number of items the channel may store. + Specifies the type of data in the channel. + The created channel. + + + Creates a channel with the specified maximum capacity. + Options that guide the behavior of the channel. + Specifies the type of data in the channel. + The created channel. + + + Creates a channel subject to the provided options. + Options that guide the behavior of the channel. + Delegate that will be called when item is being dropped from channel. See . + Specifies the type of data in the channel. + The created channel. + + + Creates an unbounded channel usable by any number of readers and writers concurrently. + The type of data in the channel. + The created channel. + + + Creates an unbounded channel subject to the provided options. + Options that guide the behavior of the channel. + Specifies the type of data in the channel. + The created channel. + + + Provides a base class for channels that support reading and writing elements of type . + Specifies the type of data readable and writable in the channel. + + + Initializes an instance of the class. + + + Provides a base class for channels that support reading elements of type and writing elements of type . + Specifies the type of data that may be written to the channel. + Specifies the type of data that may be read from the channel. + + + Initializes an instance of the class. + + + Implicit cast from a to its readable half. + The being cast. + The readable half. + + + Implicit cast from a to its writable half. + The being cast. + The writable half. + + + Gets the readable half of this channel. + + + Gets the writable half of this channel. + + + Exception thrown when a channel is used after it's been closed. + + + Initializes a new instance of the class. + + + Initializes a new instance of the class. + The exception that is the cause of this exception. + + + Initializes a new instance of the class with serialized data. + The object that holds the serialized object data. + The contextual information about the source or destination. + + + Initializes a new instance of the class. + The message that describes the error. + + + Initializes a new instance of the class. + The message that describes the error. + The exception that is the cause of this exception. + + + Provides options that control the behavior of channel instances. + + + Initializes an instance of the class. + + + + if operations performed on a channel may synchronously invoke continuations subscribed to + notifications of pending async operations; if all continuations should be invoked asynchronously. + + + + readers from the channel guarantee that there will only ever be at most one read operation at a time; + if no such constraint is guaranteed. + + + + if writers to the channel guarantee that there will only ever be at most one write operation + at a time; if no such constraint is guaranteed. + + + Provides a base class for reading from a channel. + Specifies the type of data that may be read from the channel. + + + Initializes an instance of the class. + + + Creates an that enables reading all of the data from the channel. + The cancellation token to use to cancel the enumeration. If data is immediately ready for reading, then that data may be yielded even after cancellation has been requested. + The created async enumerable. + + + Asynchronously reads an item from the channel. + A used to cancel the read operation. + A that represents the asynchronous read operation. + + + Attempts to peek at an item from the channel. + The peeked item, or a default value if no item could be peeked. + + if an item was read; otherwise, . + + + Attempts to read an item from the channel. + The read item, or a default value if no item could be read. + + if an item was read; otherwise, . + + + Returns a that will complete when data is available to read. + A used to cancel the wait operation. + + A that will complete with a result when data is available to read + or with a result when no further data will ever be available to be read due to the channel completing successfully. + If the channel completes with an exception, the task will also complete with an exception. + + + + Gets a value that indicates whether is available for use on this instance. + + + Gets a value that indicates whether is available for use on this instance. + + if peeking is supported by this channel instance; otherwise. + + + Gets a that completes when no more data will ever + be available to be read from this channel. + + + Gets the current number of items available from this channel reader. + Counting is not supported on this instance. + + + Provides a base class for writing to a channel. + Specifies the type of data that may be written to the channel. + + + Initializes an instance of the class. + + + Mark the channel as being complete, meaning no more items will be written to it. + Optional Exception indicating a failure that's causing the channel to complete. + The channel has already been marked as complete. + + + Attempts to mark the channel as being completed, meaning no more data will be written to it. + An indicating the failure causing no more data to be written, or null for success. + + if this operation successfully completes the channel; otherwise, if the channel could not be marked for completion, + for example due to having already been marked as such, or due to not supporting completion. + . + + + Attempts to write the specified item to the channel. + The item to write. + + if the item was written; otherwise, . + + + Returns a that will complete when space is available to write an item. + A used to cancel the wait operation. + A that will complete with a result when space is available to write an item + or with a result when no further writing will be permitted. + + + Asynchronously writes an item to the channel. + The value to write to the channel. + A used to cancel the write operation. + A that represents the asynchronous write operation. + + + Provides options that control the behavior of unbounded instances. + + + Initializes a new instance of the class. + + + \ No newline at end of file diff --git a/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.xml.meta b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.xml.meta new file mode 100644 index 000000000..17d99b9e3 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Plugins/System.Threading.Channels/8.0.0/lib/netstandard2.1/System.Threading.Channels.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3a999c3e0c15371408a73b75c174eeac +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientBuilder.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientBuilder.cs index ab907ade1..8db75bc3b 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientBuilder.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientBuilder.cs @@ -49,6 +49,7 @@ public static AssemblyBuilder Save() internal #endif static class DynamicStreamingHubClientBuilder + where TStreamingHub : IStreamingHub { public static readonly Type ClientType; // static readonly Type ClientFireAndForgetType; @@ -133,9 +134,10 @@ static void VerifyMethodDefinitions(MethodDefinition[] definitions) if (returnTypeNonGenericOrOpenGeneric != typeof(ValueTask) && returnTypeNonGenericOrOpenGeneric != typeof(Task) && returnTypeNonGenericOrOpenGeneric != typeof(ValueTask<>) && - returnTypeNonGenericOrOpenGeneric != typeof(Task<>)) + returnTypeNonGenericOrOpenGeneric != typeof(Task<>) && + returnTypeNonGenericOrOpenGeneric != typeof(void)) { - throw new Exception($"Invalid definition, TStreamingHub's return type must only be `Task`, `Task`, `ValueTask` or `ValueTask`. {item.MethodInfo.Name}."); + throw new Exception($"Invalid definition, TStreamingHub's return type must only be `void`, `Task`, `Task`, `ValueTask` or `ValueTask`. {item.MethodInfo.Name}."); } item.MethodId = methodId; @@ -148,20 +150,18 @@ static void VerifyMethodDefinitions(MethodDefinition[] definitions) static FieldInfo DefineConstructor(TypeBuilder typeBuilder, Type interfaceType, Type receiverType, ConstructorInfo fireAndForgetClientCtor) { - // .ctor(CallInvoker callInvoker, string host, CallOptions option, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger) :base(...) + // .ctor(TReceiver receiver, CallInvoker callInvoker, StreamingHubClientOptions options) : base("InterfaceName", receiver, callInvoker, options) { - var argTypes = new[] { typeof(CallInvoker), typeof(string), typeof(CallOptions), typeof(IMagicOnionSerializerProvider), typeof(IMagicOnionClientLogger) }; + var argTypes = new[] { receiverType, typeof(CallInvoker), typeof(StreamingHubClientOptions) }; var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, argTypes); var il = ctor.GetILGenerator(); - // base(serviceName, callInvoker, host, option, serializerProvider, logger); + // base("InterfaceName", receiver, callInvoker, options); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldstr, interfaceType.Name); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Ldarg_3); - il.Emit(OpCodes.Ldarg_S, (byte)4); - il.Emit(OpCodes.Ldarg_S, (byte)5); + il.Emit(OpCodes.Ldarg_1); // receiver + il.Emit(OpCodes.Ldarg_2); // callInvoker + il.Emit(OpCodes.Ldarg_3); // options il.Emit(OpCodes.Call, typeBuilder.BaseType! .GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).First()); @@ -240,13 +240,14 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece // receiver types borrow from DynamicBroadcastBuilder { - // protected abstract void OnResponseEvent(int methodId, object taskCompletionSource, ArraySegment data); + // protected abstract void OnResponseEvent(int methodId, object taskCompletionSource, ReadOnlyMemory data); { var method = typeBuilder.DefineMethod("OnResponseEvent", MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual, - null, new[] { typeof(int), typeof(object), typeof(ArraySegment) }); + null, new[] { typeof(int), typeof(object), typeof(ReadOnlyMemory) }); var il = method.GetILGenerator(); var labels = definitions + .Where(x => x.MethodInfo.ReturnType != typeof(void)) // If the return type if `void`, we always need to treat as fire-and-forget. .Select(x => new { def = x, label = il.DefineLabel() }) .ToArray(); @@ -264,6 +265,7 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece { // SetResultForResponse(taskCompletionSource, data); Type responseType; + if (item.def.MethodInfo.ReturnType == typeof(Task) || item.def.MethodInfo.ReturnType == typeof(ValueTask)) { // Task methods uses TaskCompletionSource @@ -285,13 +287,13 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece il.Emit(OpCodes.Ret); } } - // protected abstract void OnBroadcastEvent(int methodId, ArraySegment data); + // protected abstract void OnBroadcastEvent(int methodId, ReadOnlyMemory data); { var methodDefinitions = BroadcasterHelper.SearchDefinitions(receiverType); BroadcasterHelper.VerifyMethodDefinitions(methodDefinitions); var method = typeBuilder.DefineMethod("OnBroadcastEvent", MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual, - typeof(void), new[] { typeof(int), typeof(ArraySegment) }); + typeof(void), new[] { typeof(int), typeof(ReadOnlyMemory) }); var il = method.GetILGenerator(); var labels = methodDefinitions @@ -314,7 +316,6 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece // var value = Deserialize>(data); // receiver.OnMessage(value.Item1, value.Item2); - var deserializeMethod = baseType.GetMethod("Deserialize", BindingFlags.Instance | BindingFlags.NonPublic)!; var parameters = item.def.MethodInfo.GetParameters(); if (parameters.Length == 0) { @@ -332,7 +333,7 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece // this.Deserialize(data) il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldarg_2); // data - il.Emit(OpCodes.Call, deserializeMethod.MakeGenericMethod(parameters[0].ParameterType)); + il.Emit(OpCodes.Call, MethodInfoCache.StreamingHubClientBase_Deserialize.MakeGenericMethod(parameters[0].ParameterType)); } il.Emit(OpCodes.Callvirt, item.def.MethodInfo); } @@ -346,7 +347,7 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece { il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldarg_2); // data - il.Emit(OpCodes.Call, deserializeMethod.MakeGenericMethod(deserializeType)); + il.Emit(OpCodes.Call, MethodInfoCache.StreamingHubClientBase_Deserialize.MakeGenericMethod(deserializeType)); il.Emit(OpCodes.Stloc, lc); } @@ -365,6 +366,162 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece } } } + // protected abstract void OnClientResultEvent(int methodId, Guid messageId, ReadOnlyMemory data); + { + var methodDefinitions = BroadcasterHelper.SearchDefinitions(receiverType); + BroadcasterHelper.VerifyMethodDefinitions(methodDefinitions); + + var method = typeBuilder.DefineMethod("OnClientResultEvent", MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual, + typeof(void), new[] { typeof(int), typeof(Guid), typeof(ReadOnlyMemory) }); + { + var il = method.GetILGenerator(); + var localEx = il.DeclareLocal(typeof(Exception)); + + var clientResultMethods = methodDefinitions.Where(x => x.IsClientResult) + .Select(x => (Label: il.DefineLabel(), Method: x)).ToArray(); + var labelReturn = il.DefineLabel(); + + var localCtDefault = il.DeclareLocal(typeof(CancellationToken)); + { + // var ctDefault = default(CancellationToken); + il.Emit(OpCodes.Ldloca_S, localCtDefault!); + il.Emit(OpCodes.Initobj, typeof(CancellationToken)); + } + + + // try { + il.BeginExceptionBlock(); + foreach (var (labelMethodBlock, methodClientResult) in clientResultMethods) + { + // if (methodId == ...) goto label; + il.Emit(OpCodes.Ldarg_1); // methodId + il.Emit(OpCodes.Ldc_I4, methodClientResult.MethodId); + il.Emit(OpCodes.Beq, labelMethodBlock); + } + // goto Return; + il.Emit(OpCodes.Leave, labelReturn); + + foreach (var (labelMethodBlock, methodClientResult) in clientResultMethods) + { + var parameters = methodClientResult.MethodInfo.GetParameters().Where(x => x.ParameterType != typeof(CancellationToken)).ToArray(); + var deserializeType = parameters.Length switch + { + 0 => typeof(MessagePack.Nil), + 1 => parameters[0].ParameterType, + _ => BroadcasterHelper.DynamicArgumentTupleTypes[parameters.Length - 2].MakeGenericType(parameters.Select(x => x.ParameterType).ToArray()) + }; + + // Method: + // { + // var local_0 = base.Deserialize(data); + // var task = this.SomeMethod(local0.Item1, local0.Item2); + // base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, localTask); + // return; + // } + // break; + + // Method: + il.MarkLabel(labelMethodBlock); + // var local0 = base.Deserialize(data); + il.Emit(OpCodes.Ldarg_0); // base + il.Emit(OpCodes.Ldarg_3); // data + il.Emit(OpCodes.Call, MethodInfoCache.StreamingHubClientBase_Deserialize.MakeGenericMethod(deserializeType)); + var local0 = il.DeclareLocal(deserializeType); + il.Emit(OpCodes.Stloc_S, local0); + + // var task = receiver.SomeMethod(local0.Item1, local0.Item2); + il.Emit(OpCodes.Ldarg_0); // receiver + il.Emit(OpCodes.Ldfld, receiverField); + + if (parameters.Length == 0) + { + foreach (var p in methodClientResult.MethodInfo.GetParameters()) + { + // default(CancellationToken) + il.Emit(OpCodes.Ldloca_S, localCtDefault!); + il.Emit(OpCodes.Initobj, typeof(CancellationToken)); + il.Emit(OpCodes.Ldloc_S, localCtDefault!); + } + } + else if (parameters.Length == 1) + { + foreach (var p in methodClientResult.MethodInfo.GetParameters()) + { + if (p.ParameterType == typeof(CancellationToken)) + { + // default(CancellationToken) + il.Emit(OpCodes.Ldloca_S, localCtDefault!); + il.Emit(OpCodes.Initobj, typeof(CancellationToken)); + il.Emit(OpCodes.Ldloc_S, localCtDefault!); + } + else if (p == parameters[0]) + { + // local0 + il.Emit(OpCodes.Ldloc_S, local0); + } + else + { + throw new InvalidOperationException(); + } + } + } + else + { + var itemIndex = 1; + foreach (var p in methodClientResult.MethodInfo.GetParameters()) + { + if (p.ParameterType == typeof(CancellationToken)) + { + // default(CancellationToken) + il.Emit(OpCodes.Ldloca_S, localCtDefault!); + il.Emit(OpCodes.Initobj, typeof(CancellationToken)); + il.Emit(OpCodes.Ldloc_S, localCtDefault!); + } + else + { + // local0.ItemX + il.Emit(OpCodes.Ldloc_S, local0); + il.Emit(OpCodes.Ldfld, deserializeType.GetField($"Item{itemIndex}")!); + itemIndex++; + } + } + } + il.Emit(OpCodes.Callvirt, methodClientResult.MethodInfo); + + // var localTask = task; + var localTask = il.DeclareLocal(methodClientResult.MethodInfo.ReturnType); + il.Emit(OpCodes.Stloc_S, localTask); + + // base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, localTask); + il.Emit(OpCodes.Ldarg_0); // base + il.Emit(OpCodes.Ldarg_1); // methodId + il.Emit(OpCodes.Ldarg_2); // messageId + il.Emit(OpCodes.Ldloc_S, localTask); + il.Emit(OpCodes.Call, MethodInfoCache.GetStreamingHubClientBase_AwaitAndWriteClientResultResponseMessage(methodClientResult.MethodInfo.ReturnType)); + + il.Emit(OpCodes.Leave, labelReturn); + } + // } catch (Exception ex) { + il.BeginCatchBlock(typeof(Exception)); + il.Emit(OpCodes.Stloc_S, localEx); + { + // base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + il.Emit(OpCodes.Ldarg_0); // base + il.Emit(OpCodes.Ldarg_1); // methodId + il.Emit(OpCodes.Ldarg_2); // messageId + il.Emit(OpCodes.Ldloc_S, localEx); // ex + il.Emit(OpCodes.Call, MethodInfoCache.StreamingHubClientBase_WriteClientResultResponseMessageForError); + il.Emit(OpCodes.Leave, labelReturn); + } + il.EndExceptionBlock(); + // } + + // Return: + // return; + il.MarkLabel(labelReturn); + il.Emit(OpCodes.Ret); + } + } // Proxy Methods for (int i = 0; i < definitions.Length; i++) @@ -413,13 +570,15 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece if (def.MethodInfo.ReturnType == typeof(Task) || def.MethodInfo.ReturnType == typeof(ValueTask)) { - var mInfo = baseType.GetMethod("WriteMessageWithResponseAsync", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!; - il.Emit(OpCodes.Callvirt, mInfo.MakeGenericMethod(callType, typeof(Nil))); + il.Emit(OpCodes.Callvirt, MethodInfoCache.StreamingHubClientBase_WriteMessageWithResponseAsync.MakeGenericMethod(callType, typeof(Nil))); + } + else if (def.MethodInfo.ReturnType == typeof(void)) + { + il.Emit(OpCodes.Callvirt, MethodInfoCache.StreamingHubClientBase_WriteMessageFireAndForgetAsync.MakeGenericMethod(callType, typeof(Nil))); } else { - var mInfo = baseType.GetMethod("WriteMessageWithResponseAsync", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!; - il.Emit(OpCodes.Callvirt, mInfo.MakeGenericMethod(callType, def.MethodInfo.ReturnType.GetGenericArguments()[0])); + il.Emit(OpCodes.Callvirt, MethodInfoCache.StreamingHubClientBase_WriteMessageWithResponseAsync.MakeGenericMethod(callType, def.MethodInfo.ReturnType.GetGenericArguments()[0])); } // If the return type is `ValueTask`, the task must be wrapped as ValueTask. @@ -432,6 +591,10 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece var returnTypeOfT = def.MethodInfo.ReturnType.GetGenericArguments()[0]; il.Emit(OpCodes.Newobj, typeof(ValueTask<>).MakeGenericType(returnTypeOfT).GetConstructor(new [] { typeof(Task<>).MakeGenericType(returnTypeOfT) })!); } + else if (def.MethodInfo.ReturnType == typeof(void)) + { + il.Emit(OpCodes.Pop); + } il.Emit(OpCodes.Ret); } @@ -515,7 +678,7 @@ static void DefineMethodsFireAndForget(TypeBuilder typeBuilder, Type interfaceTy } Type responseType; - if (def.MethodInfo.ReturnType == typeof(Task) || def.MethodInfo.ReturnType == typeof(ValueTask)) + if (def.MethodInfo.ReturnType == typeof(Task) || def.MethodInfo.ReturnType == typeof(ValueTask) || def.MethodInfo.ReturnType == typeof(void)) { responseType = typeof(Nil); } @@ -538,11 +701,58 @@ static void DefineMethodsFireAndForget(TypeBuilder typeBuilder, Type interfaceTy var returnTypeOfT = def.MethodInfo.ReturnType.GetGenericArguments()[0]; il.Emit(OpCodes.Newobj, typeof(ValueTask<>).MakeGenericType(returnTypeOfT).GetConstructor(new [] { typeof(Task<>).MakeGenericType(returnTypeOfT) })!); } + else if (def.MethodInfo.ReturnType == typeof(void)) + { + il.Emit(OpCodes.Pop); + } il.Emit(OpCodes.Ret); } } + static class MethodInfoCache + { + // ReSharper disable StaticMemberInGenericType + // ReSharper disable InconsistentNaming +#pragma warning disable IDE1006 // Naming Styles + public static readonly MethodInfo StreamingHubClientBase_Deserialize + = typeof(StreamingHubClientBase).GetMethod("Deserialize", BindingFlags.Instance | BindingFlags.NonPublic)!; + + static readonly MethodInfo StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_Task; + static readonly MethodInfo StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_TaskOfT; + static readonly MethodInfo StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTask; + static readonly MethodInfo StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTaskOfT; + + public static MethodInfo GetStreamingHubClientBase_AwaitAndWriteClientResultResponseMessage(Type t) + => t == typeof(Task) ? StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_Task + : t == typeof(ValueTask) ? StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTask + : t.GetGenericTypeDefinition() == typeof(Task<>) ? StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_TaskOfT.MakeGenericMethod(t.GetGenericArguments()) + : t.GetGenericTypeDefinition() == typeof(ValueTask<>) ? StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTaskOfT.MakeGenericMethod(t.GetGenericArguments()) + : throw new InvalidOperationException(); + + public static readonly MethodInfo StreamingHubClientBase_WriteClientResultResponseMessageForError + = typeof(StreamingHubClientBase).GetMethod("WriteClientResultResponseMessageForError", BindingFlags.NonPublic | BindingFlags.Instance)!; + public static readonly MethodInfo StreamingHubClientBase_WriteMessageWithResponseAsync + = typeof(StreamingHubClientBase).GetMethod("WriteMessageWithResponseAsync", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!; + public static readonly MethodInfo StreamingHubClientBase_WriteMessageFireAndForgetAsync + = typeof(StreamingHubClientBase).GetMethod("WriteMessageFireAndForgetAsync", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!; + // ReSharper restore StaticMemberInGenericType + // ReSharper restore InconsistentNaming +#pragma warning restore IDE1006 // Naming Styles + + static MethodInfoCache() + { + var methodsAwaitAndWriteClientResultResponseMessage = typeof(StreamingHubClientBase) + .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(x => x.Name == "AwaitAndWriteClientResultResponseMessage") + .ToArray(); + + StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_Task = methodsAwaitAndWriteClientResultResponseMessage.Single(x => x.GetParameters()[2].ParameterType == typeof(Task)); + StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_TaskOfT = methodsAwaitAndWriteClientResultResponseMessage.Single(x => x.GetParameters()[2].ParameterType is { IsGenericType: true } paramType && paramType.GetGenericTypeDefinition() == typeof(Task<>)); + StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTask = methodsAwaitAndWriteClientResultResponseMessage.Single(x => x.GetParameters()[2].ParameterType == typeof(ValueTask)); + StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTaskOfT = methodsAwaitAndWriteClientResultResponseMessage.Single(x => x.GetParameters()[2].ParameterType is { IsGenericType: true } paramType && paramType.GetGenericTypeDefinition() == typeof(ValueTask<>)); + } + } class MethodDefinition { diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientFactoryProvider.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientFactoryProvider.cs index 59256a5a0..6f82872fa 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientFactoryProvider.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientFactoryProvider.cs @@ -31,8 +31,7 @@ public bool TryGetFactory([NotNullWhen(true)] out Stre static class Cache where TStreamingHub : IStreamingHub { public static readonly StreamingHubClientFactoryDelegate Factory - = (callInvoker, receiver, host, callOptions, serializerProvider, logger) - => (TStreamingHub)Activator.CreateInstance(DynamicStreamingHubClientBuilder.ClientType, callInvoker, host, callOptions, serializerProvider, logger)!; + = (receiver, callInvoker, options) => (TStreamingHub)Activator.CreateInstance(DynamicStreamingHubClientBuilder.ClientType, receiver, callInvoker, options)!; } } #endif diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/BroadcasterHelper.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/BroadcasterHelper.cs index ceaa5b6f8..eab0c5c9d 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/BroadcasterHelper.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/BroadcasterHelper.cs @@ -58,10 +58,10 @@ internal static void VerifyMethodDefinitions(MethodDefinition[] definitions) } map.Add(methodId, item); - if (!(item.MethodInfo.ReturnType == typeof(void))) - { - throw new Exception($"Invalid definition, TReceiver's return type must only be `void`. {item.MethodInfo.Name}."); - } + //if (!(item.MethodInfo.ReturnType == typeof(void))) + //{ + // throw new Exception($"Invalid definition, TReceiver's return type must only be `void`. {item.MethodInfo.Name}."); + //} item.MethodId = methodId; } @@ -74,12 +74,14 @@ internal class MethodDefinition public Type ReceiverType { get; set; } public MethodInfo MethodInfo { get; set; } public int MethodId { get; set; } + public bool IsClientResult { get; set; } public MethodDefinition(Type receiverType, MethodInfo methodInfo, int methodId) { ReceiverType = receiverType; MethodInfo = methodInfo; MethodId = methodId; + IsClientResult = methodInfo.ReturnType != typeof(void); } } } diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/Buffers/ArrayPoolBufferWriter.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/Buffers/ArrayPoolBufferWriter.cs index db49478bb..8c5806321 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/Buffers/ArrayPoolBufferWriter.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/Buffers/ArrayPoolBufferWriter.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Diagnostics; namespace MagicOnion.Internal.Buffers { @@ -15,7 +16,14 @@ public static ArrayPoolBufferWriter RentThreadStaticWriter() staticInstance = new ArrayPoolBufferWriter(); } staticInstance.Prepare(); + +#if DEBUG + var currentInstance = staticInstance; + staticInstance = null; + return currentInstance; +#else return staticInstance; +#endif } const int MinimumBufferSize = 32767; // use 32k buffer. @@ -96,6 +104,11 @@ public void Dispose() ArrayPool.Shared.Return(buffer); buffer = null; + +#if DEBUG + Debug.Assert(staticInstance is null); + staticInstance = this; +#endif } } } diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/Buffers/MemoryPoolBufferWriter.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/Buffers/MemoryPoolBufferWriter.cs new file mode 100644 index 000000000..2666b3aba --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/Buffers/MemoryPoolBufferWriter.cs @@ -0,0 +1,69 @@ +using System; +using System.Buffers; + +namespace MagicOnion.Internal.Buffers +{ + public class MemoryPoolBufferWriter : IBufferWriter + { + readonly MemoryPool memoryPool; + IMemoryOwner? buffer; + int written; + + [ThreadStatic] + static MemoryPoolBufferWriter? shared; + public static MemoryPoolBufferWriter RentThreadStaticWriter() => shared ??= new MemoryPoolBufferWriter(MemoryPool.Shared); + + public MemoryPoolBufferWriter(MemoryPool memoryPool) + { + this.memoryPool = memoryPool; + this.buffer = null; + this.written = 0; + } + + public void Advance(int count) + { + written += count; + } + + public Memory GetMemory(int sizeHint = 0) + { + if (buffer != null && (buffer.Memory.Length - written) > sizeHint) + { + return buffer.Memory.Slice(written); + } + else + { + if (buffer == null) + { + // New + buffer = memoryPool.Rent(sizeHint > 0 ? sizeHint : 32767); + } + else + { + // Grow + var oldBuffer = buffer; + var newBuffer = memoryPool.Rent(buffer.Memory.Length * 2); + + oldBuffer.Memory.Slice(0, written).CopyTo(newBuffer.Memory); + oldBuffer.Dispose(); + + buffer = newBuffer; + } + return buffer.Memory.Slice(written); + } + } + + public Span GetSpan(int sizeHint = 0) + { + return GetMemory(sizeHint).Span; + } + + public (IMemoryOwner Owner, int Written) ToMemoryOwnerAndReturn() + { + var result = (buffer ?? memoryPool.Rent(0), written); + written = 0; + buffer = null; + return result; + } + } +} diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/Buffers/MemoryPoolBufferWriter.cs.meta b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/Buffers/MemoryPoolBufferWriter.cs.meta new file mode 100644 index 000000000..fa5ec2709 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/Buffers/MemoryPoolBufferWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c58e159a2bd5bed469aa95ac4ef89613 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/MagicOnionMarshallers.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/MagicOnionMarshallers.cs index 6e93ee430..af9233361 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/MagicOnionMarshallers.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/MagicOnionMarshallers.cs @@ -1,6 +1,7 @@ using Grpc.Core; using MessagePack; using System; +using System.Buffers; using System.Linq; using System.Reflection; @@ -15,7 +16,21 @@ internal static class MagicOnionMarshallers .OrderBy(x => x.GetGenericArguments().Length) .ToArray(); - public static readonly Marshaller ThroughMarshaller = new Marshaller(x => x, x => x); + internal static Marshaller StreamingHubMarshaller { get; } = new( + serializer: static (payload, context) => + { + context.SetPayloadLength(payload.Length); + var bufferWriter = context.GetBufferWriter(); + payload.Span.CopyTo(bufferWriter.GetSpan(payload.Length)); + bufferWriter.Advance(payload.Length); + context.Complete(); + StreamingHubPayloadPool.Shared.Return(payload); + }, + deserializer: static context => + { + return StreamingHubPayloadPool.Shared.RentOrCreate(context.PayloadAsReadOnlySequence()); + } + ); internal static Type CreateRequestType(ParameterInfo[] parameters) { diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs new file mode 100644 index 000000000..247caec1f --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs @@ -0,0 +1,86 @@ +using System; +using MessagePack; + +namespace MagicOnion.Internal +{ + internal ref struct StreamingHubClientMessageReader + { + readonly ReadOnlyMemory data; + MessagePackReader reader; + + public StreamingHubClientMessageReader(ReadOnlyMemory data) + { + this.data = data; + this.reader = new MessagePackReader(data); + } + + public StreamingHubMessageType ReadMessageType() + { + var arrayLength = this.reader.ReadArrayHeader(); + return arrayLength switch + { + 2 => StreamingHubMessageType.Broadcast, + 3 => StreamingHubMessageType.Response, + 4 => StreamingHubMessageType.ResponseWithError, + 5 => reader.ReadByte() switch + { + 0x00 /* 0:ClientResultRequest */ => StreamingHubMessageType.ClientResultRequest, + 0x7f /* 127:Heartbeat */ => StreamingHubMessageType.Heartbeat, + var x => throw new InvalidOperationException($"Unknown Type: {x}"), + }, + _ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"), + }; + } + + public (int MethodId, int Cosumed) ReadBroadcastMessageMethodId() + { + return (reader.ReadInt32(), (int)reader.Consumed); + } + + public (int MethodId, ReadOnlyMemory Body) ReadBroadcastMessage() + { + var methodId = reader.ReadInt32(); + var offset = (int)reader.Consumed; + return (methodId, data.Slice(offset)); + } + + public (int MessageId, int MethodId, ReadOnlyMemory Body) ReadResponseMessage() + { + var messageId = reader.ReadInt32(); + var methodId = reader.ReadInt32(); + var offset = (int)reader.Consumed; + return (messageId, methodId, data.Slice(offset)); + } + + public (int MessageId, int StatusCode, string? Detail, string? Error) ReadResponseWithErrorMessage() + { + var messageId = reader.ReadInt32(); + var statusCode = reader.ReadInt32(); + var detail = reader.ReadString(); + var error = reader.ReadString(); + + return (messageId, statusCode, detail, error); + } + + public (Guid ClientResultRequestMessageId, int MethodId, ReadOnlyMemory Body) ReadClientResultRequestMessage() + { + //var type = reader.ReadByte(); // Type is already read by ReadMessageType + reader.Skip(); // Dummy + var clientRequestMessageId = MessagePackSerializer.Deserialize(ref reader); + var methodId = reader.ReadInt32(); + var offset = (int)reader.Consumed; + + return (clientRequestMessageId, methodId, data.Slice(offset)); + } + + public ReadOnlyMemory ReadHeartbeat() + { + //var type = reader.ReadByte(); // Type is already read by ReadMessageType + reader.Skip(); // Dummy (1) + reader.Skip(); // Dummy (2) + reader.Skip(); // Dummy (3) + + return data.Slice((int)reader.Consumed); + } + } +} diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs.meta b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs.meta new file mode 100644 index 000000000..3f9896648 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 84187af9e9bbd9b47ad00a2e646a3862 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs new file mode 100644 index 000000000..93832281e --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs @@ -0,0 +1,250 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using MagicOnion.Serialization; +using MessagePack; + +namespace MagicOnion.Internal +{ + /// + /// StreamingHub message formats (from Server to Client): + /// + /// + /// Response: InvokeHubMethod (from server to client) + /// Array(3): [MessageId(int), MethodId(int), SerializedResponse] + /// + /// + /// Response: InvokeHubMethod (from server to client; with Exception) + /// Array(4): [MessageId(int), StatusCode(int), Detail(string), Message(string)] + /// + /// + /// Broadcast: from server to client + /// Array(2): [MethodId(int), SerializedArgument] + /// + /// + /// ClientInvoke/Request: InvokeClientMethod (from server to client) + /// Array(5): [Type=0x00, Nil, ClientResultMessageId(Guid), MethodId(int), SerializedArguments] + /// + /// + /// Heartbeat: + /// Array(5): [Type=0x7f, Nil, Nil, Nil, Extras] + /// + /// + /// StreamingHub message formats (from Client to Server): + /// + /// + /// Request: InvokeHubMethod (from client; void; fire-and-forget) + /// Array(2): [MethodId(int), SerializedArguments] + /// + /// + /// Request: InvokeHubMethod (from client; non-void) + /// Array(3): [MessageId(int), MethodId(int), SerializedArguments] + /// + /// + /// ClientInvoke/Response: InvokeClientMethod (from client to server) + /// Array(4): [Type=0x00, ClientResultMessageId(Guid), MethodId(int), SerializedResponse] + /// + /// + /// ClientInvoke/Response: InvokeClientMethod (from client to server; with Exception) + /// Array(4): [Type=0x01, ClientResultMessageId(Guid), MethodId(int), [StatusCode(int), Detail(string), Message(string)]] + /// + /// + /// Heartbeat/Response: + /// Array(4): [Type=0x7f, Nil, Nil, Nil] + /// + /// + /// + internal static class StreamingHubMessageWriter + { + /// + /// Writes a broadcast message of Hub method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteBroadcastMessage(IBufferWriter bufferWriter, int methodId, T value, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(2); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, value); + } + + /// + /// Writes a request message of Hub method. + /// + public static void WriteRequestMessageVoid(IBufferWriter bufferWriter, int methodId, T value, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(2); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, value); + } + + /// + /// Writes a request message of Hub method. + /// + public static void WriteRequestMessage(IBufferWriter bufferWriter, int methodId, int messageId, T value, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(3); + writer.Write(messageId); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, value); + } + + /// + /// Writes an empty response message of Hub method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteResponseMessage(IBufferWriter bufferWriter, int methodId, int messageId) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(3); + writer.Write(messageId); + writer.Write(methodId); + writer.WriteNil(); + writer.Flush(); + } + + /// + /// Writes a response message of Hub method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteResponseMessage(IBufferWriter bufferWriter, int methodId, int messageId, T v, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(3); + writer.Write(messageId); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, v); + } + + /// + /// Write an error response message of Hub method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteResponseMessageForError(IBufferWriter bufferWriter, int messageId, int statusCode, string detail, Exception? ex, bool isReturnExceptionStackTraceInErrorDetail) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(4); + writer.Write(messageId); + writer.Write(statusCode); + writer.Write(detail); + + var msg = (isReturnExceptionStackTraceInErrorDetail && ex != null) + ? ex.ToString() + : null; + + writer.Write(msg); + writer.Flush(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteClientResultRequestMessage(IBufferWriter bufferWriter, int methodId, Guid messageId, T request, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(5); + writer.Write(0); // Type = ClientResultRequest (0) + writer.WriteNil(); // Dummy + MessagePackSerializer.Serialize(ref writer, messageId); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, request); + } + + /// + /// Writes a response message for client result. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteClientResultResponseMessage(IBufferWriter bufferWriter, int methodId, Guid messageId, T response, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(4); + writer.Write(0); // Result = 0 (success) + MessagePackSerializer.Serialize(ref writer, messageId); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, response); + } + + /// + /// Writes an error response message for client result. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteClientResultResponseMessageForError(IBufferWriter bufferWriter, int methodId, Guid messageId, int statusCode, string detail, Exception? ex, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(4); + writer.Write(1); // Result = 1 (failed) + MessagePackSerializer.Serialize(ref writer, messageId); + writer.Write(methodId); + + writer.WriteArrayHeader(3); + { + writer.Write(statusCode); + writer.Write(detail); + writer.Write(ex?.ToString()); + } + writer.Flush(); + } + + + // Array(5)[127, Nil, Nil, Nil, ] + static ReadOnlySpan HeartbeatMessageForServerToClientHeader => new byte[] { 0x95, 0x7f, 0xc0, 0xc0, 0xc0 }; + + /// + /// Writes a heartbeat message for sending from the server. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteHeartbeatMessageForServerToClientHeader(IBufferWriter bufferWriter) + { + bufferWriter.Write(HeartbeatMessageForServerToClientHeader); + //var writer = new MessagePackWriter(bufferWriter); + //writer.WriteArrayHeader(5); + //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat) + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.Flush(); + } + + // Array(4)[127, Nil, Nil, Nil] + static ReadOnlySpan HeartbeatMessageForClientToServer => new byte[] { 0x94, 0x7f, 0xc0, 0xc0, 0xc0 }; + + /// + /// Writes a heartbeat message for sending from the client. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteHeartbeatMessageForClientToServer(IBufferWriter bufferWriter) + { + bufferWriter.Write(HeartbeatMessageForClientToServer); + //var writer = new MessagePackWriter(bufferWriter); + //writer.WriteArrayHeader(4); + //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat) + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.Flush(); + } + } + + internal enum StreamingHubMessageType + { + // Client to Server + Request, + RequestFireAndForget, + Response, + ResponseWithError, + HeartbeatResponse, + + // Server to Client + Broadcast, + ClientResultRequest, + ClientResultResponse, + ClientResultResponseWithError, + Heartbeat, + } +} diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs.meta b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs.meta new file mode 100644 index 000000000..16a91124e --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f412c8c0f99f39344a9188e75b97a219 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs new file mode 100644 index 000000000..3bebc7d94 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs @@ -0,0 +1,81 @@ +#nullable enable +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; + +namespace MagicOnion.Internal +{ + internal class StreamingHubPayload : IStreamingHubPayload + { + byte[]? buffer; + ReadOnlyMemory? memory; + + public int Length => memory!.Value.Length; + public ReadOnlySpan Span => memory!.Value.Span; + public ReadOnlyMemory Memory => memory!.Value; + + void IStreamingHubPayload.Initialize(ReadOnlySpan data) + { + ThrowIfUsing(); + + buffer = ArrayPool.Shared.Rent(data.Length); + data.CopyTo(buffer); + memory = buffer.AsMemory(0, (int)data.Length); + } + + void IStreamingHubPayload.Initialize(ReadOnlySequence data) + { + ThrowIfUsing(); + if (data.Length > int.MaxValue) throw new InvalidOperationException("A body size of StreamingHubPayload must be less than int.MaxValue"); + + buffer = ArrayPool.Shared.Rent((int)data.Length); + data.CopyTo(buffer); + memory = buffer.AsMemory(0, (int)data.Length); + } + + void IStreamingHubPayload.Initialize(ReadOnlyMemory data) + { + ThrowIfUsing(); + + buffer = null; + memory = data; + } + + void IStreamingHubPayload.Uninitialize() + { + ThrowIfDisposed(); + + if (buffer != null) + { +#if DEBUG && NET6_0_OR_GREATER + Array.Fill(buffer, 0xff); +#endif + ArrayPool.Shared.Return(buffer); + } + + memory = null; + buffer = null; + } + +#if NON_UNITY && !NETSTANDARD2_0 && !NETSTANDARD2_1 + [MemberNotNull(nameof(memory))] +#endif + void ThrowIfDisposed() + { + if (memory is null) throw new ObjectDisposedException(nameof(StreamingHubPayload)); + } + + void ThrowIfUsing() + { + if (memory is not null) throw new InvalidOperationException(nameof(StreamingHubPayload)); + } + } + + internal interface IStreamingHubPayload + { + void Initialize(ReadOnlySpan data); + void Initialize(ReadOnlySequence data); + void Initialize(ReadOnlyMemory data); + void Uninitialize(); + } +} diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs.meta b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs.meta new file mode 100644 index 000000000..9e8fb86b6 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 29c0ae37b5a14d74f9ff3d1ba17d88b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs new file mode 100644 index 000000000..68166f3c2 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs @@ -0,0 +1,98 @@ +#if !USE_OBJECTPOOL_STREAMINGHUBPAYLOADPOOL +#nullable enable +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace MagicOnion.Internal +{ + internal class StreamingHubPayloadPool + { + StreamingHubPayload? pool1; + StreamingHubPayload? pool2; + StreamingHubPayload? pool3; + StreamingHubPayload? pool4; + + static StreamingHubPayloadPool pool = new(); + + public static StreamingHubPayloadPool Shared => pool; + + public void Return(StreamingHubPayload payload) + { + ((IStreamingHubPayload)payload).Uninitialize(); + + var pooled = TryReturn(ref pool1, payload) || + TryReturn(ref pool2, payload) || + TryReturn(ref pool3, payload) || + TryReturn(ref pool4, payload); + } + + public StreamingHubPayload RentOrCreate(ReadOnlySequence data) + { + StreamingHubPayload? tmpPayload; + if (!(TryGet(ref pool1, out tmpPayload) || + TryGet(ref pool2, out tmpPayload) || + TryGet(ref pool3, out tmpPayload) || + TryGet(ref pool4, out tmpPayload))) + { + tmpPayload = new StreamingHubPayload(); + } + + ((IStreamingHubPayload)tmpPayload).Initialize(data); + + return tmpPayload; + } + + public StreamingHubPayload RentOrCreate(ReadOnlySpan data) + { + StreamingHubPayload? tmpPayload; + if (!(TryGet(ref pool1, out tmpPayload) || + TryGet(ref pool2, out tmpPayload) || + TryGet(ref pool3, out tmpPayload) || + TryGet(ref pool4, out tmpPayload))) + { + tmpPayload = new StreamingHubPayload(); + } + + ((IStreamingHubPayload)tmpPayload).Initialize(data); + + return tmpPayload; + } + + public StreamingHubPayload RentOrCreate(ReadOnlyMemory data) + { + StreamingHubPayload? tmpPayload; + if (!(TryGet(ref pool1, out tmpPayload) || + TryGet(ref pool2, out tmpPayload) || + TryGet(ref pool3, out tmpPayload) || + TryGet(ref pool4, out tmpPayload))) + { + tmpPayload = new StreamingHubPayload(); + } + + ((IStreamingHubPayload)tmpPayload).Initialize(data); + + return tmpPayload; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool TryReturn(ref StreamingHubPayload? field, StreamingHubPayload payload) + => Interlocked.CompareExchange(ref field, payload, null) == null; + + bool TryGet(ref StreamingHubPayload? field, [NotNullWhen(true)] out StreamingHubPayload? payload) + { + var tmp = field; + if (tmp != null && Interlocked.CompareExchange(ref field, null, tmp) == tmp) + { + payload = tmp; + return true; + } + + payload = null; + return false; + } + } +} +#endif diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs.meta b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs.meta new file mode 100644 index 000000000..eb73abab4 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b10ef269ace167b4dae0bdd13ef2c7ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs new file mode 100644 index 000000000..ae7abf115 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs @@ -0,0 +1,55 @@ +#if USE_OBJECTPOOL_STREAMINGHUBPAYLOADPOOL +using Microsoft.Extensions.ObjectPool; +using System.Buffers; + +namespace MagicOnion.Internal; + +internal class StreamingHubPayloadPool +{ + const int MaximumRetained = 2 << 7; + + readonly ObjectPool pool = new DefaultObjectPool(new Policy(), MaximumRetained); + + public static StreamingHubPayloadPool Shared { get; } = new StreamingHubPayloadPool(); + + public StreamingHubPayload RentOrCreate(ReadOnlySequence data) + { + var payload = pool.Get(); + ((IStreamingHubPayload)payload).Initialize(data); + return payload; + } + + public StreamingHubPayload RentOrCreate(ReadOnlySpan data) + { + var payload = pool.Get(); + ((IStreamingHubPayload)payload).Initialize(data); + return payload; + } + + public StreamingHubPayload RentOrCreate(ReadOnlyMemory data) + { + var payload = pool.Get(); + ((IStreamingHubPayload)payload).Initialize(data); + return payload; + } + + public void Return(StreamingHubPayload payload) + { + pool.Return(payload); + } + + class Policy : IPooledObjectPolicy + { + public StreamingHubPayload Create() + { + return new StreamingHubPayload(); + } + + public bool Return(StreamingHubPayload obj) + { + ((IStreamingHubPayload)obj).Uninitialize(); + return true; + } + } +} +#endif diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs.meta b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs.meta new file mode 100644 index 000000000..f6ea9615e --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a0db1221924fc1a47a6966366a8028c1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs new file mode 100644 index 000000000..b1d8326cf --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs @@ -0,0 +1,79 @@ +using System; +using MessagePack; + +namespace MagicOnion.Internal +{ + internal ref struct StreamingHubServerMessageReader + { + readonly ReadOnlyMemory data; + MessagePackReader reader; + + public StreamingHubServerMessageReader(ReadOnlyMemory data) + { + this.data = data; + this.reader = new MessagePackReader(data); + } + + public StreamingHubMessageType ReadMessageType() + { + var arrayLength = this.reader.ReadArrayHeader(); + return arrayLength switch + { + 2 => StreamingHubMessageType.RequestFireAndForget, + 3 => StreamingHubMessageType.Request, + 4 => reader.ReadByte() switch + { + 0x00 => StreamingHubMessageType.ClientResultResponse, + 0x01 => StreamingHubMessageType.ClientResultResponseWithError, + 0x7f => StreamingHubMessageType.HeartbeatResponse, + var subType => throw new InvalidOperationException($"Unknown client response message: {subType}"), + }, + _ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"), + }; + } + + public (int MethodId, ReadOnlyMemory Body) ReadRequestFireAndForget() + { + // void: [methodId, [argument]] + var methodId = reader.ReadInt32(); + var consumed = (int)reader.Consumed; + + return (methodId, data.Slice(consumed)); + } + + public (int MessageId, int MethodId, ReadOnlyMemory Body) ReadRequest() + { + // T: [messageId, methodId, [argument]] + var messageId = reader.ReadInt32(); + var methodId = reader.ReadInt32(); + var consumed = (int)reader.Consumed; + + return (messageId, methodId, data.Slice(consumed)); + } + + public (Guid ClientResultMessageId, int ClientMethodId, ReadOnlyMemory Body) ReadClientResultResponse() + { + // T: [0, clientResultMessageId, methodId, result] + var clientResultMessageId = MessagePackSerializer.Deserialize(ref reader); + var clientMethodId = reader.ReadInt32(); + var consumed = (int)reader.Consumed; + + return (clientResultMessageId, clientMethodId, data.Slice(consumed)); + } + + public (Guid ClientResultMessageId, int ClientMethodId, int StatusCode, string Detail, string Message) ReadClientResultResponseForError() + { + // T: [1, clientResultMessageId, methodId, [statusCode, detail, message]] + var clientResultMessageId = MessagePackSerializer.Deserialize(ref reader); + var clientMethodId = reader.ReadInt32(); + var bodyArray = reader.ReadArrayHeader(); + if (bodyArray != 3) throw new InvalidOperationException($"Invalid ClientResponse: The BodyArray length is {bodyArray}"); + + var statusCode = reader.ReadInt32(); + var detail = reader.ReadString() ?? string.Empty; + var message = reader.ReadString() ?? string.Empty; + + return (clientResultMessageId, clientMethodId, statusCode, detail, message); + } + } +} diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs.meta b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs.meta new file mode 100644 index 000000000..2e9d41b3c --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d4c013c0f3f500459b936e3cee4b01b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal/DictionaryExtensions.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal/DictionaryExtensions.cs new file mode 100644 index 000000000..ca34940c8 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal/DictionaryExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +// ReSharper disable once CheckNamespace +namespace System.Collections.Generic +{ + internal static class DictionaryExtensions + { +#if NETSTANDARD2_0 + public static bool Remove(this IDictionary dict, TKey key, [NotNullWhen(true)] out TValue? value) + { + if (dict.TryGetValue(key, out var v)) + { + dict.Remove(key); + value = v!; + return true; + } + + value = default; + return false; + } +#endif + } +} diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal/DictionaryExtensions.cs.meta b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal/DictionaryExtensions.cs.meta new file mode 100644 index 000000000..38b0a2bf0 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal/DictionaryExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4e248d00dbcc30c40928a09bba388efc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Client.SourceGenerator.Unity/MagicOnion.Client.SourceGenerator.Unity.dll b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Client.SourceGenerator.Unity/MagicOnion.Client.SourceGenerator.Unity.dll index 9695037d0..1ae0e388c 100644 Binary files a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Client.SourceGenerator.Unity/MagicOnion.Client.SourceGenerator.Unity.dll and b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnion.Client.SourceGenerator.Unity/MagicOnion.Client.SourceGenerator.Unity.dll differ diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnionClientBase.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnionClientBase.cs index 9a370287c..035841aca 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnionClientBase.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/MagicOnionClientBase.cs @@ -6,14 +6,14 @@ namespace MagicOnion.Client { - public readonly struct MagicOnionClientOptions + public class MagicOnionClientOptions { public string? Host { get; } public CallInvoker CallInvoker { get; } public IReadOnlyList Filters { get; } public CallOptions CallOptions { get; } - public MagicOnionClientOptions(CallInvoker callInvoker, string? host, CallOptions callOptions, IReadOnlyList filters) + public MagicOnionClientOptions(CallInvoker callInvoker, string? host, CallOptions callOptions, IReadOnlyList? filters) { Host = host; CallOptions = callOptions; @@ -22,11 +22,11 @@ public MagicOnionClientOptions(CallInvoker callInvoker, string? host, CallOption } public MagicOnionClientOptions WithCallOptions(CallOptions callOptions) - => new MagicOnionClientOptions(CallInvoker, Host, callOptions, Filters); + => new (CallInvoker, Host, callOptions, Filters); public MagicOnionClientOptions WithHost(string? host) - => new MagicOnionClientOptions(CallInvoker, host, CallOptions, Filters); + => new (CallInvoker, host, CallOptions, Filters); public MagicOnionClientOptions WithFilters(IReadOnlyList filters) - => new MagicOnionClientOptions(CallInvoker, Host, CallOptions, filters); + => new (CallInvoker, Host, CallOptions, filters); } public class MagicOnionClientBase diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClient.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClient.cs index 879e46285..833b91939 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClient.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClient.cs @@ -1,6 +1,5 @@ using Grpc.Core; using MagicOnion.Serialization; -using MessagePack; using System; using System.Threading; using System.Threading.Tasks; @@ -22,10 +21,17 @@ public static partial class StreamingHubClient return hubClient; } - public static async Task ConnectAsync(ChannelBase channel, TReceiver receiver, string? host = null, CallOptions option = default(CallOptions), IMagicOnionSerializerProvider? serializerProvider = null, IStreamingHubClientFactoryProvider? factoryProvider = null, IMagicOnionClientLogger? logger = null, CancellationToken cancellationToken = default) + public static Task ConnectAsync(ChannelBase channel, TReceiver receiver, string? host = null, CallOptions option = default(CallOptions), IMagicOnionSerializerProvider? serializerProvider = null, IStreamingHubClientFactoryProvider? factoryProvider = null, IMagicOnionClientLogger? logger = null, CancellationToken cancellationToken = default) where TStreamingHub : IStreamingHub { - var hubClient = await ConnectAsync(channel.CreateCallInvoker(), receiver, host, option, serializerProvider, factoryProvider, logger, cancellationToken); + var options = StreamingHubClientOptions.CreateWithDefault(host, option, serializerProvider, logger); + return ConnectAsync(channel, receiver, options, factoryProvider, cancellationToken); + } + + public static async Task ConnectAsync(ChannelBase channel, TReceiver receiver, StreamingHubClientOptions options, IStreamingHubClientFactoryProvider? factoryProvider = null, CancellationToken cancellationToken = default) + where TStreamingHub : IStreamingHub + { + var hubClient = await ConnectAsync(channel.CreateCallInvoker(), receiver, options, factoryProvider, cancellationToken); // ReSharper disable once SuspiciousTypeConversion.Global if (channel is IMagicOnionAwareGrpcChannel magicOnionAwareGrpcChannel) { @@ -38,11 +44,12 @@ public static partial class StreamingHubClient public static TStreamingHub Connect(CallInvoker callInvoker, TReceiver receiver, string? host = null, CallOptions option = default(CallOptions), IMagicOnionSerializerProvider? serializerProvider = null, IStreamingHubClientFactoryProvider? factoryProvider = null, IMagicOnionClientLogger? logger = null) where TStreamingHub : IStreamingHub { - var client = CreateClient(callInvoker, receiver, host, option, serializerProvider, factoryProvider, logger); + var options = StreamingHubClientOptions.CreateWithDefault(host, option); + var client = CreateClient(receiver, callInvoker, options, factoryProvider); async void ConnectAndForget() { - var task = client.__ConnectAndSubscribeAsync(receiver, CancellationToken.None); + var task = client.__ConnectAndSubscribeAsync(CancellationToken.None); try { await task.ConfigureAwait(false); @@ -58,27 +65,32 @@ async void ConnectAndForget() return (TStreamingHub)(object)client; } - public static async Task ConnectAsync(CallInvoker callInvoker, TReceiver receiver, string? host = null, CallOptions option = default(CallOptions), IMagicOnionSerializerProvider? serializerProvider = null, IStreamingHubClientFactoryProvider? factoryProvider = null, IMagicOnionClientLogger? logger = null, CancellationToken cancellationToken = default) + public static Task ConnectAsync(CallInvoker callInvoker, TReceiver receiver, string? host = null, CallOptions option = default(CallOptions), IMagicOnionSerializerProvider? serializerProvider = null, IStreamingHubClientFactoryProvider? factoryProvider = null, IMagicOnionClientLogger? logger = null, CancellationToken cancellationToken = default) + where TStreamingHub : IStreamingHub + { + var options = StreamingHubClientOptions.CreateWithDefault(host, option, serializerProvider, logger); + return ConnectAsync(callInvoker, receiver, options, factoryProvider, cancellationToken); + } + + public static async Task ConnectAsync(CallInvoker callInvoker, TReceiver receiver, StreamingHubClientOptions options, IStreamingHubClientFactoryProvider? factoryProvider = null, CancellationToken cancellationToken = default) where TStreamingHub : IStreamingHub { - var client = CreateClient(callInvoker, receiver, host, option, serializerProvider, factoryProvider, logger); - await client.__ConnectAndSubscribeAsync(receiver, cancellationToken).ConfigureAwait(false); + var client = CreateClient(receiver, callInvoker, options, factoryProvider); + await client.__ConnectAndSubscribeAsync(cancellationToken).ConfigureAwait(false); return (TStreamingHub)(object)client; } - static StreamingHubClientBase CreateClient(CallInvoker callInvoker, TReceiver receiver, string? host, CallOptions option, IMagicOnionSerializerProvider? serializerProvider, IStreamingHubClientFactoryProvider? factoryProvider, IMagicOnionClientLogger? logger) + static StreamingHubClientBase CreateClient(TReceiver receiver, CallInvoker callInvoker, StreamingHubClientOptions options, IStreamingHubClientFactoryProvider? factoryProvider) where TStreamingHub : IStreamingHub { - serializerProvider ??= MagicOnionSerializerProvider.Default; factoryProvider ??= StreamingHubClientFactoryProvider.Default; - logger ??= NullMagicOnionClientLogger.Instance; if (!factoryProvider.TryGetFactory(out var factory)) { throw new NotSupportedException($"Unable to get client factory for StreamingHub type '{typeof(TStreamingHub).FullName}'."); } - return (StreamingHubClientBase)(object)factory(callInvoker, receiver, host, option, serializerProvider, logger); + return (StreamingHubClientBase)(object)factory(receiver, callInvoker, options); } } } diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs index a0d8da805..9518d098e 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs @@ -1,20 +1,75 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Buffers; +using System.Collections.Concurrent; using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Channels; using Grpc.Core; using MagicOnion.Client.Internal.Threading; using MagicOnion.Client.Internal.Threading.Tasks; using MagicOnion.Internal; using MagicOnion.Serialization; using MagicOnion.Internal.Buffers; -using MessagePack; namespace MagicOnion.Client { + public class StreamingHubClientOptions + { + public string? Host { get; } + public CallOptions CallOptions { get; } + public IMagicOnionSerializerProvider SerializerProvider { get; } + public IMagicOnionClientLogger Logger { get; } + + public TimeSpan? HeartbeatInterval { get; } + public Action>? HeartbeatReceivedFromServer { get; } + + public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger) + : this(host, callOptions, serializerProvider, logger, default, default) + { + } + + public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? heartbeatInterval, Action>? heartbeatReceivedFromServer) + { + Host = host; + CallOptions = callOptions; + SerializerProvider = serializerProvider ?? throw new ArgumentNullException(nameof(serializerProvider)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + HeartbeatInterval = heartbeatInterval; + HeartbeatReceivedFromServer = heartbeatReceivedFromServer; + } + + public static StreamingHubClientOptions CreateWithDefault(string? host = default, CallOptions callOptions = default, IMagicOnionSerializerProvider? serializerProvider = default, IMagicOnionClientLogger? logger = default) + => new(host, callOptions, serializerProvider ?? MagicOnionSerializerProvider.Default, logger ?? NullMagicOnionClientLogger.Instance); + + public StreamingHubClientOptions WithHost(string? host) + => new(host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + public StreamingHubClientOptions WithCallOptions(CallOptions callOptions) + => new(Host, callOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + public StreamingHubClientOptions WithSerializerProvider(IMagicOnionSerializerProvider serializerProvider) + => new(Host, CallOptions, serializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + public StreamingHubClientOptions WithLogger(IMagicOnionClientLogger logger) + => new(Host, CallOptions, SerializerProvider, logger, HeartbeatInterval, HeartbeatReceivedFromServer); + + /// + /// Sets a heartbeat interval. If a value is , the heartbeat from the client is disabled. + /// + /// + /// + public StreamingHubClientOptions WithHeartbeatInterval(TimeSpan? interval) + => new(Host, CallOptions, SerializerProvider, Logger, interval, HeartbeatReceivedFromServer); + + /// + /// Sets a heartbeat callback. If additional metadata is provided by the server in the heartbeat message, this metadata is provided as an argument. + /// + /// + /// + public StreamingHubClientOptions WithHeartbeatReceived(Action>? onHeartbeatReceived) + => new(Host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, onHeartbeatReceived); + } + public abstract class StreamingHubClientBase where TStreamingHub : IStreamingHub { @@ -23,47 +78,52 @@ public abstract class StreamingHubClientBase const string StreamingHubVersionHeaderValue = "2"; #pragma warning restore IDE1006 // Naming Styles - readonly string? host; - readonly CallOptions option; readonly CallInvoker callInvoker; + readonly StreamingHubClientOptions options; readonly IMagicOnionClientLogger logger; readonly IMagicOnionSerializer messageSerializer; - readonly AsyncLock asyncLock = new AsyncLock(); - readonly Method duplexStreamingConnectMethod; + readonly Method duplexStreamingConnectMethod; + // {messageId, TaskCompletionSource} + readonly Dictionary responseFutures = new(); + readonly TaskCompletionSource waitForDisconnect = new(); + readonly CancellationTokenSource cancellationTokenSource = new(); - IClientStreamWriter writer = default!; - IAsyncStreamReader reader = default!; + readonly Dictionary postCallbackCache = new(); + SendOrPostCallback? heartbeatCallbackCache; - protected TReceiver receiver = default!; - Task subscription = default!; + int messageIdSequence = 0; + bool disposed; - TaskCompletionSource waitForDisconnect = new TaskCompletionSource(); + Task? heartbeatTask; + DateTimeOffset lastHeartbeatSentAt; - // {messageId, TaskCompletionSource} - ConcurrentDictionary responseFutures = new ConcurrentDictionary(); - protected CancellationTokenSource cts = new CancellationTokenSource(); - int messageId = 0; - bool disposed; + readonly Channel writerQueue = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader = true, SingleWriter = false, AllowSynchronousContinuations = false }); + Task? writerTask; + IClientStreamWriter writer = default!; + IAsyncStreamReader reader = default!; + + Task subscription = default!; + + protected readonly TReceiver receiver; - protected StreamingHubClientBase(string serviceName, CallInvoker callInvoker, string? host, CallOptions option, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger) + protected StreamingHubClientBase(string serviceName, TReceiver receiver, CallInvoker callInvoker, StreamingHubClientOptions options) { + this.callInvoker = callInvoker; + this.receiver = receiver; + this.options = options; + this.logger = options.Logger; this.duplexStreamingConnectMethod = CreateConnectMethod(serviceName); - this.callInvoker = callInvoker ?? throw new ArgumentNullException(nameof(callInvoker)); - this.host = host; - this.option = option; - this.messageSerializer = serializerProvider?.Create(MethodType.DuplexStreaming, null) ?? throw new ArgumentNullException(nameof(serializerProvider)); - this.logger = logger ?? NullMagicOnionClientLogger.Instance; + this.messageSerializer = options.SerializerProvider.Create(MethodType.DuplexStreaming, null); } // call immediately after create. - public async Task __ConnectAndSubscribeAsync(TReceiver receiver, CancellationToken cancellationToken) + public async Task __ConnectAndSubscribeAsync(CancellationToken cancellationToken) { var syncContext = SynchronizationContext.Current; // capture SynchronizationContext. - var callResult = callInvoker.AsyncDuplexStreamingCall(duplexStreamingConnectMethod, host, option); + var callResult = callInvoker.AsyncDuplexStreamingCall(duplexStreamingConnectMethod, options.Host, options.CallOptions); this.writer = callResult.RequestStream; this.reader = callResult.ResponseStream; - this.receiver = receiver; // Establish StreamingHub connection between the client and the server. Metadata.Entry? messageVersion; @@ -91,7 +151,7 @@ public async Task __ConnectAndSubscribeAsync(TReceiver receiver, CancellationTok throw new RpcException(e.Status, $"Failed to connect to StreamingHub '{duplexStreamingConnectMethod.ServiceName}'. ({e.Status})"); } - var firstMoveNextTask = reader.MoveNext(CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cts.Token).Token); + var firstMoveNextTask = reader.MoveNext(CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cancellationTokenSource.Token).Token); if (firstMoveNextTask.IsFaulted || messageVersion == null) { // NOTE: Grpc.Net: @@ -111,21 +171,29 @@ public async Task __ConnectAndSubscribeAsync(TReceiver receiver, CancellationTok } // Helper methods to make building clients easy. - protected void SetResultForResponse(object taskCompletionSource, ArraySegment data) + protected void SetResultForResponse(object taskCompletionSource, ReadOnlyMemory data) => ((TaskCompletionSource)taskCompletionSource).TrySetResult(Deserialize(data)); protected void Serialize(IBufferWriter writer, in T value) => messageSerializer.Serialize(writer, value); - protected T Deserialize(ArraySegment bytes) - => messageSerializer.Deserialize(new ReadOnlySequence(bytes)); + protected T Deserialize(ReadOnlyMemory data) + => messageSerializer.Deserialize(new ReadOnlySequence(data)); - protected abstract void OnResponseEvent(int methodId, object taskCompletionSource, ArraySegment data); - protected abstract void OnBroadcastEvent(int methodId, ArraySegment data); + protected abstract void OnClientResultEvent(int methodId, Guid messageId, ReadOnlyMemory data); + protected abstract void OnResponseEvent(int methodId, object taskCompletionSource, ReadOnlyMemory data); + protected abstract void OnBroadcastEvent(int methodId, ReadOnlyMemory data); - static Method CreateConnectMethod(string serviceName) - => new Method(MethodType.DuplexStreaming, serviceName, "Connect", MagicOnionMarshallers.ThroughMarshaller, MagicOnionMarshallers.ThroughMarshaller); + static Method CreateConnectMethod(string serviceName) + => new (MethodType.DuplexStreaming, serviceName, "Connect", MagicOnionMarshallers.StreamingHubMarshaller, MagicOnionMarshallers.StreamingHubMarshaller); async Task StartSubscribe(SynchronizationContext? syncContext, Task firstMoveNext) { + if (options.HeartbeatInterval is { } heartbeatInterval) + { + heartbeatTask = RunHeartbeatLoopAsync(heartbeatInterval, cancellationTokenSource.Token); + } + + writerTask = RunWriterLoopAsync(cancellationTokenSource.Token); + var reader = this.reader; try { @@ -150,7 +218,7 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } } - moveNext = reader.MoveNext(cts.Token); + moveNext = reader.MoveNext(cancellationTokenSource.Token); } } catch (Exception ex) @@ -195,104 +263,214 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM // broadcast: [methodId, [argument]] // response: [messageId, methodId, response] // error-response: [messageId, statusCode, detail, StringMessage] - void ConsumeData(SynchronizationContext? syncContext, byte[] data) + void ConsumeData(SynchronizationContext? syncContext, StreamingHubPayload payload) + { + var messageReader = new StreamingHubClientMessageReader(payload.Memory); + switch (messageReader.ReadMessageType()) + { + case StreamingHubMessageType.Broadcast: + ProcessBroadcast(syncContext, payload, ref messageReader); + break; + case StreamingHubMessageType.Response: + ProcessResponse(syncContext, payload, ref messageReader); + break; + case StreamingHubMessageType.ResponseWithError: + ProcessResponseWithError(syncContext, payload, ref messageReader); + break; + case StreamingHubMessageType.ClientResultRequest: + ProcessClientResultRequest(syncContext, payload, ref messageReader); + break; + case StreamingHubMessageType.Heartbeat: + ProcessHeartbeat(syncContext, payload, ref messageReader); + break; + } + } + + void ProcessBroadcast(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) { - var messagePackReader = new MessagePackReader(data); - var arrayLength = messagePackReader.ReadArrayHeader(); - if (arrayLength == 3) + if (syncContext is null) { - var messageId = messagePackReader.ReadInt32(); - if (responseFutures.TryRemove(messageId, out var future)) + var message = messageReader.ReadBroadcastMessage(); + OnBroadcastEvent(message.MethodId, message.Body); + StreamingHubPayloadPool.Shared.Return(payload); + } + else + { + var (methodId, consumed) = messageReader.ReadBroadcastMessageMethodId(); + if (!postCallbackCache.TryGetValue(methodId, out var postCallback)) { - var methodId = messagePackReader.ReadInt32(); - try - { - var offset = (int)messagePackReader.Consumed; - var rest = new ArraySegment(data, offset, data.Length - offset); - OnResponseEvent(methodId, future, rest); - } - catch (Exception ex) - { - if (!future.TrySetException(ex)) - { - throw; - } - } + // Create and cache a callback delegate capturing `this` and the header size. + postCallback = postCallbackCache[methodId] = CreateBroadcastCallback(methodId, consumed); } + syncContext.Post(postCallback, payload); } - else if (arrayLength == 4) + } + + SendOrPostCallback CreateBroadcastCallback(int methodId, int consumed) + { + return (state) => { - var messageId = messagePackReader.ReadInt32(); - if (responseFutures.TryRemove(messageId, out var future)) + var p = (StreamingHubPayload)state!; + this.OnBroadcastEvent(methodId, p.Memory.Slice(consumed)); + StreamingHubPayloadPool.Shared.Return(p); + }; + } + + void ProcessResponse(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) + { + var message = messageReader.ReadResponseMessage(); + + ITaskCompletion? future; + lock (responseFutures) + { + if (!responseFutures.Remove(message.MessageId, out future)) { - var statusCode = messagePackReader.ReadInt32(); - var detail = messagePackReader.ReadString(); - var offset = (int)messagePackReader.Consumed; - var error = messagePackReader.ReadString(); - var ex = default(RpcException); - if (string.IsNullOrWhiteSpace(error)) - { - ex = new RpcException(new Status((StatusCode)statusCode, detail ?? string.Empty)); - } - else - { - ex = new RpcException(new Status((StatusCode)statusCode, detail ?? string.Empty), detail + Environment.NewLine + error); - } + return; + } + } - future.TrySetException(ex); + try + { + OnResponseEvent(message.MethodId, future, message.Body); + StreamingHubPayloadPool.Shared.Return(payload); + } + catch (Exception ex) + { + if (!future.TrySetException(ex)) + { + throw; + } + } + } + + void ProcessResponseWithError(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) + { + var message = messageReader.ReadResponseWithErrorMessage(); + + ITaskCompletion? future; + lock (responseFutures) + { + if (!responseFutures.Remove(message.MessageId, out future)) + { + return; + } + } + + if (responseFutures.Remove(message.MessageId, out future)) + { + RpcException ex; + if (string.IsNullOrWhiteSpace(message.Error)) + { + ex = new RpcException(new Status((StatusCode)message.StatusCode, message.Detail ?? string.Empty)); } + else + { + ex = new RpcException(new Status((StatusCode)message.StatusCode, message.Detail ?? string.Empty), message.Detail + Environment.NewLine + message.Error); + } + + future.TrySetException(ex); + StreamingHubPayloadPool.Shared.Return(payload); + } + } + + void ProcessClientResultRequest(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) + { + var message = messageReader.ReadClientResultRequestMessage(); + if (syncContext is null) + { + OnClientResultEvent(message.MethodId, message.ClientResultRequestMessageId, message.Body); + StreamingHubPayloadPool.Shared.Return(payload); } else { - var methodId = messagePackReader.ReadInt32(); - var offset = (int)messagePackReader.Consumed; - if (syncContext != null) + var tuple = Tuple.Create(this, message.MethodId, message.ClientResultRequestMessageId, message.Body, payload); + syncContext.Post(static state => { - var tuple = Tuple.Create(methodId, data, offset, data.Length - offset); - syncContext.Post(state => - { - var t = (Tuple)state!; - OnBroadcastEvent(t.Item1, new ArraySegment(t.Item2, t.Item3, t.Item4)); - }, tuple); + var t = (Tuple, int, Guid, ReadOnlyMemory, StreamingHubPayload>)state!; + t.Item1.OnClientResultEvent(t.Item2, t.Item3, t.Item4); + StreamingHubPayloadPool.Shared.Return(t.Item5); + }, tuple); + } + } + + void ProcessHeartbeat(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) + { + var metadata = messageReader.ReadHeartbeat(); + if (this.options.HeartbeatReceivedFromServer is { } heartbeatReceived) + { + if (syncContext is null) + { + heartbeatReceived(metadata); + StreamingHubPayloadPool.Shared.Return(payload); } else { - OnBroadcastEvent(methodId, new ArraySegment(data, offset, data.Length - offset)); + heartbeatCallbackCache ??= CreateHeartbeatCallback(heartbeatReceived); + syncContext.Post(heartbeatCallbackCache, payload); } } + WriteHeartbeat(); } - protected async Task WriteMessageFireAndForgetAsync(int methodId, TRequest message) + SendOrPostCallback CreateHeartbeatCallback(Action> heartbeatReceivedAction) => (state) => { - ThrowIfDisposed(); + var p = (StreamingHubPayload)state!; + heartbeatReceivedAction(p.Memory.Slice(5)); + StreamingHubPayloadPool.Shared.Return(p); + }; - byte[] BuildMessage() + async Task RunHeartbeatLoopAsync(TimeSpan heartbeatInterval, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) { - using (var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter()) + await Task.Delay(heartbeatInterval, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + + if ((DateTimeOffset.UtcNow - lastHeartbeatSentAt) > heartbeatInterval) { - var writer = new MessagePackWriter(buffer); - writer.WriteArrayHeader(2); - writer.Write(methodId); - writer.Flush(); - Serialize(buffer, message); - return buffer.WrittenSpan.ToArray(); + WriteHeartbeat(); } } + } - var v = BuildMessage(); - using (await asyncLock.LockAsync().ConfigureAwait(false)) + async Task RunWriterLoopAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) { - await writer.WriteAsync(v).ConfigureAwait(false); + if (await writerQueue.Reader.WaitToReadAsync(default).ConfigureAwait(false)) + { + while (writerQueue.Reader.TryRead(out var payload)) + { + await writer.WriteAsync(payload).ConfigureAwait(false); + } + } } + } + + void WriteHeartbeat() + { + if (disposed) return; + var v = BuildHeartbeatMessage(); + _ = writerQueue.Writer.TryWrite(v); + + lastHeartbeatSentAt = DateTimeOffset.UtcNow; + } + + protected Task WriteMessageFireAndForgetAsync(int methodId, TRequest message) + { + ThrowIfDisposed(); + + var v = BuildRequestMessage(methodId, message); + _ = writerQueue.Writer.TryWrite(v); - return default!; + return Task.FromResult(default!); } - protected async Task WriteMessageWithResponseAsync(int methodId, TRequest message) + protected Task WriteMessageWithResponseAsync(int methodId, TRequest message) { ThrowIfDisposed(); - var mid = Interlocked.Increment(ref messageId); + var mid = Interlocked.Increment(ref messageIdSequence); // NOTE: The continuations (user code) should be executed asynchronously. (Except: Unity WebGL) // This is because the continuation may block the thread, for example, Console.ReadLine(). // If the thread is blocked, it will no longer return to the message consuming loop. @@ -301,29 +479,79 @@ protected async Task WriteMessageWithResponseAsync AwaitAndWriteClientResultResponseMessage(methodId, clientResultMessageId, new ValueTask(task)); + + protected async void AwaitAndWriteClientResultResponseMessage(int methodId, Guid clientResultMessageId, ValueTask task) + { + try + { + await task.ConfigureAwait(false); + await WriteClientResultResponseMessageAsync(methodId, clientResultMessageId, MessagePack.Nil.Default).ConfigureAwait(false); + } + catch (Exception e) + { + await WriteClientResultResponseMessageForErrorAsync(methodId, clientResultMessageId, e).ConfigureAwait(false); + } + } + + protected void AwaitAndWriteClientResultResponseMessage(int methodId, Guid clientResultMessageId, Task task) + => AwaitAndWriteClientResultResponseMessage(methodId, clientResultMessageId, new ValueTask(task)); - byte[] BuildMessage() + protected async void AwaitAndWriteClientResultResponseMessage(int methodId, Guid clientResultMessageId, ValueTask task) + { + try { - using (var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter()) - { - var writer = new MessagePackWriter(buffer); - writer.WriteArrayHeader(3); - writer.Write(mid); - writer.Write(methodId); - writer.Flush(); - Serialize(buffer, message); - return buffer.WrittenSpan.ToArray(); - } + var result = await task.ConfigureAwait(false); + await WriteClientResultResponseMessageAsync(methodId, clientResultMessageId, result).ConfigureAwait(false); + } + catch (Exception e) + { + await WriteClientResultResponseMessageForErrorAsync(methodId, clientResultMessageId, e).ConfigureAwait(false); } + } - var v = BuildMessage(); - using (await asyncLock.LockAsync().ConfigureAwait(false)) + protected async void WriteClientResultResponseMessageForError(int methodId, Guid clientResultMessageId, Exception ex) + { + try + { + await WriteClientResultResponseMessageForErrorAsync(methodId, clientResultMessageId, ex).ConfigureAwait(false); + } + catch { - await writer.WriteAsync(v).ConfigureAwait(false); + // Ignore Exception } + } - return await tcs.Task.ConfigureAwait(false); // wait until server return response(or error). if connection was closed, throws cancellation from DisposeAsyncCore. + protected Task WriteClientResultResponseMessageAsync(int methodId, Guid clientResultMessageId, T result) + { + var v = BuildClientResultResponseMessage(methodId, clientResultMessageId, result); + _ = writerQueue.Writer.TryWrite(v); + return Task.CompletedTask; + } + + protected Task WriteClientResultResponseMessageForErrorAsync(int methodId, Guid clientResultMessageId, Exception ex) + { + var statusCode = ex is RpcException rpcException + ? rpcException.StatusCode + : StatusCode.Internal; + + var v = BuildClientResultResponseMessageForError(methodId, clientResultMessageId, (int)statusCode, ex.Message, ex); + _ = writerQueue.Writer.TryWrite(v); + + return Task.CompletedTask; } void ThrowIfDisposed() @@ -353,13 +581,14 @@ async Task DisposeAsyncCore(bool waitSubscription) try { + writerQueue.Writer.Complete(); await writer.CompleteAsync().ConfigureAwait(false); } catch { } // ignore error? finally { - cts.Cancel(); - cts.Dispose(); + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); try { if (waitSubscription) @@ -401,5 +630,40 @@ async Task DisposeAsyncCore(bool waitSubscription) } } } + + StreamingHubPayload BuildRequestMessage(int methodId, T message) + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteRequestMessageVoid(buffer, methodId, message, messageSerializer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + StreamingHubPayload BuildRequestMessage(int methodId, int messageId, T message) + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteRequestMessage(buffer, methodId, messageId, message, messageSerializer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + StreamingHubPayload BuildClientResultResponseMessage(int methodId, Guid messageId, T response) + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteClientResultResponseMessage(buffer, methodId, messageId, response, messageSerializer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + StreamingHubPayload BuildClientResultResponseMessageForError(int methodId, Guid messageId, int statusCode, string detail, Exception? ex) + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteClientResultResponseMessageForError(buffer, methodId, messageId, statusCode, detail, ex, messageSerializer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + StreamingHubPayload BuildHeartbeatMessage() + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteHeartbeatMessageForClientToServer(buffer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } } } diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientFactoryProvider.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientFactoryProvider.cs index a1b3a6652..715a47b0f 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientFactoryProvider.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientFactoryProvider.cs @@ -22,7 +22,7 @@ public static class StreamingHubClientFactoryProvider #endif } - public delegate TStreamingHub StreamingHubClientFactoryDelegate(CallInvoker callInvoker, TReceiver receiver, string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger) + public delegate TStreamingHub StreamingHubClientFactoryDelegate(TReceiver receiver, CallInvoker callInvoker, StreamingHubClientOptions options) where TStreamingHub : IStreamingHub; /// diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Serialization.MemoryPack/DynamicArgumentTupleMemoryPackFormatter.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Serialization.MemoryPack/DynamicArgumentTupleMemoryPackFormatter.cs index 5d3bd7215..da1a16f8b 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Serialization.MemoryPack/DynamicArgumentTupleMemoryPackFormatter.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Serialization.MemoryPack/DynamicArgumentTupleMemoryPackFormatter.cs @@ -1,4 +1,4 @@ -// +// #pragma warning disable CS8669 #nullable enable diff --git a/src/MagicOnion.Client.Unity/ProjectSettings/ProjectVersion.txt b/src/MagicOnion.Client.Unity/ProjectSettings/ProjectVersion.txt index 3384268eb..f8251a7af 100644 --- a/src/MagicOnion.Client.Unity/ProjectSettings/ProjectVersion.txt +++ b/src/MagicOnion.Client.Unity/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2021.3.0f1 -m_EditorVersionWithRevision: 2021.3.0f1 (6eacc8284459) +m_EditorVersion: 2021.3.22f1 +m_EditorVersionWithRevision: 2021.3.22f1 (b6c551784ba3) diff --git a/src/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientBuilder.cs b/src/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientBuilder.cs index ab907ade1..8db75bc3b 100644 --- a/src/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientBuilder.cs +++ b/src/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientBuilder.cs @@ -49,6 +49,7 @@ public static AssemblyBuilder Save() internal #endif static class DynamicStreamingHubClientBuilder + where TStreamingHub : IStreamingHub { public static readonly Type ClientType; // static readonly Type ClientFireAndForgetType; @@ -133,9 +134,10 @@ static void VerifyMethodDefinitions(MethodDefinition[] definitions) if (returnTypeNonGenericOrOpenGeneric != typeof(ValueTask) && returnTypeNonGenericOrOpenGeneric != typeof(Task) && returnTypeNonGenericOrOpenGeneric != typeof(ValueTask<>) && - returnTypeNonGenericOrOpenGeneric != typeof(Task<>)) + returnTypeNonGenericOrOpenGeneric != typeof(Task<>) && + returnTypeNonGenericOrOpenGeneric != typeof(void)) { - throw new Exception($"Invalid definition, TStreamingHub's return type must only be `Task`, `Task`, `ValueTask` or `ValueTask`. {item.MethodInfo.Name}."); + throw new Exception($"Invalid definition, TStreamingHub's return type must only be `void`, `Task`, `Task`, `ValueTask` or `ValueTask`. {item.MethodInfo.Name}."); } item.MethodId = methodId; @@ -148,20 +150,18 @@ static void VerifyMethodDefinitions(MethodDefinition[] definitions) static FieldInfo DefineConstructor(TypeBuilder typeBuilder, Type interfaceType, Type receiverType, ConstructorInfo fireAndForgetClientCtor) { - // .ctor(CallInvoker callInvoker, string host, CallOptions option, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger) :base(...) + // .ctor(TReceiver receiver, CallInvoker callInvoker, StreamingHubClientOptions options) : base("InterfaceName", receiver, callInvoker, options) { - var argTypes = new[] { typeof(CallInvoker), typeof(string), typeof(CallOptions), typeof(IMagicOnionSerializerProvider), typeof(IMagicOnionClientLogger) }; + var argTypes = new[] { receiverType, typeof(CallInvoker), typeof(StreamingHubClientOptions) }; var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, argTypes); var il = ctor.GetILGenerator(); - // base(serviceName, callInvoker, host, option, serializerProvider, logger); + // base("InterfaceName", receiver, callInvoker, options); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldstr, interfaceType.Name); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Ldarg_3); - il.Emit(OpCodes.Ldarg_S, (byte)4); - il.Emit(OpCodes.Ldarg_S, (byte)5); + il.Emit(OpCodes.Ldarg_1); // receiver + il.Emit(OpCodes.Ldarg_2); // callInvoker + il.Emit(OpCodes.Ldarg_3); // options il.Emit(OpCodes.Call, typeBuilder.BaseType! .GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).First()); @@ -240,13 +240,14 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece // receiver types borrow from DynamicBroadcastBuilder { - // protected abstract void OnResponseEvent(int methodId, object taskCompletionSource, ArraySegment data); + // protected abstract void OnResponseEvent(int methodId, object taskCompletionSource, ReadOnlyMemory data); { var method = typeBuilder.DefineMethod("OnResponseEvent", MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual, - null, new[] { typeof(int), typeof(object), typeof(ArraySegment) }); + null, new[] { typeof(int), typeof(object), typeof(ReadOnlyMemory) }); var il = method.GetILGenerator(); var labels = definitions + .Where(x => x.MethodInfo.ReturnType != typeof(void)) // If the return type if `void`, we always need to treat as fire-and-forget. .Select(x => new { def = x, label = il.DefineLabel() }) .ToArray(); @@ -264,6 +265,7 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece { // SetResultForResponse(taskCompletionSource, data); Type responseType; + if (item.def.MethodInfo.ReturnType == typeof(Task) || item.def.MethodInfo.ReturnType == typeof(ValueTask)) { // Task methods uses TaskCompletionSource @@ -285,13 +287,13 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece il.Emit(OpCodes.Ret); } } - // protected abstract void OnBroadcastEvent(int methodId, ArraySegment data); + // protected abstract void OnBroadcastEvent(int methodId, ReadOnlyMemory data); { var methodDefinitions = BroadcasterHelper.SearchDefinitions(receiverType); BroadcasterHelper.VerifyMethodDefinitions(methodDefinitions); var method = typeBuilder.DefineMethod("OnBroadcastEvent", MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual, - typeof(void), new[] { typeof(int), typeof(ArraySegment) }); + typeof(void), new[] { typeof(int), typeof(ReadOnlyMemory) }); var il = method.GetILGenerator(); var labels = methodDefinitions @@ -314,7 +316,6 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece // var value = Deserialize>(data); // receiver.OnMessage(value.Item1, value.Item2); - var deserializeMethod = baseType.GetMethod("Deserialize", BindingFlags.Instance | BindingFlags.NonPublic)!; var parameters = item.def.MethodInfo.GetParameters(); if (parameters.Length == 0) { @@ -332,7 +333,7 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece // this.Deserialize(data) il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldarg_2); // data - il.Emit(OpCodes.Call, deserializeMethod.MakeGenericMethod(parameters[0].ParameterType)); + il.Emit(OpCodes.Call, MethodInfoCache.StreamingHubClientBase_Deserialize.MakeGenericMethod(parameters[0].ParameterType)); } il.Emit(OpCodes.Callvirt, item.def.MethodInfo); } @@ -346,7 +347,7 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece { il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldarg_2); // data - il.Emit(OpCodes.Call, deserializeMethod.MakeGenericMethod(deserializeType)); + il.Emit(OpCodes.Call, MethodInfoCache.StreamingHubClientBase_Deserialize.MakeGenericMethod(deserializeType)); il.Emit(OpCodes.Stloc, lc); } @@ -365,6 +366,162 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece } } } + // protected abstract void OnClientResultEvent(int methodId, Guid messageId, ReadOnlyMemory data); + { + var methodDefinitions = BroadcasterHelper.SearchDefinitions(receiverType); + BroadcasterHelper.VerifyMethodDefinitions(methodDefinitions); + + var method = typeBuilder.DefineMethod("OnClientResultEvent", MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual, + typeof(void), new[] { typeof(int), typeof(Guid), typeof(ReadOnlyMemory) }); + { + var il = method.GetILGenerator(); + var localEx = il.DeclareLocal(typeof(Exception)); + + var clientResultMethods = methodDefinitions.Where(x => x.IsClientResult) + .Select(x => (Label: il.DefineLabel(), Method: x)).ToArray(); + var labelReturn = il.DefineLabel(); + + var localCtDefault = il.DeclareLocal(typeof(CancellationToken)); + { + // var ctDefault = default(CancellationToken); + il.Emit(OpCodes.Ldloca_S, localCtDefault!); + il.Emit(OpCodes.Initobj, typeof(CancellationToken)); + } + + + // try { + il.BeginExceptionBlock(); + foreach (var (labelMethodBlock, methodClientResult) in clientResultMethods) + { + // if (methodId == ...) goto label; + il.Emit(OpCodes.Ldarg_1); // methodId + il.Emit(OpCodes.Ldc_I4, methodClientResult.MethodId); + il.Emit(OpCodes.Beq, labelMethodBlock); + } + // goto Return; + il.Emit(OpCodes.Leave, labelReturn); + + foreach (var (labelMethodBlock, methodClientResult) in clientResultMethods) + { + var parameters = methodClientResult.MethodInfo.GetParameters().Where(x => x.ParameterType != typeof(CancellationToken)).ToArray(); + var deserializeType = parameters.Length switch + { + 0 => typeof(MessagePack.Nil), + 1 => parameters[0].ParameterType, + _ => BroadcasterHelper.DynamicArgumentTupleTypes[parameters.Length - 2].MakeGenericType(parameters.Select(x => x.ParameterType).ToArray()) + }; + + // Method: + // { + // var local_0 = base.Deserialize(data); + // var task = this.SomeMethod(local0.Item1, local0.Item2); + // base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, localTask); + // return; + // } + // break; + + // Method: + il.MarkLabel(labelMethodBlock); + // var local0 = base.Deserialize(data); + il.Emit(OpCodes.Ldarg_0); // base + il.Emit(OpCodes.Ldarg_3); // data + il.Emit(OpCodes.Call, MethodInfoCache.StreamingHubClientBase_Deserialize.MakeGenericMethod(deserializeType)); + var local0 = il.DeclareLocal(deserializeType); + il.Emit(OpCodes.Stloc_S, local0); + + // var task = receiver.SomeMethod(local0.Item1, local0.Item2); + il.Emit(OpCodes.Ldarg_0); // receiver + il.Emit(OpCodes.Ldfld, receiverField); + + if (parameters.Length == 0) + { + foreach (var p in methodClientResult.MethodInfo.GetParameters()) + { + // default(CancellationToken) + il.Emit(OpCodes.Ldloca_S, localCtDefault!); + il.Emit(OpCodes.Initobj, typeof(CancellationToken)); + il.Emit(OpCodes.Ldloc_S, localCtDefault!); + } + } + else if (parameters.Length == 1) + { + foreach (var p in methodClientResult.MethodInfo.GetParameters()) + { + if (p.ParameterType == typeof(CancellationToken)) + { + // default(CancellationToken) + il.Emit(OpCodes.Ldloca_S, localCtDefault!); + il.Emit(OpCodes.Initobj, typeof(CancellationToken)); + il.Emit(OpCodes.Ldloc_S, localCtDefault!); + } + else if (p == parameters[0]) + { + // local0 + il.Emit(OpCodes.Ldloc_S, local0); + } + else + { + throw new InvalidOperationException(); + } + } + } + else + { + var itemIndex = 1; + foreach (var p in methodClientResult.MethodInfo.GetParameters()) + { + if (p.ParameterType == typeof(CancellationToken)) + { + // default(CancellationToken) + il.Emit(OpCodes.Ldloca_S, localCtDefault!); + il.Emit(OpCodes.Initobj, typeof(CancellationToken)); + il.Emit(OpCodes.Ldloc_S, localCtDefault!); + } + else + { + // local0.ItemX + il.Emit(OpCodes.Ldloc_S, local0); + il.Emit(OpCodes.Ldfld, deserializeType.GetField($"Item{itemIndex}")!); + itemIndex++; + } + } + } + il.Emit(OpCodes.Callvirt, methodClientResult.MethodInfo); + + // var localTask = task; + var localTask = il.DeclareLocal(methodClientResult.MethodInfo.ReturnType); + il.Emit(OpCodes.Stloc_S, localTask); + + // base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, localTask); + il.Emit(OpCodes.Ldarg_0); // base + il.Emit(OpCodes.Ldarg_1); // methodId + il.Emit(OpCodes.Ldarg_2); // messageId + il.Emit(OpCodes.Ldloc_S, localTask); + il.Emit(OpCodes.Call, MethodInfoCache.GetStreamingHubClientBase_AwaitAndWriteClientResultResponseMessage(methodClientResult.MethodInfo.ReturnType)); + + il.Emit(OpCodes.Leave, labelReturn); + } + // } catch (Exception ex) { + il.BeginCatchBlock(typeof(Exception)); + il.Emit(OpCodes.Stloc_S, localEx); + { + // base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + il.Emit(OpCodes.Ldarg_0); // base + il.Emit(OpCodes.Ldarg_1); // methodId + il.Emit(OpCodes.Ldarg_2); // messageId + il.Emit(OpCodes.Ldloc_S, localEx); // ex + il.Emit(OpCodes.Call, MethodInfoCache.StreamingHubClientBase_WriteClientResultResponseMessageForError); + il.Emit(OpCodes.Leave, labelReturn); + } + il.EndExceptionBlock(); + // } + + // Return: + // return; + il.MarkLabel(labelReturn); + il.Emit(OpCodes.Ret); + } + } // Proxy Methods for (int i = 0; i < definitions.Length; i++) @@ -413,13 +570,15 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece if (def.MethodInfo.ReturnType == typeof(Task) || def.MethodInfo.ReturnType == typeof(ValueTask)) { - var mInfo = baseType.GetMethod("WriteMessageWithResponseAsync", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!; - il.Emit(OpCodes.Callvirt, mInfo.MakeGenericMethod(callType, typeof(Nil))); + il.Emit(OpCodes.Callvirt, MethodInfoCache.StreamingHubClientBase_WriteMessageWithResponseAsync.MakeGenericMethod(callType, typeof(Nil))); + } + else if (def.MethodInfo.ReturnType == typeof(void)) + { + il.Emit(OpCodes.Callvirt, MethodInfoCache.StreamingHubClientBase_WriteMessageFireAndForgetAsync.MakeGenericMethod(callType, typeof(Nil))); } else { - var mInfo = baseType.GetMethod("WriteMessageWithResponseAsync", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!; - il.Emit(OpCodes.Callvirt, mInfo.MakeGenericMethod(callType, def.MethodInfo.ReturnType.GetGenericArguments()[0])); + il.Emit(OpCodes.Callvirt, MethodInfoCache.StreamingHubClientBase_WriteMessageWithResponseAsync.MakeGenericMethod(callType, def.MethodInfo.ReturnType.GetGenericArguments()[0])); } // If the return type is `ValueTask`, the task must be wrapped as ValueTask. @@ -432,6 +591,10 @@ static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, Type rece var returnTypeOfT = def.MethodInfo.ReturnType.GetGenericArguments()[0]; il.Emit(OpCodes.Newobj, typeof(ValueTask<>).MakeGenericType(returnTypeOfT).GetConstructor(new [] { typeof(Task<>).MakeGenericType(returnTypeOfT) })!); } + else if (def.MethodInfo.ReturnType == typeof(void)) + { + il.Emit(OpCodes.Pop); + } il.Emit(OpCodes.Ret); } @@ -515,7 +678,7 @@ static void DefineMethodsFireAndForget(TypeBuilder typeBuilder, Type interfaceTy } Type responseType; - if (def.MethodInfo.ReturnType == typeof(Task) || def.MethodInfo.ReturnType == typeof(ValueTask)) + if (def.MethodInfo.ReturnType == typeof(Task) || def.MethodInfo.ReturnType == typeof(ValueTask) || def.MethodInfo.ReturnType == typeof(void)) { responseType = typeof(Nil); } @@ -538,11 +701,58 @@ static void DefineMethodsFireAndForget(TypeBuilder typeBuilder, Type interfaceTy var returnTypeOfT = def.MethodInfo.ReturnType.GetGenericArguments()[0]; il.Emit(OpCodes.Newobj, typeof(ValueTask<>).MakeGenericType(returnTypeOfT).GetConstructor(new [] { typeof(Task<>).MakeGenericType(returnTypeOfT) })!); } + else if (def.MethodInfo.ReturnType == typeof(void)) + { + il.Emit(OpCodes.Pop); + } il.Emit(OpCodes.Ret); } } + static class MethodInfoCache + { + // ReSharper disable StaticMemberInGenericType + // ReSharper disable InconsistentNaming +#pragma warning disable IDE1006 // Naming Styles + public static readonly MethodInfo StreamingHubClientBase_Deserialize + = typeof(StreamingHubClientBase).GetMethod("Deserialize", BindingFlags.Instance | BindingFlags.NonPublic)!; + + static readonly MethodInfo StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_Task; + static readonly MethodInfo StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_TaskOfT; + static readonly MethodInfo StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTask; + static readonly MethodInfo StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTaskOfT; + + public static MethodInfo GetStreamingHubClientBase_AwaitAndWriteClientResultResponseMessage(Type t) + => t == typeof(Task) ? StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_Task + : t == typeof(ValueTask) ? StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTask + : t.GetGenericTypeDefinition() == typeof(Task<>) ? StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_TaskOfT.MakeGenericMethod(t.GetGenericArguments()) + : t.GetGenericTypeDefinition() == typeof(ValueTask<>) ? StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTaskOfT.MakeGenericMethod(t.GetGenericArguments()) + : throw new InvalidOperationException(); + + public static readonly MethodInfo StreamingHubClientBase_WriteClientResultResponseMessageForError + = typeof(StreamingHubClientBase).GetMethod("WriteClientResultResponseMessageForError", BindingFlags.NonPublic | BindingFlags.Instance)!; + public static readonly MethodInfo StreamingHubClientBase_WriteMessageWithResponseAsync + = typeof(StreamingHubClientBase).GetMethod("WriteMessageWithResponseAsync", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!; + public static readonly MethodInfo StreamingHubClientBase_WriteMessageFireAndForgetAsync + = typeof(StreamingHubClientBase).GetMethod("WriteMessageFireAndForgetAsync", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!; + // ReSharper restore StaticMemberInGenericType + // ReSharper restore InconsistentNaming +#pragma warning restore IDE1006 // Naming Styles + + static MethodInfoCache() + { + var methodsAwaitAndWriteClientResultResponseMessage = typeof(StreamingHubClientBase) + .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(x => x.Name == "AwaitAndWriteClientResultResponseMessage") + .ToArray(); + + StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_Task = methodsAwaitAndWriteClientResultResponseMessage.Single(x => x.GetParameters()[2].ParameterType == typeof(Task)); + StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_TaskOfT = methodsAwaitAndWriteClientResultResponseMessage.Single(x => x.GetParameters()[2].ParameterType is { IsGenericType: true } paramType && paramType.GetGenericTypeDefinition() == typeof(Task<>)); + StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTask = methodsAwaitAndWriteClientResultResponseMessage.Single(x => x.GetParameters()[2].ParameterType == typeof(ValueTask)); + StreamingHubClientBase_AwaitAndWriteClientResultResponseMessage_ValueTaskOfT = methodsAwaitAndWriteClientResultResponseMessage.Single(x => x.GetParameters()[2].ParameterType is { IsGenericType: true } paramType && paramType.GetGenericTypeDefinition() == typeof(ValueTask<>)); + } + } class MethodDefinition { diff --git a/src/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientFactoryProvider.cs b/src/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientFactoryProvider.cs index 59256a5a0..6f82872fa 100644 --- a/src/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientFactoryProvider.cs +++ b/src/MagicOnion.Client/DynamicClient/DynamicStreamingHubClientFactoryProvider.cs @@ -31,8 +31,7 @@ public bool TryGetFactory([NotNullWhen(true)] out Stre static class Cache where TStreamingHub : IStreamingHub { public static readonly StreamingHubClientFactoryDelegate Factory - = (callInvoker, receiver, host, callOptions, serializerProvider, logger) - => (TStreamingHub)Activator.CreateInstance(DynamicStreamingHubClientBuilder.ClientType, callInvoker, host, callOptions, serializerProvider, logger)!; + = (receiver, callInvoker, options) => (TStreamingHub)Activator.CreateInstance(DynamicStreamingHubClientBuilder.ClientType, receiver, callInvoker, options)!; } } #endif diff --git a/src/MagicOnion.Client/Internal/DictionaryExtensions.cs b/src/MagicOnion.Client/Internal/DictionaryExtensions.cs new file mode 100644 index 000000000..ca34940c8 --- /dev/null +++ b/src/MagicOnion.Client/Internal/DictionaryExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +// ReSharper disable once CheckNamespace +namespace System.Collections.Generic +{ + internal static class DictionaryExtensions + { +#if NETSTANDARD2_0 + public static bool Remove(this IDictionary dict, TKey key, [NotNullWhen(true)] out TValue? value) + { + if (dict.TryGetValue(key, out var v)) + { + dict.Remove(key); + value = v!; + return true; + } + + value = default; + return false; + } +#endif + } +} diff --git a/src/MagicOnion.Client/MagicOnion.Client.csproj b/src/MagicOnion.Client/MagicOnion.Client.csproj index 293255c72..962af34cf 100644 --- a/src/MagicOnion.Client/MagicOnion.Client.csproj +++ b/src/MagicOnion.Client/MagicOnion.Client.csproj @@ -1,7 +1,7 @@ - netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 + netstandard2.0;netstandard2.1;net6.0;net8.0 $(_LangVersionUnityBaseline) enable @@ -17,14 +17,12 @@ - - - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -70,5 +68,6 @@ + diff --git a/src/MagicOnion.Client/MagicOnionClientBase.cs b/src/MagicOnion.Client/MagicOnionClientBase.cs index 9a370287c..035841aca 100644 --- a/src/MagicOnion.Client/MagicOnionClientBase.cs +++ b/src/MagicOnion.Client/MagicOnionClientBase.cs @@ -6,14 +6,14 @@ namespace MagicOnion.Client { - public readonly struct MagicOnionClientOptions + public class MagicOnionClientOptions { public string? Host { get; } public CallInvoker CallInvoker { get; } public IReadOnlyList Filters { get; } public CallOptions CallOptions { get; } - public MagicOnionClientOptions(CallInvoker callInvoker, string? host, CallOptions callOptions, IReadOnlyList filters) + public MagicOnionClientOptions(CallInvoker callInvoker, string? host, CallOptions callOptions, IReadOnlyList? filters) { Host = host; CallOptions = callOptions; @@ -22,11 +22,11 @@ public MagicOnionClientOptions(CallInvoker callInvoker, string? host, CallOption } public MagicOnionClientOptions WithCallOptions(CallOptions callOptions) - => new MagicOnionClientOptions(CallInvoker, Host, callOptions, Filters); + => new (CallInvoker, Host, callOptions, Filters); public MagicOnionClientOptions WithHost(string? host) - => new MagicOnionClientOptions(CallInvoker, host, CallOptions, Filters); + => new (CallInvoker, host, CallOptions, Filters); public MagicOnionClientOptions WithFilters(IReadOnlyList filters) - => new MagicOnionClientOptions(CallInvoker, Host, CallOptions, filters); + => new (CallInvoker, Host, CallOptions, filters); } public class MagicOnionClientBase diff --git a/src/MagicOnion.Client/StreamingHubClient.cs b/src/MagicOnion.Client/StreamingHubClient.cs index 879e46285..833b91939 100644 --- a/src/MagicOnion.Client/StreamingHubClient.cs +++ b/src/MagicOnion.Client/StreamingHubClient.cs @@ -1,6 +1,5 @@ using Grpc.Core; using MagicOnion.Serialization; -using MessagePack; using System; using System.Threading; using System.Threading.Tasks; @@ -22,10 +21,17 @@ public static partial class StreamingHubClient return hubClient; } - public static async Task ConnectAsync(ChannelBase channel, TReceiver receiver, string? host = null, CallOptions option = default(CallOptions), IMagicOnionSerializerProvider? serializerProvider = null, IStreamingHubClientFactoryProvider? factoryProvider = null, IMagicOnionClientLogger? logger = null, CancellationToken cancellationToken = default) + public static Task ConnectAsync(ChannelBase channel, TReceiver receiver, string? host = null, CallOptions option = default(CallOptions), IMagicOnionSerializerProvider? serializerProvider = null, IStreamingHubClientFactoryProvider? factoryProvider = null, IMagicOnionClientLogger? logger = null, CancellationToken cancellationToken = default) where TStreamingHub : IStreamingHub { - var hubClient = await ConnectAsync(channel.CreateCallInvoker(), receiver, host, option, serializerProvider, factoryProvider, logger, cancellationToken); + var options = StreamingHubClientOptions.CreateWithDefault(host, option, serializerProvider, logger); + return ConnectAsync(channel, receiver, options, factoryProvider, cancellationToken); + } + + public static async Task ConnectAsync(ChannelBase channel, TReceiver receiver, StreamingHubClientOptions options, IStreamingHubClientFactoryProvider? factoryProvider = null, CancellationToken cancellationToken = default) + where TStreamingHub : IStreamingHub + { + var hubClient = await ConnectAsync(channel.CreateCallInvoker(), receiver, options, factoryProvider, cancellationToken); // ReSharper disable once SuspiciousTypeConversion.Global if (channel is IMagicOnionAwareGrpcChannel magicOnionAwareGrpcChannel) { @@ -38,11 +44,12 @@ public static partial class StreamingHubClient public static TStreamingHub Connect(CallInvoker callInvoker, TReceiver receiver, string? host = null, CallOptions option = default(CallOptions), IMagicOnionSerializerProvider? serializerProvider = null, IStreamingHubClientFactoryProvider? factoryProvider = null, IMagicOnionClientLogger? logger = null) where TStreamingHub : IStreamingHub { - var client = CreateClient(callInvoker, receiver, host, option, serializerProvider, factoryProvider, logger); + var options = StreamingHubClientOptions.CreateWithDefault(host, option); + var client = CreateClient(receiver, callInvoker, options, factoryProvider); async void ConnectAndForget() { - var task = client.__ConnectAndSubscribeAsync(receiver, CancellationToken.None); + var task = client.__ConnectAndSubscribeAsync(CancellationToken.None); try { await task.ConfigureAwait(false); @@ -58,27 +65,32 @@ async void ConnectAndForget() return (TStreamingHub)(object)client; } - public static async Task ConnectAsync(CallInvoker callInvoker, TReceiver receiver, string? host = null, CallOptions option = default(CallOptions), IMagicOnionSerializerProvider? serializerProvider = null, IStreamingHubClientFactoryProvider? factoryProvider = null, IMagicOnionClientLogger? logger = null, CancellationToken cancellationToken = default) + public static Task ConnectAsync(CallInvoker callInvoker, TReceiver receiver, string? host = null, CallOptions option = default(CallOptions), IMagicOnionSerializerProvider? serializerProvider = null, IStreamingHubClientFactoryProvider? factoryProvider = null, IMagicOnionClientLogger? logger = null, CancellationToken cancellationToken = default) + where TStreamingHub : IStreamingHub + { + var options = StreamingHubClientOptions.CreateWithDefault(host, option, serializerProvider, logger); + return ConnectAsync(callInvoker, receiver, options, factoryProvider, cancellationToken); + } + + public static async Task ConnectAsync(CallInvoker callInvoker, TReceiver receiver, StreamingHubClientOptions options, IStreamingHubClientFactoryProvider? factoryProvider = null, CancellationToken cancellationToken = default) where TStreamingHub : IStreamingHub { - var client = CreateClient(callInvoker, receiver, host, option, serializerProvider, factoryProvider, logger); - await client.__ConnectAndSubscribeAsync(receiver, cancellationToken).ConfigureAwait(false); + var client = CreateClient(receiver, callInvoker, options, factoryProvider); + await client.__ConnectAndSubscribeAsync(cancellationToken).ConfigureAwait(false); return (TStreamingHub)(object)client; } - static StreamingHubClientBase CreateClient(CallInvoker callInvoker, TReceiver receiver, string? host, CallOptions option, IMagicOnionSerializerProvider? serializerProvider, IStreamingHubClientFactoryProvider? factoryProvider, IMagicOnionClientLogger? logger) + static StreamingHubClientBase CreateClient(TReceiver receiver, CallInvoker callInvoker, StreamingHubClientOptions options, IStreamingHubClientFactoryProvider? factoryProvider) where TStreamingHub : IStreamingHub { - serializerProvider ??= MagicOnionSerializerProvider.Default; factoryProvider ??= StreamingHubClientFactoryProvider.Default; - logger ??= NullMagicOnionClientLogger.Instance; if (!factoryProvider.TryGetFactory(out var factory)) { throw new NotSupportedException($"Unable to get client factory for StreamingHub type '{typeof(TStreamingHub).FullName}'."); } - return (StreamingHubClientBase)(object)factory(callInvoker, receiver, host, option, serializerProvider, logger); + return (StreamingHubClientBase)(object)factory(receiver, callInvoker, options); } } } diff --git a/src/MagicOnion.Client/StreamingHubClientBase.cs b/src/MagicOnion.Client/StreamingHubClientBase.cs index a0d8da805..fcb51a355 100644 --- a/src/MagicOnion.Client/StreamingHubClientBase.cs +++ b/src/MagicOnion.Client/StreamingHubClientBase.cs @@ -1,20 +1,75 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Buffers; +using System.Collections.Concurrent; using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Channels; using Grpc.Core; using MagicOnion.Client.Internal.Threading; using MagicOnion.Client.Internal.Threading.Tasks; using MagicOnion.Internal; using MagicOnion.Serialization; using MagicOnion.Internal.Buffers; -using MessagePack; namespace MagicOnion.Client { + public class StreamingHubClientOptions + { + public string? Host { get; } + public CallOptions CallOptions { get; } + public IMagicOnionSerializerProvider SerializerProvider { get; } + public IMagicOnionClientLogger Logger { get; } + + public TimeSpan? HeartbeatInterval { get; } + public Action>? HeartbeatReceivedFromServer { get; } + + public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger) + : this(host, callOptions, serializerProvider, logger, default, default) + { + } + + public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? heartbeatInterval, Action>? heartbeatReceivedFromServer) + { + Host = host; + CallOptions = callOptions; + SerializerProvider = serializerProvider ?? throw new ArgumentNullException(nameof(serializerProvider)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + HeartbeatInterval = heartbeatInterval; + HeartbeatReceivedFromServer = heartbeatReceivedFromServer; + } + + public static StreamingHubClientOptions CreateWithDefault(string? host = default, CallOptions callOptions = default, IMagicOnionSerializerProvider? serializerProvider = default, IMagicOnionClientLogger? logger = default) + => new(host, callOptions, serializerProvider ?? MagicOnionSerializerProvider.Default, logger ?? NullMagicOnionClientLogger.Instance); + + public StreamingHubClientOptions WithHost(string? host) + => new(host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + public StreamingHubClientOptions WithCallOptions(CallOptions callOptions) + => new(Host, callOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + public StreamingHubClientOptions WithSerializerProvider(IMagicOnionSerializerProvider serializerProvider) + => new(Host, CallOptions, serializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + public StreamingHubClientOptions WithLogger(IMagicOnionClientLogger logger) + => new(Host, CallOptions, SerializerProvider, logger, HeartbeatInterval, HeartbeatReceivedFromServer); + + /// + /// Sets a heartbeat interval. If a value is , the heartbeat from the client is disabled. + /// + /// + /// + public StreamingHubClientOptions WithHeartbeatInterval(TimeSpan? interval) + => new(Host, CallOptions, SerializerProvider, Logger, interval, HeartbeatReceivedFromServer); + + /// + /// Sets a heartbeat callback. If additional metadata is provided by the server in the heartbeat message, this metadata is provided as an argument. + /// + /// + /// + public StreamingHubClientOptions WithHeartbeatReceived(Action>? onHeartbeatReceived) + => new(Host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, onHeartbeatReceived); + } + public abstract class StreamingHubClientBase where TStreamingHub : IStreamingHub { @@ -23,47 +78,52 @@ public abstract class StreamingHubClientBase const string StreamingHubVersionHeaderValue = "2"; #pragma warning restore IDE1006 // Naming Styles - readonly string? host; - readonly CallOptions option; readonly CallInvoker callInvoker; + readonly StreamingHubClientOptions options; readonly IMagicOnionClientLogger logger; readonly IMagicOnionSerializer messageSerializer; - readonly AsyncLock asyncLock = new AsyncLock(); - readonly Method duplexStreamingConnectMethod; + readonly Method duplexStreamingConnectMethod; + // {messageId, TaskCompletionSource} + readonly Dictionary responseFutures = new(); + readonly TaskCompletionSource waitForDisconnect = new(); + readonly CancellationTokenSource cancellationTokenSource = new(); - IClientStreamWriter writer = default!; - IAsyncStreamReader reader = default!; + readonly Dictionary postCallbackCache = new(); + SendOrPostCallback? heartbeatCallbackCache; - protected TReceiver receiver = default!; - Task subscription = default!; + int messageIdSequence = 0; + bool disposed; - TaskCompletionSource waitForDisconnect = new TaskCompletionSource(); + Task? heartbeatTask; + DateTimeOffset lastHeartbeatSentAt; - // {messageId, TaskCompletionSource} - ConcurrentDictionary responseFutures = new ConcurrentDictionary(); - protected CancellationTokenSource cts = new CancellationTokenSource(); - int messageId = 0; - bool disposed; + readonly Channel writerQueue = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader = true, SingleWriter = false, AllowSynchronousContinuations = false }); + Task? writerTask; + IClientStreamWriter writer = default!; + IAsyncStreamReader reader = default!; + + Task subscription = default!; + + protected readonly TReceiver receiver; - protected StreamingHubClientBase(string serviceName, CallInvoker callInvoker, string? host, CallOptions option, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger) + protected StreamingHubClientBase(string serviceName, TReceiver receiver, CallInvoker callInvoker, StreamingHubClientOptions options) { + this.callInvoker = callInvoker; + this.receiver = receiver; + this.options = options; + this.logger = options.Logger; this.duplexStreamingConnectMethod = CreateConnectMethod(serviceName); - this.callInvoker = callInvoker ?? throw new ArgumentNullException(nameof(callInvoker)); - this.host = host; - this.option = option; - this.messageSerializer = serializerProvider?.Create(MethodType.DuplexStreaming, null) ?? throw new ArgumentNullException(nameof(serializerProvider)); - this.logger = logger ?? NullMagicOnionClientLogger.Instance; + this.messageSerializer = options.SerializerProvider.Create(MethodType.DuplexStreaming, null); } // call immediately after create. - public async Task __ConnectAndSubscribeAsync(TReceiver receiver, CancellationToken cancellationToken) + public async Task __ConnectAndSubscribeAsync(CancellationToken cancellationToken) { var syncContext = SynchronizationContext.Current; // capture SynchronizationContext. - var callResult = callInvoker.AsyncDuplexStreamingCall(duplexStreamingConnectMethod, host, option); + var callResult = callInvoker.AsyncDuplexStreamingCall(duplexStreamingConnectMethod, options.Host, options.CallOptions); this.writer = callResult.RequestStream; this.reader = callResult.ResponseStream; - this.receiver = receiver; // Establish StreamingHub connection between the client and the server. Metadata.Entry? messageVersion; @@ -91,7 +151,7 @@ public async Task __ConnectAndSubscribeAsync(TReceiver receiver, CancellationTok throw new RpcException(e.Status, $"Failed to connect to StreamingHub '{duplexStreamingConnectMethod.ServiceName}'. ({e.Status})"); } - var firstMoveNextTask = reader.MoveNext(CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cts.Token).Token); + var firstMoveNextTask = reader.MoveNext(CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cancellationTokenSource.Token).Token); if (firstMoveNextTask.IsFaulted || messageVersion == null) { // NOTE: Grpc.Net: @@ -111,21 +171,29 @@ public async Task __ConnectAndSubscribeAsync(TReceiver receiver, CancellationTok } // Helper methods to make building clients easy. - protected void SetResultForResponse(object taskCompletionSource, ArraySegment data) + protected void SetResultForResponse(object taskCompletionSource, ReadOnlyMemory data) => ((TaskCompletionSource)taskCompletionSource).TrySetResult(Deserialize(data)); protected void Serialize(IBufferWriter writer, in T value) => messageSerializer.Serialize(writer, value); - protected T Deserialize(ArraySegment bytes) - => messageSerializer.Deserialize(new ReadOnlySequence(bytes)); + protected T Deserialize(ReadOnlyMemory data) + => messageSerializer.Deserialize(new ReadOnlySequence(data)); - protected abstract void OnResponseEvent(int methodId, object taskCompletionSource, ArraySegment data); - protected abstract void OnBroadcastEvent(int methodId, ArraySegment data); + protected abstract void OnClientResultEvent(int methodId, Guid messageId, ReadOnlyMemory data); + protected abstract void OnResponseEvent(int methodId, object taskCompletionSource, ReadOnlyMemory data); + protected abstract void OnBroadcastEvent(int methodId, ReadOnlyMemory data); - static Method CreateConnectMethod(string serviceName) - => new Method(MethodType.DuplexStreaming, serviceName, "Connect", MagicOnionMarshallers.ThroughMarshaller, MagicOnionMarshallers.ThroughMarshaller); + static Method CreateConnectMethod(string serviceName) + => new (MethodType.DuplexStreaming, serviceName, "Connect", MagicOnionMarshallers.StreamingHubMarshaller, MagicOnionMarshallers.StreamingHubMarshaller); async Task StartSubscribe(SynchronizationContext? syncContext, Task firstMoveNext) { + if (options.HeartbeatInterval is { } heartbeatInterval) + { + heartbeatTask = RunHeartbeatLoopAsync(heartbeatInterval, cancellationTokenSource.Token); + } + + writerTask = RunWriterLoopAsync(cancellationTokenSource.Token); + var reader = this.reader; try { @@ -150,7 +218,7 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } } - moveNext = reader.MoveNext(cts.Token); + moveNext = reader.MoveNext(cancellationTokenSource.Token); } } catch (Exception ex) @@ -195,104 +263,211 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM // broadcast: [methodId, [argument]] // response: [messageId, methodId, response] // error-response: [messageId, statusCode, detail, StringMessage] - void ConsumeData(SynchronizationContext? syncContext, byte[] data) + void ConsumeData(SynchronizationContext? syncContext, StreamingHubPayload payload) + { + var messageReader = new StreamingHubClientMessageReader(payload.Memory); + switch (messageReader.ReadMessageType()) + { + case StreamingHubMessageType.Broadcast: + ProcessBroadcast(syncContext, payload, ref messageReader); + break; + case StreamingHubMessageType.Response: + ProcessResponse(syncContext, payload, ref messageReader); + break; + case StreamingHubMessageType.ResponseWithError: + ProcessResponseWithError(syncContext, payload, ref messageReader); + break; + case StreamingHubMessageType.ClientResultRequest: + ProcessClientResultRequest(syncContext, payload, ref messageReader); + break; + case StreamingHubMessageType.Heartbeat: + ProcessHeartbeat(syncContext, payload, ref messageReader); + break; + } + } + + void ProcessBroadcast(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) { - var messagePackReader = new MessagePackReader(data); - var arrayLength = messagePackReader.ReadArrayHeader(); - if (arrayLength == 3) + if (syncContext is null) { - var messageId = messagePackReader.ReadInt32(); - if (responseFutures.TryRemove(messageId, out var future)) + var message = messageReader.ReadBroadcastMessage(); + OnBroadcastEvent(message.MethodId, message.Body); + StreamingHubPayloadPool.Shared.Return(payload); + } + else + { + var (methodId, consumed) = messageReader.ReadBroadcastMessageMethodId(); + if (!postCallbackCache.TryGetValue(methodId, out var postCallback)) { - var methodId = messagePackReader.ReadInt32(); - try - { - var offset = (int)messagePackReader.Consumed; - var rest = new ArraySegment(data, offset, data.Length - offset); - OnResponseEvent(methodId, future, rest); - } - catch (Exception ex) - { - if (!future.TrySetException(ex)) - { - throw; - } - } + // Create and cache a callback delegate capturing `this` and the header size. + postCallback = postCallbackCache[methodId] = CreateBroadcastCallback(methodId, consumed); } + syncContext.Post(postCallback, payload); } - else if (arrayLength == 4) + } + + SendOrPostCallback CreateBroadcastCallback(int methodId, int consumed) + { + return (state) => + { + var p = (StreamingHubPayload)state!; + this.OnBroadcastEvent(methodId, p.Memory.Slice(consumed)); + StreamingHubPayloadPool.Shared.Return(p); + }; + } + + void ProcessResponse(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) + { + var message = messageReader.ReadResponseMessage(); + + ITaskCompletion? future; + lock (responseFutures) { - var messageId = messagePackReader.ReadInt32(); - if (responseFutures.TryRemove(messageId, out var future)) + if (!responseFutures.Remove(message.MessageId, out future)) { - var statusCode = messagePackReader.ReadInt32(); - var detail = messagePackReader.ReadString(); - var offset = (int)messagePackReader.Consumed; - var error = messagePackReader.ReadString(); - var ex = default(RpcException); - if (string.IsNullOrWhiteSpace(error)) - { - ex = new RpcException(new Status((StatusCode)statusCode, detail ?? string.Empty)); - } - else - { - ex = new RpcException(new Status((StatusCode)statusCode, detail ?? string.Empty), detail + Environment.NewLine + error); - } + return; + } + } + + try + { + OnResponseEvent(message.MethodId, future, message.Body); + StreamingHubPayloadPool.Shared.Return(payload); + } + catch (Exception ex) + { + if (!future.TrySetException(ex)) + { + throw; + } + } + } - future.TrySetException(ex); + void ProcessResponseWithError(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) + { + var message = messageReader.ReadResponseWithErrorMessage(); + + ITaskCompletion? future; + lock (responseFutures) + { + if (!responseFutures.Remove(message.MessageId, out future)) + { + return; } } + + RpcException ex; + if (string.IsNullOrWhiteSpace(message.Error)) + { + ex = new RpcException(new Status((StatusCode)message.StatusCode, message.Detail ?? string.Empty)); + } else { - var methodId = messagePackReader.ReadInt32(); - var offset = (int)messagePackReader.Consumed; - if (syncContext != null) + ex = new RpcException(new Status((StatusCode)message.StatusCode, message.Detail ?? string.Empty), message.Detail + Environment.NewLine + message.Error); + } + + future.TrySetException(ex); + StreamingHubPayloadPool.Shared.Return(payload); + } + + void ProcessClientResultRequest(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) + { + var message = messageReader.ReadClientResultRequestMessage(); + if (syncContext is null) + { + OnClientResultEvent(message.MethodId, message.ClientResultRequestMessageId, message.Body); + StreamingHubPayloadPool.Shared.Return(payload); + } + else + { + var tuple = Tuple.Create(this, message.MethodId, message.ClientResultRequestMessageId, message.Body, payload); + syncContext.Post(static state => { - var tuple = Tuple.Create(methodId, data, offset, data.Length - offset); - syncContext.Post(state => - { - var t = (Tuple)state!; - OnBroadcastEvent(t.Item1, new ArraySegment(t.Item2, t.Item3, t.Item4)); - }, tuple); + var t = (Tuple, int, Guid, ReadOnlyMemory, StreamingHubPayload>)state!; + t.Item1.OnClientResultEvent(t.Item2, t.Item3, t.Item4); + StreamingHubPayloadPool.Shared.Return(t.Item5); + }, tuple); + } + } + + void ProcessHeartbeat(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) + { + var metadata = messageReader.ReadHeartbeat(); + if (this.options.HeartbeatReceivedFromServer is { } heartbeatReceived) + { + if (syncContext is null) + { + heartbeatReceived(metadata); + StreamingHubPayloadPool.Shared.Return(payload); } else { - OnBroadcastEvent(methodId, new ArraySegment(data, offset, data.Length - offset)); + heartbeatCallbackCache ??= CreateHeartbeatCallback(heartbeatReceived); + syncContext.Post(heartbeatCallbackCache, payload); } } + WriteHeartbeat(); } - protected async Task WriteMessageFireAndForgetAsync(int methodId, TRequest message) + SendOrPostCallback CreateHeartbeatCallback(Action> heartbeatReceivedAction) => (state) => { - ThrowIfDisposed(); + var p = (StreamingHubPayload)state!; + heartbeatReceivedAction(p.Memory.Slice(5)); + StreamingHubPayloadPool.Shared.Return(p); + }; - byte[] BuildMessage() + async Task RunHeartbeatLoopAsync(TimeSpan heartbeatInterval, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) { - using (var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter()) + await Task.Delay(heartbeatInterval, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + + if ((DateTimeOffset.UtcNow - lastHeartbeatSentAt) > heartbeatInterval) { - var writer = new MessagePackWriter(buffer); - writer.WriteArrayHeader(2); - writer.Write(methodId); - writer.Flush(); - Serialize(buffer, message); - return buffer.WrittenSpan.ToArray(); + WriteHeartbeat(); } } + } - var v = BuildMessage(); - using (await asyncLock.LockAsync().ConfigureAwait(false)) + async Task RunWriterLoopAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) { - await writer.WriteAsync(v).ConfigureAwait(false); + if (await writerQueue.Reader.WaitToReadAsync(default).ConfigureAwait(false)) + { + while (writerQueue.Reader.TryRead(out var payload)) + { + await writer.WriteAsync(payload).ConfigureAwait(false); + } + } } + } + + void WriteHeartbeat() + { + if (disposed) return; + var v = BuildHeartbeatMessage(); + _ = writerQueue.Writer.TryWrite(v); + + lastHeartbeatSentAt = DateTimeOffset.UtcNow; + } + + protected Task WriteMessageFireAndForgetAsync(int methodId, TRequest message) + { + ThrowIfDisposed(); - return default!; + var v = BuildRequestMessage(methodId, message); + _ = writerQueue.Writer.TryWrite(v); + + return Task.FromResult(default!); } - protected async Task WriteMessageWithResponseAsync(int methodId, TRequest message) + protected Task WriteMessageWithResponseAsync(int methodId, TRequest message) { ThrowIfDisposed(); - var mid = Interlocked.Increment(ref messageId); + var mid = Interlocked.Increment(ref messageIdSequence); // NOTE: The continuations (user code) should be executed asynchronously. (Except: Unity WebGL) // This is because the continuation may block the thread, for example, Console.ReadLine(). // If the thread is blocked, it will no longer return to the message consuming loop. @@ -301,29 +476,79 @@ protected async Task WriteMessageWithResponseAsync AwaitAndWriteClientResultResponseMessage(methodId, clientResultMessageId, new ValueTask(task)); + + protected async void AwaitAndWriteClientResultResponseMessage(int methodId, Guid clientResultMessageId, ValueTask task) + { + try { - using (var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter()) - { - var writer = new MessagePackWriter(buffer); - writer.WriteArrayHeader(3); - writer.Write(mid); - writer.Write(methodId); - writer.Flush(); - Serialize(buffer, message); - return buffer.WrittenSpan.ToArray(); - } + await task.ConfigureAwait(false); + await WriteClientResultResponseMessageAsync(methodId, clientResultMessageId, MessagePack.Nil.Default).ConfigureAwait(false); + } + catch (Exception e) + { + await WriteClientResultResponseMessageForErrorAsync(methodId, clientResultMessageId, e).ConfigureAwait(false); + } + } + + protected void AwaitAndWriteClientResultResponseMessage(int methodId, Guid clientResultMessageId, Task task) + => AwaitAndWriteClientResultResponseMessage(methodId, clientResultMessageId, new ValueTask(task)); + + protected async void AwaitAndWriteClientResultResponseMessage(int methodId, Guid clientResultMessageId, ValueTask task) + { + try + { + var result = await task.ConfigureAwait(false); + await WriteClientResultResponseMessageAsync(methodId, clientResultMessageId, result).ConfigureAwait(false); + } + catch (Exception e) + { + await WriteClientResultResponseMessageForErrorAsync(methodId, clientResultMessageId, e).ConfigureAwait(false); } + } - var v = BuildMessage(); - using (await asyncLock.LockAsync().ConfigureAwait(false)) + protected async void WriteClientResultResponseMessageForError(int methodId, Guid clientResultMessageId, Exception ex) + { + try + { + await WriteClientResultResponseMessageForErrorAsync(methodId, clientResultMessageId, ex).ConfigureAwait(false); + } + catch { - await writer.WriteAsync(v).ConfigureAwait(false); + // Ignore Exception } + } + + protected Task WriteClientResultResponseMessageAsync(int methodId, Guid clientResultMessageId, T result) + { + var v = BuildClientResultResponseMessage(methodId, clientResultMessageId, result); + _ = writerQueue.Writer.TryWrite(v); + return Task.CompletedTask; + } + + protected Task WriteClientResultResponseMessageForErrorAsync(int methodId, Guid clientResultMessageId, Exception ex) + { + var statusCode = ex is RpcException rpcException + ? rpcException.StatusCode + : StatusCode.Internal; + + var v = BuildClientResultResponseMessageForError(methodId, clientResultMessageId, (int)statusCode, ex.Message, ex); + _ = writerQueue.Writer.TryWrite(v); - return await tcs.Task.ConfigureAwait(false); // wait until server return response(or error). if connection was closed, throws cancellation from DisposeAsyncCore. + return Task.CompletedTask; } void ThrowIfDisposed() @@ -353,13 +578,14 @@ async Task DisposeAsyncCore(bool waitSubscription) try { + writerQueue.Writer.Complete(); await writer.CompleteAsync().ConfigureAwait(false); } catch { } // ignore error? finally { - cts.Cancel(); - cts.Dispose(); + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); try { if (waitSubscription) @@ -401,5 +627,40 @@ async Task DisposeAsyncCore(bool waitSubscription) } } } + + StreamingHubPayload BuildRequestMessage(int methodId, T message) + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteRequestMessageVoid(buffer, methodId, message, messageSerializer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + StreamingHubPayload BuildRequestMessage(int methodId, int messageId, T message) + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteRequestMessage(buffer, methodId, messageId, message, messageSerializer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + StreamingHubPayload BuildClientResultResponseMessage(int methodId, Guid messageId, T response) + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteClientResultResponseMessage(buffer, methodId, messageId, response, messageSerializer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + StreamingHubPayload BuildClientResultResponseMessageForError(int methodId, Guid messageId, int statusCode, string detail, Exception? ex) + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteClientResultResponseMessageForError(buffer, methodId, messageId, statusCode, detail, ex, messageSerializer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + StreamingHubPayload BuildHeartbeatMessage() + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteHeartbeatMessageForClientToServer(buffer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } } } diff --git a/src/MagicOnion.Client/StreamingHubClientFactoryProvider.cs b/src/MagicOnion.Client/StreamingHubClientFactoryProvider.cs index a1b3a6652..715a47b0f 100644 --- a/src/MagicOnion.Client/StreamingHubClientFactoryProvider.cs +++ b/src/MagicOnion.Client/StreamingHubClientFactoryProvider.cs @@ -22,7 +22,7 @@ public static class StreamingHubClientFactoryProvider #endif } - public delegate TStreamingHub StreamingHubClientFactoryDelegate(CallInvoker callInvoker, TReceiver receiver, string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger) + public delegate TStreamingHub StreamingHubClientFactoryDelegate(TReceiver receiver, CallInvoker callInvoker, StreamingHubClientOptions options) where TStreamingHub : IStreamingHub; /// diff --git a/src/MagicOnion.Internal/BroadcasterHelper.cs b/src/MagicOnion.Internal/BroadcasterHelper.cs index ceaa5b6f8..eab0c5c9d 100644 --- a/src/MagicOnion.Internal/BroadcasterHelper.cs +++ b/src/MagicOnion.Internal/BroadcasterHelper.cs @@ -58,10 +58,10 @@ internal static void VerifyMethodDefinitions(MethodDefinition[] definitions) } map.Add(methodId, item); - if (!(item.MethodInfo.ReturnType == typeof(void))) - { - throw new Exception($"Invalid definition, TReceiver's return type must only be `void`. {item.MethodInfo.Name}."); - } + //if (!(item.MethodInfo.ReturnType == typeof(void))) + //{ + // throw new Exception($"Invalid definition, TReceiver's return type must only be `void`. {item.MethodInfo.Name}."); + //} item.MethodId = methodId; } @@ -74,12 +74,14 @@ internal class MethodDefinition public Type ReceiverType { get; set; } public MethodInfo MethodInfo { get; set; } public int MethodId { get; set; } + public bool IsClientResult { get; set; } public MethodDefinition(Type receiverType, MethodInfo methodInfo, int methodId) { ReceiverType = receiverType; MethodInfo = methodInfo; MethodId = methodId; + IsClientResult = methodInfo.ReturnType != typeof(void); } } } diff --git a/src/MagicOnion.Internal/Buffers/ArrayPoolBufferWriter.cs b/src/MagicOnion.Internal/Buffers/ArrayPoolBufferWriter.cs index db49478bb..8c5806321 100644 --- a/src/MagicOnion.Internal/Buffers/ArrayPoolBufferWriter.cs +++ b/src/MagicOnion.Internal/Buffers/ArrayPoolBufferWriter.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Diagnostics; namespace MagicOnion.Internal.Buffers { @@ -15,7 +16,14 @@ public static ArrayPoolBufferWriter RentThreadStaticWriter() staticInstance = new ArrayPoolBufferWriter(); } staticInstance.Prepare(); + +#if DEBUG + var currentInstance = staticInstance; + staticInstance = null; + return currentInstance; +#else return staticInstance; +#endif } const int MinimumBufferSize = 32767; // use 32k buffer. @@ -96,6 +104,11 @@ public void Dispose() ArrayPool.Shared.Return(buffer); buffer = null; + +#if DEBUG + Debug.Assert(staticInstance is null); + staticInstance = this; +#endif } } } diff --git a/src/MagicOnion.Internal/Buffers/MemoryPoolBufferWriter.cs b/src/MagicOnion.Internal/Buffers/MemoryPoolBufferWriter.cs new file mode 100644 index 000000000..2666b3aba --- /dev/null +++ b/src/MagicOnion.Internal/Buffers/MemoryPoolBufferWriter.cs @@ -0,0 +1,69 @@ +using System; +using System.Buffers; + +namespace MagicOnion.Internal.Buffers +{ + public class MemoryPoolBufferWriter : IBufferWriter + { + readonly MemoryPool memoryPool; + IMemoryOwner? buffer; + int written; + + [ThreadStatic] + static MemoryPoolBufferWriter? shared; + public static MemoryPoolBufferWriter RentThreadStaticWriter() => shared ??= new MemoryPoolBufferWriter(MemoryPool.Shared); + + public MemoryPoolBufferWriter(MemoryPool memoryPool) + { + this.memoryPool = memoryPool; + this.buffer = null; + this.written = 0; + } + + public void Advance(int count) + { + written += count; + } + + public Memory GetMemory(int sizeHint = 0) + { + if (buffer != null && (buffer.Memory.Length - written) > sizeHint) + { + return buffer.Memory.Slice(written); + } + else + { + if (buffer == null) + { + // New + buffer = memoryPool.Rent(sizeHint > 0 ? sizeHint : 32767); + } + else + { + // Grow + var oldBuffer = buffer; + var newBuffer = memoryPool.Rent(buffer.Memory.Length * 2); + + oldBuffer.Memory.Slice(0, written).CopyTo(newBuffer.Memory); + oldBuffer.Dispose(); + + buffer = newBuffer; + } + return buffer.Memory.Slice(written); + } + } + + public Span GetSpan(int sizeHint = 0) + { + return GetMemory(sizeHint).Span; + } + + public (IMemoryOwner Owner, int Written) ToMemoryOwnerAndReturn() + { + var result = (buffer ?? memoryPool.Rent(0), written); + written = 0; + buffer = null; + return result; + } + } +} diff --git a/src/MagicOnion.Internal/MagicOnionMarshallers.cs b/src/MagicOnion.Internal/MagicOnionMarshallers.cs index 6e93ee430..af9233361 100644 --- a/src/MagicOnion.Internal/MagicOnionMarshallers.cs +++ b/src/MagicOnion.Internal/MagicOnionMarshallers.cs @@ -1,6 +1,7 @@ using Grpc.Core; using MessagePack; using System; +using System.Buffers; using System.Linq; using System.Reflection; @@ -15,7 +16,21 @@ internal static class MagicOnionMarshallers .OrderBy(x => x.GetGenericArguments().Length) .ToArray(); - public static readonly Marshaller ThroughMarshaller = new Marshaller(x => x, x => x); + internal static Marshaller StreamingHubMarshaller { get; } = new( + serializer: static (payload, context) => + { + context.SetPayloadLength(payload.Length); + var bufferWriter = context.GetBufferWriter(); + payload.Span.CopyTo(bufferWriter.GetSpan(payload.Length)); + bufferWriter.Advance(payload.Length); + context.Complete(); + StreamingHubPayloadPool.Shared.Return(payload); + }, + deserializer: static context => + { + return StreamingHubPayloadPool.Shared.RentOrCreate(context.PayloadAsReadOnlySequence()); + } + ); internal static Type CreateRequestType(ParameterInfo[] parameters) { diff --git a/src/MagicOnion.Internal/StreamingHubClientMessageReader.cs b/src/MagicOnion.Internal/StreamingHubClientMessageReader.cs new file mode 100644 index 000000000..247caec1f --- /dev/null +++ b/src/MagicOnion.Internal/StreamingHubClientMessageReader.cs @@ -0,0 +1,86 @@ +using System; +using MessagePack; + +namespace MagicOnion.Internal +{ + internal ref struct StreamingHubClientMessageReader + { + readonly ReadOnlyMemory data; + MessagePackReader reader; + + public StreamingHubClientMessageReader(ReadOnlyMemory data) + { + this.data = data; + this.reader = new MessagePackReader(data); + } + + public StreamingHubMessageType ReadMessageType() + { + var arrayLength = this.reader.ReadArrayHeader(); + return arrayLength switch + { + 2 => StreamingHubMessageType.Broadcast, + 3 => StreamingHubMessageType.Response, + 4 => StreamingHubMessageType.ResponseWithError, + 5 => reader.ReadByte() switch + { + 0x00 /* 0:ClientResultRequest */ => StreamingHubMessageType.ClientResultRequest, + 0x7f /* 127:Heartbeat */ => StreamingHubMessageType.Heartbeat, + var x => throw new InvalidOperationException($"Unknown Type: {x}"), + }, + _ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"), + }; + } + + public (int MethodId, int Cosumed) ReadBroadcastMessageMethodId() + { + return (reader.ReadInt32(), (int)reader.Consumed); + } + + public (int MethodId, ReadOnlyMemory Body) ReadBroadcastMessage() + { + var methodId = reader.ReadInt32(); + var offset = (int)reader.Consumed; + return (methodId, data.Slice(offset)); + } + + public (int MessageId, int MethodId, ReadOnlyMemory Body) ReadResponseMessage() + { + var messageId = reader.ReadInt32(); + var methodId = reader.ReadInt32(); + var offset = (int)reader.Consumed; + return (messageId, methodId, data.Slice(offset)); + } + + public (int MessageId, int StatusCode, string? Detail, string? Error) ReadResponseWithErrorMessage() + { + var messageId = reader.ReadInt32(); + var statusCode = reader.ReadInt32(); + var detail = reader.ReadString(); + var error = reader.ReadString(); + + return (messageId, statusCode, detail, error); + } + + public (Guid ClientResultRequestMessageId, int MethodId, ReadOnlyMemory Body) ReadClientResultRequestMessage() + { + //var type = reader.ReadByte(); // Type is already read by ReadMessageType + reader.Skip(); // Dummy + var clientRequestMessageId = MessagePackSerializer.Deserialize(ref reader); + var methodId = reader.ReadInt32(); + var offset = (int)reader.Consumed; + + return (clientRequestMessageId, methodId, data.Slice(offset)); + } + + public ReadOnlyMemory ReadHeartbeat() + { + //var type = reader.ReadByte(); // Type is already read by ReadMessageType + reader.Skip(); // Dummy (1) + reader.Skip(); // Dummy (2) + reader.Skip(); // Dummy (3) + + return data.Slice((int)reader.Consumed); + } + } +} diff --git a/src/MagicOnion.Internal/StreamingHubMessageWriter.cs b/src/MagicOnion.Internal/StreamingHubMessageWriter.cs new file mode 100644 index 000000000..93832281e --- /dev/null +++ b/src/MagicOnion.Internal/StreamingHubMessageWriter.cs @@ -0,0 +1,250 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using MagicOnion.Serialization; +using MessagePack; + +namespace MagicOnion.Internal +{ + /// + /// StreamingHub message formats (from Server to Client): + /// + /// + /// Response: InvokeHubMethod (from server to client) + /// Array(3): [MessageId(int), MethodId(int), SerializedResponse] + /// + /// + /// Response: InvokeHubMethod (from server to client; with Exception) + /// Array(4): [MessageId(int), StatusCode(int), Detail(string), Message(string)] + /// + /// + /// Broadcast: from server to client + /// Array(2): [MethodId(int), SerializedArgument] + /// + /// + /// ClientInvoke/Request: InvokeClientMethod (from server to client) + /// Array(5): [Type=0x00, Nil, ClientResultMessageId(Guid), MethodId(int), SerializedArguments] + /// + /// + /// Heartbeat: + /// Array(5): [Type=0x7f, Nil, Nil, Nil, Extras] + /// + /// + /// StreamingHub message formats (from Client to Server): + /// + /// + /// Request: InvokeHubMethod (from client; void; fire-and-forget) + /// Array(2): [MethodId(int), SerializedArguments] + /// + /// + /// Request: InvokeHubMethod (from client; non-void) + /// Array(3): [MessageId(int), MethodId(int), SerializedArguments] + /// + /// + /// ClientInvoke/Response: InvokeClientMethod (from client to server) + /// Array(4): [Type=0x00, ClientResultMessageId(Guid), MethodId(int), SerializedResponse] + /// + /// + /// ClientInvoke/Response: InvokeClientMethod (from client to server; with Exception) + /// Array(4): [Type=0x01, ClientResultMessageId(Guid), MethodId(int), [StatusCode(int), Detail(string), Message(string)]] + /// + /// + /// Heartbeat/Response: + /// Array(4): [Type=0x7f, Nil, Nil, Nil] + /// + /// + /// + internal static class StreamingHubMessageWriter + { + /// + /// Writes a broadcast message of Hub method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteBroadcastMessage(IBufferWriter bufferWriter, int methodId, T value, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(2); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, value); + } + + /// + /// Writes a request message of Hub method. + /// + public static void WriteRequestMessageVoid(IBufferWriter bufferWriter, int methodId, T value, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(2); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, value); + } + + /// + /// Writes a request message of Hub method. + /// + public static void WriteRequestMessage(IBufferWriter bufferWriter, int methodId, int messageId, T value, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(3); + writer.Write(messageId); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, value); + } + + /// + /// Writes an empty response message of Hub method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteResponseMessage(IBufferWriter bufferWriter, int methodId, int messageId) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(3); + writer.Write(messageId); + writer.Write(methodId); + writer.WriteNil(); + writer.Flush(); + } + + /// + /// Writes a response message of Hub method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteResponseMessage(IBufferWriter bufferWriter, int methodId, int messageId, T v, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(3); + writer.Write(messageId); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, v); + } + + /// + /// Write an error response message of Hub method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteResponseMessageForError(IBufferWriter bufferWriter, int messageId, int statusCode, string detail, Exception? ex, bool isReturnExceptionStackTraceInErrorDetail) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(4); + writer.Write(messageId); + writer.Write(statusCode); + writer.Write(detail); + + var msg = (isReturnExceptionStackTraceInErrorDetail && ex != null) + ? ex.ToString() + : null; + + writer.Write(msg); + writer.Flush(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteClientResultRequestMessage(IBufferWriter bufferWriter, int methodId, Guid messageId, T request, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(5); + writer.Write(0); // Type = ClientResultRequest (0) + writer.WriteNil(); // Dummy + MessagePackSerializer.Serialize(ref writer, messageId); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, request); + } + + /// + /// Writes a response message for client result. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteClientResultResponseMessage(IBufferWriter bufferWriter, int methodId, Guid messageId, T response, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(4); + writer.Write(0); // Result = 0 (success) + MessagePackSerializer.Serialize(ref writer, messageId); + writer.Write(methodId); + writer.Flush(); + messageSerializer.Serialize(bufferWriter, response); + } + + /// + /// Writes an error response message for client result. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteClientResultResponseMessageForError(IBufferWriter bufferWriter, int methodId, Guid messageId, int statusCode, string detail, Exception? ex, IMagicOnionSerializer messageSerializer) + { + var writer = new MessagePackWriter(bufferWriter); + writer.WriteArrayHeader(4); + writer.Write(1); // Result = 1 (failed) + MessagePackSerializer.Serialize(ref writer, messageId); + writer.Write(methodId); + + writer.WriteArrayHeader(3); + { + writer.Write(statusCode); + writer.Write(detail); + writer.Write(ex?.ToString()); + } + writer.Flush(); + } + + + // Array(5)[127, Nil, Nil, Nil, ] + static ReadOnlySpan HeartbeatMessageForServerToClientHeader => new byte[] { 0x95, 0x7f, 0xc0, 0xc0, 0xc0 }; + + /// + /// Writes a heartbeat message for sending from the server. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteHeartbeatMessageForServerToClientHeader(IBufferWriter bufferWriter) + { + bufferWriter.Write(HeartbeatMessageForServerToClientHeader); + //var writer = new MessagePackWriter(bufferWriter); + //writer.WriteArrayHeader(5); + //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat) + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.Flush(); + } + + // Array(4)[127, Nil, Nil, Nil] + static ReadOnlySpan HeartbeatMessageForClientToServer => new byte[] { 0x94, 0x7f, 0xc0, 0xc0, 0xc0 }; + + /// + /// Writes a heartbeat message for sending from the client. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteHeartbeatMessageForClientToServer(IBufferWriter bufferWriter) + { + bufferWriter.Write(HeartbeatMessageForClientToServer); + //var writer = new MessagePackWriter(bufferWriter); + //writer.WriteArrayHeader(4); + //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat) + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.Flush(); + } + } + + internal enum StreamingHubMessageType + { + // Client to Server + Request, + RequestFireAndForget, + Response, + ResponseWithError, + HeartbeatResponse, + + // Server to Client + Broadcast, + ClientResultRequest, + ClientResultResponse, + ClientResultResponseWithError, + Heartbeat, + } +} diff --git a/src/MagicOnion.Internal/StreamingHubPayload.cs b/src/MagicOnion.Internal/StreamingHubPayload.cs new file mode 100644 index 000000000..3bebc7d94 --- /dev/null +++ b/src/MagicOnion.Internal/StreamingHubPayload.cs @@ -0,0 +1,81 @@ +#nullable enable +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; + +namespace MagicOnion.Internal +{ + internal class StreamingHubPayload : IStreamingHubPayload + { + byte[]? buffer; + ReadOnlyMemory? memory; + + public int Length => memory!.Value.Length; + public ReadOnlySpan Span => memory!.Value.Span; + public ReadOnlyMemory Memory => memory!.Value; + + void IStreamingHubPayload.Initialize(ReadOnlySpan data) + { + ThrowIfUsing(); + + buffer = ArrayPool.Shared.Rent(data.Length); + data.CopyTo(buffer); + memory = buffer.AsMemory(0, (int)data.Length); + } + + void IStreamingHubPayload.Initialize(ReadOnlySequence data) + { + ThrowIfUsing(); + if (data.Length > int.MaxValue) throw new InvalidOperationException("A body size of StreamingHubPayload must be less than int.MaxValue"); + + buffer = ArrayPool.Shared.Rent((int)data.Length); + data.CopyTo(buffer); + memory = buffer.AsMemory(0, (int)data.Length); + } + + void IStreamingHubPayload.Initialize(ReadOnlyMemory data) + { + ThrowIfUsing(); + + buffer = null; + memory = data; + } + + void IStreamingHubPayload.Uninitialize() + { + ThrowIfDisposed(); + + if (buffer != null) + { +#if DEBUG && NET6_0_OR_GREATER + Array.Fill(buffer, 0xff); +#endif + ArrayPool.Shared.Return(buffer); + } + + memory = null; + buffer = null; + } + +#if NON_UNITY && !NETSTANDARD2_0 && !NETSTANDARD2_1 + [MemberNotNull(nameof(memory))] +#endif + void ThrowIfDisposed() + { + if (memory is null) throw new ObjectDisposedException(nameof(StreamingHubPayload)); + } + + void ThrowIfUsing() + { + if (memory is not null) throw new InvalidOperationException(nameof(StreamingHubPayload)); + } + } + + internal interface IStreamingHubPayload + { + void Initialize(ReadOnlySpan data); + void Initialize(ReadOnlySequence data); + void Initialize(ReadOnlyMemory data); + void Uninitialize(); + } +} diff --git a/src/MagicOnion.Internal/StreamingHubPayloadPool.BuiltIn.cs b/src/MagicOnion.Internal/StreamingHubPayloadPool.BuiltIn.cs new file mode 100644 index 000000000..68166f3c2 --- /dev/null +++ b/src/MagicOnion.Internal/StreamingHubPayloadPool.BuiltIn.cs @@ -0,0 +1,98 @@ +#if !USE_OBJECTPOOL_STREAMINGHUBPAYLOADPOOL +#nullable enable +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace MagicOnion.Internal +{ + internal class StreamingHubPayloadPool + { + StreamingHubPayload? pool1; + StreamingHubPayload? pool2; + StreamingHubPayload? pool3; + StreamingHubPayload? pool4; + + static StreamingHubPayloadPool pool = new(); + + public static StreamingHubPayloadPool Shared => pool; + + public void Return(StreamingHubPayload payload) + { + ((IStreamingHubPayload)payload).Uninitialize(); + + var pooled = TryReturn(ref pool1, payload) || + TryReturn(ref pool2, payload) || + TryReturn(ref pool3, payload) || + TryReturn(ref pool4, payload); + } + + public StreamingHubPayload RentOrCreate(ReadOnlySequence data) + { + StreamingHubPayload? tmpPayload; + if (!(TryGet(ref pool1, out tmpPayload) || + TryGet(ref pool2, out tmpPayload) || + TryGet(ref pool3, out tmpPayload) || + TryGet(ref pool4, out tmpPayload))) + { + tmpPayload = new StreamingHubPayload(); + } + + ((IStreamingHubPayload)tmpPayload).Initialize(data); + + return tmpPayload; + } + + public StreamingHubPayload RentOrCreate(ReadOnlySpan data) + { + StreamingHubPayload? tmpPayload; + if (!(TryGet(ref pool1, out tmpPayload) || + TryGet(ref pool2, out tmpPayload) || + TryGet(ref pool3, out tmpPayload) || + TryGet(ref pool4, out tmpPayload))) + { + tmpPayload = new StreamingHubPayload(); + } + + ((IStreamingHubPayload)tmpPayload).Initialize(data); + + return tmpPayload; + } + + public StreamingHubPayload RentOrCreate(ReadOnlyMemory data) + { + StreamingHubPayload? tmpPayload; + if (!(TryGet(ref pool1, out tmpPayload) || + TryGet(ref pool2, out tmpPayload) || + TryGet(ref pool3, out tmpPayload) || + TryGet(ref pool4, out tmpPayload))) + { + tmpPayload = new StreamingHubPayload(); + } + + ((IStreamingHubPayload)tmpPayload).Initialize(data); + + return tmpPayload; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool TryReturn(ref StreamingHubPayload? field, StreamingHubPayload payload) + => Interlocked.CompareExchange(ref field, payload, null) == null; + + bool TryGet(ref StreamingHubPayload? field, [NotNullWhen(true)] out StreamingHubPayload? payload) + { + var tmp = field; + if (tmp != null && Interlocked.CompareExchange(ref field, null, tmp) == tmp) + { + payload = tmp; + return true; + } + + payload = null; + return false; + } + } +} +#endif diff --git a/src/MagicOnion.Internal/StreamingHubPayloadPool.ObjectPool.cs b/src/MagicOnion.Internal/StreamingHubPayloadPool.ObjectPool.cs new file mode 100644 index 000000000..ae7abf115 --- /dev/null +++ b/src/MagicOnion.Internal/StreamingHubPayloadPool.ObjectPool.cs @@ -0,0 +1,55 @@ +#if USE_OBJECTPOOL_STREAMINGHUBPAYLOADPOOL +using Microsoft.Extensions.ObjectPool; +using System.Buffers; + +namespace MagicOnion.Internal; + +internal class StreamingHubPayloadPool +{ + const int MaximumRetained = 2 << 7; + + readonly ObjectPool pool = new DefaultObjectPool(new Policy(), MaximumRetained); + + public static StreamingHubPayloadPool Shared { get; } = new StreamingHubPayloadPool(); + + public StreamingHubPayload RentOrCreate(ReadOnlySequence data) + { + var payload = pool.Get(); + ((IStreamingHubPayload)payload).Initialize(data); + return payload; + } + + public StreamingHubPayload RentOrCreate(ReadOnlySpan data) + { + var payload = pool.Get(); + ((IStreamingHubPayload)payload).Initialize(data); + return payload; + } + + public StreamingHubPayload RentOrCreate(ReadOnlyMemory data) + { + var payload = pool.Get(); + ((IStreamingHubPayload)payload).Initialize(data); + return payload; + } + + public void Return(StreamingHubPayload payload) + { + pool.Return(payload); + } + + class Policy : IPooledObjectPolicy + { + public StreamingHubPayload Create() + { + return new StreamingHubPayload(); + } + + public bool Return(StreamingHubPayload obj) + { + ((IStreamingHubPayload)obj).Uninitialize(); + return true; + } + } +} +#endif diff --git a/src/MagicOnion.Internal/StreamingHubServerMessageReader.cs b/src/MagicOnion.Internal/StreamingHubServerMessageReader.cs new file mode 100644 index 000000000..b1d8326cf --- /dev/null +++ b/src/MagicOnion.Internal/StreamingHubServerMessageReader.cs @@ -0,0 +1,79 @@ +using System; +using MessagePack; + +namespace MagicOnion.Internal +{ + internal ref struct StreamingHubServerMessageReader + { + readonly ReadOnlyMemory data; + MessagePackReader reader; + + public StreamingHubServerMessageReader(ReadOnlyMemory data) + { + this.data = data; + this.reader = new MessagePackReader(data); + } + + public StreamingHubMessageType ReadMessageType() + { + var arrayLength = this.reader.ReadArrayHeader(); + return arrayLength switch + { + 2 => StreamingHubMessageType.RequestFireAndForget, + 3 => StreamingHubMessageType.Request, + 4 => reader.ReadByte() switch + { + 0x00 => StreamingHubMessageType.ClientResultResponse, + 0x01 => StreamingHubMessageType.ClientResultResponseWithError, + 0x7f => StreamingHubMessageType.HeartbeatResponse, + var subType => throw new InvalidOperationException($"Unknown client response message: {subType}"), + }, + _ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"), + }; + } + + public (int MethodId, ReadOnlyMemory Body) ReadRequestFireAndForget() + { + // void: [methodId, [argument]] + var methodId = reader.ReadInt32(); + var consumed = (int)reader.Consumed; + + return (methodId, data.Slice(consumed)); + } + + public (int MessageId, int MethodId, ReadOnlyMemory Body) ReadRequest() + { + // T: [messageId, methodId, [argument]] + var messageId = reader.ReadInt32(); + var methodId = reader.ReadInt32(); + var consumed = (int)reader.Consumed; + + return (messageId, methodId, data.Slice(consumed)); + } + + public (Guid ClientResultMessageId, int ClientMethodId, ReadOnlyMemory Body) ReadClientResultResponse() + { + // T: [0, clientResultMessageId, methodId, result] + var clientResultMessageId = MessagePackSerializer.Deserialize(ref reader); + var clientMethodId = reader.ReadInt32(); + var consumed = (int)reader.Consumed; + + return (clientResultMessageId, clientMethodId, data.Slice(consumed)); + } + + public (Guid ClientResultMessageId, int ClientMethodId, int StatusCode, string Detail, string Message) ReadClientResultResponseForError() + { + // T: [1, clientResultMessageId, methodId, [statusCode, detail, message]] + var clientResultMessageId = MessagePackSerializer.Deserialize(ref reader); + var clientMethodId = reader.ReadInt32(); + var bodyArray = reader.ReadArrayHeader(); + if (bodyArray != 3) throw new InvalidOperationException($"Invalid ClientResponse: The BodyArray length is {bodyArray}"); + + var statusCode = reader.ReadInt32(); + var detail = reader.ReadString() ?? string.Empty; + var message = reader.ReadString() ?? string.Empty; + + return (clientResultMessageId, clientMethodId, statusCode, detail, message); + } + } +} diff --git a/src/MagicOnion.Serialization.MemoryPack/MagicOnion.Serialization.MemoryPack.csproj b/src/MagicOnion.Serialization.MemoryPack/MagicOnion.Serialization.MemoryPack.csproj index e4dd47093..a83fabe12 100644 --- a/src/MagicOnion.Serialization.MemoryPack/MagicOnion.Serialization.MemoryPack.csproj +++ b/src/MagicOnion.Serialization.MemoryPack/MagicOnion.Serialization.MemoryPack.csproj @@ -1,7 +1,7 @@ - netstandard2.1;net7.0 + netstandard2.1;net6.0;net8.0 enable enable diff --git a/src/MagicOnion.Server.HttpGateway/MagicOnion.Server.HttpGateway.csproj b/src/MagicOnion.Server.HttpGateway/MagicOnion.Server.HttpGateway.csproj index 1234d92dc..3a1549d6a 100644 --- a/src/MagicOnion.Server.HttpGateway/MagicOnion.Server.HttpGateway.csproj +++ b/src/MagicOnion.Server.HttpGateway/MagicOnion.Server.HttpGateway.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0 + net6.0;net8.0 enable @@ -26,6 +26,8 @@ + + diff --git a/src/MagicOnion.Server.HttpGateway/MagicOnionHttpGatewayMiddleware.cs b/src/MagicOnion.Server.HttpGateway/MagicOnionHttpGatewayMiddleware.cs index e378bd7ed..2f0dfda38 100644 --- a/src/MagicOnion.Server.HttpGateway/MagicOnionHttpGatewayMiddleware.cs +++ b/src/MagicOnion.Server.HttpGateway/MagicOnionHttpGatewayMiddleware.cs @@ -139,7 +139,8 @@ public async Task Invoke(HttpContext httpContext) handler.MessageSerializer.Serialize(bufferWriter, deserializedObject); var requestObject = bufferWriter.WrittenSpan.ToArray(); - var method = new Method(MethodType.Unary, handler.ServiceName, handler.MethodName, MagicOnionMarshallers.ThroughMarshaller, MagicOnionMarshallers.ThroughMarshaller); + var throughMarshaller = new Marshaller(x => x, x => x); + var method = new Method(MethodType.Unary, handler.ServiceName, handler.MethodName, throughMarshaller, throughMarshaller); // create header var metadata = new Metadata(); diff --git a/src/MagicOnion.Server.HttpGateway/Swagger/SwaggerDefinitionBuilder.cs b/src/MagicOnion.Server.HttpGateway/Swagger/SwaggerDefinitionBuilder.cs index 2b848129f..8744aeff2 100644 --- a/src/MagicOnion.Server.HttpGateway/Swagger/SwaggerDefinitionBuilder.cs +++ b/src/MagicOnion.Server.HttpGateway/Swagger/SwaggerDefinitionBuilder.cs @@ -166,7 +166,9 @@ Schemas.Parameter[] BuildParameters(IDictionary definitions, Xml { BuildSchema(definitions, x.ParameterType); refSchema = new Schema { @ref = BuildSchema(definitions, x.ParameterType) }; +#pragma warning disable SYSLIB0050 var unknownObj = System.Runtime.Serialization.FormatterServices.GetUninitializedObject(x.ParameterType); +#pragma warning restore SYSLIB0050 defaultObjectExample = JsonConvert.SerializeObject(unknownObj, new[] { new Newtonsoft.Json.Converters.StringEnumConverter() }); swaggerDataType = "string"; // object can not attach formData. } @@ -488,4 +490,4 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ return property; } -} \ No newline at end of file +} diff --git a/src/MagicOnion.Server.Redis/MagicOnion.Server.Redis.csproj b/src/MagicOnion.Server.Redis/MagicOnion.Server.Redis.csproj index 58975f5dc..3616f4591 100644 --- a/src/MagicOnion.Server.Redis/MagicOnion.Server.Redis.csproj +++ b/src/MagicOnion.Server.Redis/MagicOnion.Server.Redis.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0 + net6.0;net8.0 enable enable @@ -15,11 +15,7 @@ - - - - - + diff --git a/src/MagicOnion.Server.Redis/MagicOnionServerBuilderRedisExtensions.cs b/src/MagicOnion.Server.Redis/MagicOnionServerBuilderRedisExtensions.cs index b5d7719a3..651293ff4 100644 --- a/src/MagicOnion.Server.Redis/MagicOnionServerBuilderRedisExtensions.cs +++ b/src/MagicOnion.Server.Redis/MagicOnionServerBuilderRedisExtensions.cs @@ -1,19 +1,19 @@ +using Cysharp.Runtime.Multicast; +using Cysharp.Runtime.Multicast.Distributed.Redis; using MagicOnion.Server; -using MagicOnion.Server.Hubs; -using MagicOnion.Server.Redis; using Microsoft.Extensions.DependencyInjection.Extensions; +// ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; public static class MagicOnionServerBuilderRedisExtensions { - public static IMagicOnionServerBuilder UseRedisGroupRepository(this IMagicOnionServerBuilder builder, Action configure, bool registerAsDefault = false) + public static IMagicOnionServerBuilder UseRedisGroup(this IMagicOnionServerBuilder builder, Action configure, bool registerAsDefault = false) { if (registerAsDefault) { - builder.Services.RemoveAll(); - builder.Services.TryAddSingleton(); - + builder.Services.RemoveAll(); + builder.Services.TryAddSingleton(); } builder.Services.Configure(configure); diff --git a/src/MagicOnion.Server.Redis/NativeGuidArrayFormatter.cs b/src/MagicOnion.Server.Redis/NativeGuidArrayFormatter.cs deleted file mode 100644 index f00641ed9..000000000 --- a/src/MagicOnion.Server.Redis/NativeGuidArrayFormatter.cs +++ /dev/null @@ -1,44 +0,0 @@ -using MessagePack; -using MessagePack.Formatters; -using System.Runtime.CompilerServices; - -namespace MagicOnion.Server.Redis; - -internal static class NativeGuidArrayFormatter -{ - static readonly IMessagePackFormatter formatter = NativeGuidFormatter.Instance; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Serialize(ref MessagePackWriter writer, Guid[]? value) - { - if (value == null) - { - writer.WriteNil(); - return; - } - - writer.WriteArrayHeader(value.Length); - for (int i = 0; i < value.Length; i++) - { - formatter.Serialize(ref writer, value[i], MessagePackSerializer.DefaultOptions); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Guid[]? Deserialize(ref MessagePackReader reader) - { - if (reader.TryReadNil()) - { - return null; - } - - var len = reader.ReadArrayHeader(); - var result = new Guid[len]; - for (int i = 0; i < len; i++) - { - result[i] = formatter.Deserialize(ref reader, MessagePackSerializer.DefaultOptions); - } - - return result; - } -} diff --git a/src/MagicOnion.Server.Redis/RedisGroup.cs b/src/MagicOnion.Server.Redis/RedisGroup.cs deleted file mode 100644 index a83f2c313..000000000 --- a/src/MagicOnion.Server.Redis/RedisGroup.cs +++ /dev/null @@ -1,208 +0,0 @@ -using MagicOnion.Serialization; -using MagicOnion.Server.Diagnostics; -using MagicOnion.Server.Hubs; -using MessagePack; -using StackExchange.Redis; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Logging; -using MagicOnion.Internal.Buffers; - -namespace MagicOnion.Server.Redis; - -public class RedisGroupRepository : IGroupRepository -{ - readonly IMagicOnionSerializer messageSerializer; - readonly ILogger logger; - - ConnectionMultiplexer connection; - int db; - - readonly Func factory; - ConcurrentDictionary dictionary = new ConcurrentDictionary(); - - public RedisGroupRepository(IMagicOnionSerializer messageSerializer, RedisGroupOptions redisGroupOptions, ILogger logger) - { - this.messageSerializer = messageSerializer; - this.logger = logger; - this.factory = CreateGroup; - this.connection = redisGroupOptions.ConnectionMultiplexer ?? throw new InvalidOperationException("RedisGroup requires add ConnectionMultiplexer to MagicOnionOptions.ServiceLocator before create it. Please try new MagicOnionOptions{DefaultServiceLocator.Register(new ConnectionMultiplexer)}"); - this.db = redisGroupOptions.Db; - } - - public IGroup GetOrAdd(string groupName) - { - return dictionary.GetOrAdd(groupName, factory); - } - - IGroup CreateGroup(string groupName) - { - return new RedisGroup(groupName, messageSerializer, new ConcurrentDictionaryGroup(groupName, this, messageSerializer, logger), connection.GetSubscriber(), connection.GetDatabase(db)); - } - - public bool TryGet(string groupName, [NotNullWhen(true)] out IGroup? group) - { - return dictionary.TryGetValue(groupName, out group); - } - - public bool TryRemove(string groupName) - { - return dictionary.TryRemove(groupName, out _); - } -} - -public class RedisGroup : IGroup -{ - ISubscriber subscriber; - IGroup inmemoryGroup; - IDatabaseAsync database; - RedisChannel channel; - IMagicOnionSerializer messageSerializer; - ChannelMessageQueue mq; - RedisKey counterKey; - - public RedisGroup(string groupName, IMagicOnionSerializer messageSerializer, IGroup inmemoryGroup, ISubscriber redisSubscriber, IDatabaseAsync database) - { - this.GroupName = groupName; - this.messageSerializer = messageSerializer; - this.channel = new RedisChannel("MagicOnion.Redis.RedisGroup?groupName=" + groupName, RedisChannel.PatternMode.Literal); - this.counterKey = "MagicOnion.Redis.RedisGroup.MemberCount?groupName=" + groupName; - this.inmemoryGroup = inmemoryGroup; - this.subscriber = redisSubscriber; - this.database = database; - - this.mq = redisSubscriber.Subscribe(channel); - mq.OnMessage(message => PublishFromRedisToMemoryGroup(message.Message, this.inmemoryGroup)); - } - - public string GroupName { get; } - - - public IInMemoryStorage GetInMemoryStorage() - where T : class - { - throw new NotSupportedException("InMemoryStorage does not support in RedisGroup."); - } - - public async ValueTask AddAsync(ServiceContext context) - { - await database.StringIncrementAsync(counterKey).ConfigureAwait(false); - await inmemoryGroup.AddAsync(context).ConfigureAwait(false); - } - - public async ValueTask RemoveAsync(ServiceContext context) - { - if (await inmemoryGroup.RemoveAsync(context)) // if inmemoryGroup.Remove succeed, removed from.RedisGroupRepository. - { - if (await database.StringDecrementAsync(counterKey) == 0) - { - await database.KeyDeleteAsync(counterKey).ConfigureAwait(false); - } - await mq.UnsubscribeAsync(); - - return true; - } - else - { - await database.StringDecrementAsync(counterKey).ConfigureAwait(false); - } - - return false; - } - - static Task PublishFromRedisToMemoryGroup(RedisValue value, IGroup group) - { - byte[] buffer = value; - var reader = new MessagePackReader(buffer); - - var len1 = reader.ReadArrayHeader(); - if (len1 == 3) - { - var isExcept = reader.ReadBoolean(); - if (isExcept) - { - var excludes = NativeGuidArrayFormatter.Deserialize(ref reader) ?? Array.Empty(); - var offset = (int)reader.Consumed; - return group.WriteExceptRawAsync(new ArraySegment(buffer, offset, buffer.Length - offset), excludes, fireAndForget: true); - } - else - { - var includes = NativeGuidArrayFormatter.Deserialize(ref reader) ?? Array.Empty(); - var offset = (int)reader.Consumed; - return group.WriteToRawAsync(new ArraySegment(buffer, offset, buffer.Length - offset), includes, fireAndForget: true); - } - } - - return Task.CompletedTask; - } - - public async ValueTask GetMemberCountAsync() - { - return (int)await database.StringGetAsync(counterKey); - } - - public Task WriteAllAsync(int methodId, T value, bool fireAndForget) - { - var flags = (fireAndForget) ? CommandFlags.FireAndForget : CommandFlags.None; - return subscriber.PublishAsync(channel, BuildMessage(methodId, value, null, true), flags); - } - - public Task WriteExceptAsync(int methodId, T value, Guid connectionId, bool fireAndForget) - { - var flags = (fireAndForget) ? CommandFlags.FireAndForget : CommandFlags.None; - return subscriber.PublishAsync(channel, BuildMessage(methodId, value, new[] { connectionId }, true), flags); - } - - public Task WriteExceptAsync(int methodId, T value, Guid[]? connectionIds, bool fireAndForget) - { - var flags = (fireAndForget) ? CommandFlags.FireAndForget : CommandFlags.None; - return subscriber.PublishAsync(channel, BuildMessage(methodId, value, connectionIds, true), flags); - } - - public Task WriteToAsync(int methodId, T value, Guid connectionId, bool fireAndForget) - { - var flags = (fireAndForget) ? CommandFlags.FireAndForget : CommandFlags.None; - return subscriber.PublishAsync(channel, BuildMessage(methodId, value, new[] { connectionId }, false), flags); - } - - public Task WriteToAsync(int methodId, T value, Guid[]? connectionIds, bool fireAndForget) - { - var flags = (fireAndForget) ? CommandFlags.FireAndForget : CommandFlags.None; - return subscriber.PublishAsync(channel, BuildMessage(methodId, value, connectionIds, false), flags); - } - - byte[] BuildMessage(int methodId, T value, Guid[]? connectionIds, bool isExcept) - { - using (var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter()) - { - // redis-format: [isExcept, [connectionIds], [raw-bloadcast-format]] - var writer = new MessagePackWriter(buffer); - - writer.WriteArrayHeader(3); - writer.Write(isExcept); - NativeGuidArrayFormatter.Serialize(ref writer, connectionIds); - - writer.WriteArrayHeader(2); - writer.WriteInt32(methodId); - writer.Flush(); - - messageSerializer.Serialize(buffer, value); - - var result = buffer.WrittenSpan.ToArray(); - return result; - } - } - - - public Task WriteExceptRawAsync(ArraySegment message, Guid[]? exceptConnectionIds, bool fireAndForget) - { - // only for the inmemory routing. - throw new NotSupportedException(); - } - - public Task WriteToRawAsync(ArraySegment message, Guid[]? connectionIds, bool fireAndForget) - { - // only for the inmemory routing. - throw new NotSupportedException(); - } -} diff --git a/src/MagicOnion.Server.Redis/RedisGroupOptions.cs b/src/MagicOnion.Server.Redis/RedisGroupOptions.cs deleted file mode 100644 index 6aafb91ce..000000000 --- a/src/MagicOnion.Server.Redis/RedisGroupOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using StackExchange.Redis; - -namespace MagicOnion.Server.Redis; - -public class RedisGroupOptions -{ - public ConnectionMultiplexer? ConnectionMultiplexer { get; set; } - public int Db { get; set; } = -1; -} diff --git a/src/MagicOnion.Server.Redis/RedisGroupRepositoryFactory.cs b/src/MagicOnion.Server.Redis/RedisGroupRepositoryFactory.cs deleted file mode 100644 index a73b4a1b6..000000000 --- a/src/MagicOnion.Server.Redis/RedisGroupRepositoryFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -using MagicOnion.Serialization; -using MagicOnion.Server.Diagnostics; -using MagicOnion.Server.Hubs; -using MessagePack; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace MagicOnion.Server.Redis; - -public class RedisGroupRepositoryFactory : IGroupRepositoryFactory -{ - readonly RedisGroupOptions options; - readonly ILogger logger; - - public RedisGroupRepositoryFactory(IOptionsMonitor options, ILogger logger) - { - this.options = options.CurrentValue; - this.logger = logger; - } - - public IGroupRepository CreateRepository(IMagicOnionSerializer messageSerializer) - { - return new RedisGroupRepository(messageSerializer, options, logger); - } -} diff --git a/src/MagicOnion.Server/Diagnostics/MagicOnionServerLog.cs b/src/MagicOnion.Server/Diagnostics/MagicOnionServerLog.cs index 3d4c627a2..03153a525 100644 --- a/src/MagicOnion.Server/Diagnostics/MagicOnionServerLog.cs +++ b/src/MagicOnion.Server/Diagnostics/MagicOnionServerLog.cs @@ -67,6 +67,18 @@ static string MethodTypeToString(MethodType type) => [LoggerMessage(EventId = 9, Level = LogLevel.Debug, EventName = nameof(InvokeHubBroadcast), Message = nameof(InvokeHubBroadcast) + " groupName:{groupName} size:{size} broadcastGroupCount:{broadcastGroupCount}")] public static partial void InvokeHubBroadcast(ILogger logger, string groupName, int size, int broadcastGroupCount); + [LoggerMessage(EventId = 10, Level = LogLevel.Debug, EventName = nameof(BeginHeartbeatTimer), Message = nameof(BeginHeartbeatTimer) + " method:{method}, heartbeatInterval:{heartbeatInterval}, timeoutDuration:{timeoutDuration}")] + public static partial void BeginHeartbeatTimer(ILogger logger, string method, TimeSpan heartbeatInterval, TimeSpan timeoutDuration); + + [LoggerMessage(EventId = 11, Level = LogLevel.Debug, EventName = nameof(ShutdownHeartbeatTimer), Message = nameof(ShutdownHeartbeatTimer) + " method:{method}")] + public static partial void ShutdownHeartbeatTimer(ILogger logger, string method); + + [LoggerMessage(EventId = 12, Level = LogLevel.Debug, EventName = nameof(HeartbeatTimedOut), Message = nameof(HeartbeatTimedOut) + " method:{method}, connectionId:{connectionId}")] + public static partial void HeartbeatTimedOut(ILogger logger, string method, Guid connectionId); + + [LoggerMessage(EventId = 13, Level = LogLevel.Debug, EventName = nameof(SendHeartbeat), Message = nameof(SendHeartbeat) + " method:{method}")] + public static partial void SendHeartbeat(ILogger logger, string method); + [LoggerMessage(EventId = 90, Level = LogLevel.Error, EventName = nameof(ErrorOnServiceMethod), Message = "A service handler throws an exception occurred in {method}")] public static partial void ErrorOnServiceMethod(ILogger logger, Exception ex, string method); diff --git a/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs index a6af042a2..9d877bfeb 100644 --- a/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs +++ b/src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs @@ -1,4 +1,7 @@ using System.Reflection; +using Cysharp.Runtime.Multicast; +using Cysharp.Runtime.Multicast.InMemory; +using Cysharp.Runtime.Multicast.Remoting; using Grpc.AspNetCore.Server.Model; using MagicOnion.Server; using MagicOnion.Server.Diagnostics; @@ -16,37 +19,45 @@ public static class MagicOnionServicesExtensions public static IMagicOnionServerBuilder AddMagicOnion(this IServiceCollection services, Action? configureOptions = null) { var configName = Options.Options.DefaultName; - services.AddSingleton(sp => MagicOnionEngine.BuildServerServiceDefinition(sp, sp.GetRequiredService>().Get(configName))); + services.TryAddSingleton(sp => MagicOnionEngine.BuildServerServiceDefinition(sp, sp.GetRequiredService>().Get(configName))); return services.AddMagicOnionCore(configureOptions); } public static IMagicOnionServerBuilder AddMagicOnion(this IServiceCollection services, Assembly[] searchAssemblies, Action? configureOptions = null) { var configName = Options.Options.DefaultName; - services.AddSingleton(sp => MagicOnionEngine.BuildServerServiceDefinition(sp, searchAssemblies, sp.GetRequiredService>().Get(configName))); + services.TryAddSingleton(sp => MagicOnionEngine.BuildServerServiceDefinition(sp, searchAssemblies, sp.GetRequiredService>().Get(configName))); return services.AddMagicOnionCore(configureOptions); } public static IMagicOnionServerBuilder AddMagicOnion(this IServiceCollection services, IEnumerable searchTypes, Action? configureOptions = null) { var configName = Options.Options.DefaultName; - services.AddSingleton(sp => MagicOnionEngine.BuildServerServiceDefinition(sp, searchTypes, sp.GetRequiredService>().Get(configName))); + services.TryAddSingleton(sp => MagicOnionEngine.BuildServerServiceDefinition(sp, searchTypes, sp.GetRequiredService>().Get(configName))); return services.AddMagicOnionCore(configureOptions); } - static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollection services, Action? configureOptions = null) + // NOTE: `internal` is required for unit tests. + internal static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollection services, Action? configureOptions = null) { var configName = Options.Options.DefaultName; - var glueServiceType = MagicOnionGlueService.CreateType(); - services.TryAddSingleton(); + // Required services (ASP.NET Core, gRPC) + services.AddLogging(); + services.AddGrpc(); + services.AddMetrics(); + + // MagicOnion: Core services + var glueServiceType = MagicOnionGlueService.CreateType(); - services.AddSingleton(sp => new MagicOnionServiceDefinitionGlueDescriptor(glueServiceType, sp.GetRequiredService())); + services.TryAddSingleton(); + services.TryAddSingleton(sp => new MagicOnionServiceDefinitionGlueDescriptor(glueServiceType, sp.GetRequiredService())); services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IServiceMethodProvider<>).MakeGenericType(glueServiceType), typeof(MagicOnionGlueServiceMethodProvider<>).MakeGenericType(glueServiceType))); - services.AddMetrics(); + // MagicOnion: Metrics services.TryAddSingleton(); + // MagicOnion: Options services.AddOptions(configName) .Configure((o, configuration) => { @@ -54,6 +65,14 @@ static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollection servic configureOptions?.Invoke(o); }); + // Add: Multicaster + services.TryAddSingleton(DynamicInMemoryProxyFactory.Instance); + services.TryAddSingleton(DynamicRemoteProxyFactory.Instance); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + return new MagicOnionServerBuilder(services); } } diff --git a/src/MagicOnion.Server/Hubs/DynamicBroadcasterBuilder.cs b/src/MagicOnion.Server/Hubs/DynamicBroadcasterBuilder.cs deleted file mode 100644 index 2b6f4c05a..000000000 --- a/src/MagicOnion.Server/Hubs/DynamicBroadcasterBuilder.cs +++ /dev/null @@ -1,249 +0,0 @@ -using MessagePack; -using System.Reflection; -using System.Reflection.Emit; -using Microsoft.Extensions.Logging; -using MagicOnion.Internal.Reflection; -using MagicOnion.Internal; - -namespace MagicOnion.Server.Hubs; -#if ENABLE_SAVE_ASSEMBLY - public -#else -internal -#endif - static class AssemblyHolder -{ - public const string ModuleName = "MagicOnion.Server.Hubs.DynamicBroadcaster"; - - readonly static DynamicAssembly assembly; - internal static DynamicAssembly Assembly { get { return assembly; } } - - static AssemblyHolder() - { - assembly = new DynamicAssembly(ModuleName); - } - -#if ENABLE_SAVE_ASSEMBLY - - public static AssemblyBuilder Save() - { - return assembly.Save(); - } - -#endif -} - -#if ENABLE_SAVE_ASSEMBLY - public -#else -internal -#endif - static class DynamicBroadcasterBuilder -{ - public static readonly Type BroadcasterType; - public static readonly Type BroadcasterType_ExceptOne; - public static readonly Type BroadcasterType_ExceptMany; - // TODO:impl Type - public static readonly Type BroadcasterType_ToOne; - public static readonly Type BroadcasterType_ToMany; - - static readonly MethodInfo groupWriteAllMethodInfo = typeof(IGroup).GetMethod(nameof(IGroup.WriteAllAsync))!; - static readonly MethodInfo groupWriteExceptOneMethodInfo = typeof(IGroup).GetMethods().First(x => x.Name == nameof(IGroup.WriteExceptAsync) && !x.GetParameters()[2].ParameterType.IsArray); - static readonly MethodInfo groupWriteExceptManyMethodInfo = typeof(IGroup).GetMethods().First(x => x.Name == nameof(IGroup.WriteExceptAsync) && x.GetParameters()[2].ParameterType.IsArray); - static readonly MethodInfo groupWriteToOneMethodInfo = typeof(IGroup).GetMethods().First(x => x.Name == nameof(IGroup.WriteToAsync) && !x.GetParameters()[2].ParameterType.IsArray); - static readonly MethodInfo groupWriteToManyMethodInfo = typeof(IGroup).GetMethods().First(x => x.Name == nameof(IGroup.WriteToAsync) && x.GetParameters()[2].ParameterType.IsArray); - - static readonly MethodInfo fireAndForget = typeof(DynamicBroadcasterBuilder).GetMethod(nameof(FireAndForget), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!; - - static DynamicBroadcasterBuilder() - { - var t = typeof(T); - var ti = t.GetTypeInfo(); - if (!ti.IsInterface) throw new Exception("Broadcaster Proxy only allows interface. Type:" + ti.Name); - - var asm = AssemblyHolder.Assembly; - var methodDefinitions = BroadcasterHelper.SearchDefinitions(t); - BroadcasterHelper.VerifyMethodDefinitions(methodDefinitions); - - { - var typeBuilder = asm.DefineType($"{AssemblyHolder.ModuleName}.{ti.FullName}Broadcaster_{Guid.NewGuid().ToString()}", TypeAttributes.Public, typeof(object), new Type[] { t })!; - var (group, ctor) = DefineConstructor(typeBuilder); - DefineMethods(typeBuilder, t, group, methodDefinitions, groupWriteAllMethodInfo, null); - BroadcasterType = typeBuilder.CreateTypeInfo()!.AsType(); - } - { - var typeBuilder = asm.DefineType($"{AssemblyHolder.ModuleName}.{ti.FullName}BroadcasterExceptOne_{Guid.NewGuid().ToString()}", TypeAttributes.Public, typeof(object), new Type[] { t }); - var (group, except, ctor) = DefineConstructor2(typeBuilder); - DefineMethods(typeBuilder, t, group, methodDefinitions, groupWriteExceptOneMethodInfo, except); - BroadcasterType_ExceptOne = typeBuilder.CreateTypeInfo()!.AsType(); - } - { - var typeBuilder = asm.DefineType($"{AssemblyHolder.ModuleName}.{ti.FullName}BroadcasterExceptMany_{Guid.NewGuid().ToString()}", TypeAttributes.Public, typeof(object), new Type[] { t }); - var (group, except, ctor) = DefineConstructor3(typeBuilder); - DefineMethods(typeBuilder, t, group, methodDefinitions, groupWriteExceptManyMethodInfo, except); - BroadcasterType_ExceptMany = typeBuilder.CreateTypeInfo()!.AsType(); - } - { - var typeBuilder = asm.DefineType($"{AssemblyHolder.ModuleName}.{ti.FullName}BroadcasterToOne_{Guid.NewGuid().ToString()}", TypeAttributes.Public, typeof(object), new Type[] { t }); - var (group, to, ctor) = DefineConstructor2(typeBuilder); - DefineMethods(typeBuilder, t, group, methodDefinitions, groupWriteToOneMethodInfo, to); - BroadcasterType_ToOne = typeBuilder.CreateTypeInfo()!.AsType(); - } - { - var typeBuilder = asm.DefineType($"{AssemblyHolder.ModuleName}.{ti.FullName}BroadcasterToMany_{Guid.NewGuid().ToString()}", TypeAttributes.Public, typeof(object), new Type[] { t }); - var (group, to, ctor) = DefineConstructor3(typeBuilder); - DefineMethods(typeBuilder, t, group, methodDefinitions, groupWriteToManyMethodInfo, to); - BroadcasterType_ToMany = typeBuilder.CreateTypeInfo()!.AsType(); - } - } - - static (FieldInfo, ConstructorInfo) DefineConstructor(TypeBuilder typeBuilder) - { - // .ctor(IGroup group) - var groupField = typeBuilder.DefineField("group", typeof(IGroup), FieldAttributes.Private | FieldAttributes.InitOnly); - - var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(IGroup) }); - var il = ctor.GetILGenerator(); - - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, typeof(object).GetConstructors().First()); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Stfld, groupField); - il.Emit(OpCodes.Ret); - - return (groupField, ctor); - } - - static (FieldInfo groupField, FieldInfo exceptField, ConstructorInfo) DefineConstructor2(TypeBuilder typeBuilder) - { - // .ctor(IGroup group, Guid except) - var groupField = typeBuilder.DefineField("group", typeof(IGroup), FieldAttributes.Private | FieldAttributes.InitOnly); - var connectionIdField = typeBuilder.DefineField("connectionId", typeof(Guid), FieldAttributes.Private | FieldAttributes.InitOnly); - - var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(IGroup), typeof(Guid) }); - var il = ctor.GetILGenerator(); - - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, typeof(object).GetConstructors().First()); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Stfld, groupField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Stfld, connectionIdField); - il.Emit(OpCodes.Ret); - - return (groupField, connectionIdField, ctor); - } - - static (FieldInfo groupField, FieldInfo exceptField, ConstructorInfo) DefineConstructor3(TypeBuilder typeBuilder) - { - // .ctor(IGroup group, Guid[] except) - var groupField = typeBuilder.DefineField("group", typeof(IGroup), FieldAttributes.Private | FieldAttributes.InitOnly); - var connectionIdsField = typeBuilder.DefineField("connectionIds", typeof(Guid[]), FieldAttributes.Private | FieldAttributes.InitOnly); - - var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(IGroup), typeof(Guid[]) }); - var il = ctor.GetILGenerator(); - - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, typeof(object).GetConstructors().First()); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Stfld, groupField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Stfld, connectionIdsField); - il.Emit(OpCodes.Ret); - - return (groupField, connectionIdsField, ctor); - } - - static void DefineMethods(TypeBuilder typeBuilder, Type interfaceType, FieldInfo groupField, BroadcasterHelper.MethodDefinition[] definitions, MethodInfo writeMethod, FieldInfo? exceptField) - { - // Proxy Methods - for (int i = 0; i < definitions.Length; i++) - { - var def = definitions[i]; - var parameters = def.MethodInfo.GetParameters().Select(x => x.ParameterType).ToArray(); - - var method = typeBuilder.DefineMethod(def.MethodInfo.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual, - def.MethodInfo.ReturnType, - parameters); - var il = method.GetILGenerator(); - - // like this. - // return group.WriteAllAsync(9013131, new DynamicArgumentTuple(senderId, message)); - // BroadcasterHelper.FireAndForget(...) - - // load group field - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldfld, groupField); - - // arg1 - il.EmitLdc_I4(def.MethodId); - - // create request for arg2 - for (int j = 0; j < parameters.Length; j++) - { - il.Emit(OpCodes.Ldarg, j + 1); - } - - Type? callType = null; - if (parameters.Length == 0) - { - // use Nil. - callType = typeof(Nil); - il.Emit(OpCodes.Ldsfld, typeof(Nil).GetField("Default")!); - } - else if (parameters.Length == 1) - { - // already loaded parameter. - callType = parameters[0]; - } - else - { - // call new DynamicArgumentTuple - callType = BroadcasterHelper.DynamicArgumentTupleTypes[parameters.Length - 2].MakeGenericType(parameters); - il.Emit(OpCodes.Newobj, callType.GetConstructors().First()); - } - - if (writeMethod != groupWriteAllMethodInfo) - { - if (exceptField == null) throw new InvalidOperationException("Non group-wide write method requires except field."); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldfld, exceptField); - } - - if (def.MethodInfo.ReturnType == typeof(void)) - { - il.Emit(OpCodes.Ldc_I4_1); // fireAndForget = true - } - else - { - il.Emit(OpCodes.Ldc_I4_0); // fireAndForget = false - } - - il.Emit(OpCodes.Callvirt, writeMethod.MakeGenericMethod(callType)); - - if (def.MethodInfo.ReturnType == typeof(void)) - { - il.Emit(OpCodes.Call, fireAndForget); - } - - il.Emit(OpCodes.Ret); - } - } - - static async void FireAndForget(Task task) - { - try - { - await task.ConfigureAwait(false); - } - catch (Exception ex) - { - MagicOnionServerInternalLogger.Current.LogError(ex, "exception occured in client broadcast."); - } - } -} diff --git a/src/MagicOnion.Server/Hubs/Group.ConcurrentDictionary.cs b/src/MagicOnion.Server/Hubs/Group.ConcurrentDictionary.cs deleted file mode 100644 index c7da05810..000000000 --- a/src/MagicOnion.Server/Hubs/Group.ConcurrentDictionary.cs +++ /dev/null @@ -1,344 +0,0 @@ -using MagicOnion.Serialization; -using MagicOnion.Server.Diagnostics; -using MessagePack; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Logging; -using MagicOnion.Internal.Buffers; - -namespace MagicOnion.Server.Hubs; - -public class ConcurrentDictionaryGroupRepositoryFactory : IGroupRepositoryFactory -{ - readonly ILogger logger; - - public ConcurrentDictionaryGroupRepositoryFactory(ILogger logger) - { - this.logger = logger; - } - - public IGroupRepository CreateRepository(IMagicOnionSerializer messageSerializer) - { - return new ConcurrentDictionaryGroupRepository(messageSerializer, logger); - } -} - -public class ConcurrentDictionaryGroupRepository : IGroupRepository -{ - readonly IMagicOnionSerializer messageSerializer; - readonly ILogger logger; - - readonly Func factory; - ConcurrentDictionary dictionary = new ConcurrentDictionary(); - - public ConcurrentDictionaryGroupRepository(IMagicOnionSerializer messageSerializer, ILogger logger) - { - this.messageSerializer = messageSerializer; - this.factory = CreateGroup; - this.logger = logger; - } - - public IGroup GetOrAdd(string groupName) - { - return dictionary.GetOrAdd(groupName, factory); - } - - IGroup CreateGroup(string groupName) - { - return new ConcurrentDictionaryGroup(groupName, this, messageSerializer, logger); - } - - public bool TryGet(string groupName, [NotNullWhen(true)] out IGroup? group) - { - return dictionary.TryGetValue(groupName, out group); - } - - public bool TryRemove(string groupName) - { - return dictionary.TryRemove(groupName, out _); - } -} - - -public class ConcurrentDictionaryGroup : IGroup -{ - // ConcurrentDictionary.Count is slow, use external counter. - int approximatelyLength; - - readonly object gate = new object(); - - readonly IGroupRepository parent; - readonly IMagicOnionSerializer messageSerializer; - readonly ILogger logger; - - ConcurrentDictionary> members; - IInMemoryStorage? inmemoryStorage; - - public string GroupName { get; } - - public ConcurrentDictionaryGroup(string groupName, IGroupRepository parent, IMagicOnionSerializer messageSerializer, ILogger logger) - { - this.GroupName = groupName; - this.parent = parent; - this.messageSerializer = messageSerializer; - this.logger = logger; - this.members = new ConcurrentDictionary>(); - } - - public ValueTask GetMemberCountAsync() - { - return new ValueTask(approximatelyLength); - } - - public IInMemoryStorage GetInMemoryStorage() - where T : class - { - lock (gate) - { - if (inmemoryStorage == null) - { - inmemoryStorage = new DefaultInMemoryStorage(); - } - else if (!(inmemoryStorage is IInMemoryStorage)) - { - throw new ArgumentException("already initialized inmemory-storage by another type, inmemory-storage only use single type"); - } - - return (IInMemoryStorage)inmemoryStorage; - } - } - - public ValueTask AddAsync(ServiceContext context) - { - if (members.TryAdd(context.ContextId, (IServiceContextWithResponseStream)context)) - { - Interlocked.Increment(ref approximatelyLength); - } - return default(ValueTask); - } - - public ValueTask RemoveAsync(ServiceContext context) - { - if (members.TryRemove(context.ContextId, out _)) - { - Interlocked.Decrement(ref approximatelyLength); - if (inmemoryStorage != null) - { - inmemoryStorage.Remove(context.ContextId); - } - } - - if (members.Count == 0) - { - if (parent.TryRemove(GroupName)) - { - return new ValueTask(true); - } - } - return new ValueTask(false); - } - - // broadcast: [methodId, [argument]] - - public Task WriteAllAsync(int methodId, T value, bool fireAndForget) - { - var message = BuildMessage(methodId, value); - - if (fireAndForget) - { - var writeCount = 0; - foreach (var item in members) - { - item.Value.QueueResponseStreamWrite(message); - writeCount++; - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteExceptAsync(int methodId, T value, Guid connectionId, bool fireAndForget) - { - var message = BuildMessage(methodId, value); - if (fireAndForget) - { - var writeCount = 0; - foreach (var item in members) - { - if (item.Value.ContextId != connectionId) - { - item.Value.QueueResponseStreamWrite(message); - writeCount++; - } - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteExceptAsync(int methodId, T value, Guid[] connectionIds, bool fireAndForget) - { - var message = BuildMessage(methodId, value); - if (fireAndForget) - { - var writeCount = 0; - foreach (var item in members) - { - foreach (var item2 in connectionIds) - { - if (item.Value.ContextId == item2) - { - goto NEXT; - } - } - item.Value.QueueResponseStreamWrite(message); - writeCount++; - NEXT: - continue; - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteToAsync(int methodId, T value, Guid connectionId, bool fireAndForget) - { - var message = BuildMessage(methodId, value); - if (fireAndForget) - { - if (members.TryGetValue(connectionId, out var context)) - { - context.QueueResponseStreamWrite(message); - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, 1); - } - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteToAsync(int methodId, T value, Guid[] connectionIds, bool fireAndForget) - { - var message = BuildMessage(methodId, value); - if (fireAndForget) - { - var writeCount = 0; - foreach (var item in connectionIds) - { - if (members.TryGetValue(item, out var context)) - { - context.QueueResponseStreamWrite(message); - writeCount++; - } - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteExceptRawAsync(ArraySegment msg, Guid[] exceptConnectionIds, bool fireAndForget) - { - // oh, copy is bad but current gRPC interface only accepts byte[]... - var message = new byte[msg.Count]; - Array.Copy(msg.Array!, msg.Offset, message, 0, message.Length); - if (fireAndForget) - { - if (exceptConnectionIds == null) - { - var writeCount = 0; - foreach (var item in members) - { - item.Value.QueueResponseStreamWrite(message); - writeCount++; - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - return Task.CompletedTask; - } - else - { - var writeCount = 0; - foreach (var item in members) - { - foreach (var item2 in exceptConnectionIds) - { - if (item.Value.ContextId == item2) - { - goto NEXT; - } - } - item.Value.QueueResponseStreamWrite(message); - writeCount++; - NEXT: - continue; - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - return Task.CompletedTask; - } - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteToRawAsync(ArraySegment msg, Guid[] connectionIds, bool fireAndForget) - { - // oh, copy is bad but current gRPC interface only accepts byte[]... - var message = new byte[msg.Count]; - Array.Copy(msg.Array!, msg.Offset, message, 0, message.Length); - if (fireAndForget) - { - if (connectionIds != null) - { - var writeCount = 0; - foreach (var item in connectionIds) - { - if (members.TryGetValue(item, out var context)) - { - context.QueueResponseStreamWrite(message); - writeCount++; - } - } - - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - } - - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - byte[] BuildMessage(int methodId, T value) - { - using (var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter()) - { - var writer = new MessagePackWriter(buffer); - writer.WriteArrayHeader(2); - writer.WriteInt32(methodId); - writer.Flush(); - messageSerializer.Serialize(buffer, value); - return buffer.WrittenSpan.ToArray(); - } - } -} diff --git a/src/MagicOnion.Server/Hubs/Group.ImmutableArray.cs b/src/MagicOnion.Server/Hubs/Group.ImmutableArray.cs deleted file mode 100644 index b67ead321..000000000 --- a/src/MagicOnion.Server/Hubs/Group.ImmutableArray.cs +++ /dev/null @@ -1,373 +0,0 @@ -using MagicOnion.Serialization; -using MagicOnion.Server.Diagnostics; -using MessagePack; -using System.Collections.Concurrent; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Logging; -using MagicOnion.Internal.Buffers; - -namespace MagicOnion.Server.Hubs; - -public class ImmutableArrayGroupRepositoryFactory : IGroupRepositoryFactory -{ - readonly ILogger logger; - - public ImmutableArrayGroupRepositoryFactory(ILogger logger) - { - this.logger = logger; - } - - public IGroupRepository CreateRepository(IMagicOnionSerializer messageSerializer) - { - return new ImmutableArrayGroupRepository(messageSerializer, logger); - } -} - -public class ImmutableArrayGroupRepository : IGroupRepository -{ - readonly IMagicOnionSerializer messageSerializer; - readonly ILogger logger; - - readonly Func factory; - ConcurrentDictionary dictionary = new ConcurrentDictionary(); - - public ImmutableArrayGroupRepository(IMagicOnionSerializer messageSerializer, ILogger logger) - { - this.messageSerializer = messageSerializer; - this.factory = CreateGroup; - this.logger = logger; - } - - public IGroup GetOrAdd(string groupName) - { - return dictionary.GetOrAdd(groupName, factory); - } - - IGroup CreateGroup(string groupName) - { - return new ImmutableArrayGroup(groupName, this, messageSerializer, logger); - } - - public bool TryGet(string groupName, [NotNullWhen(true)] out IGroup? group) - { - return dictionary.TryGetValue(groupName, out group); - } - - public bool TryRemove(string groupName) - { - return dictionary.TryRemove(groupName, out _); - } -} - -public class ImmutableArrayGroup : IGroup -{ - readonly object gate = new object(); - readonly IGroupRepository parent; - readonly IMagicOnionSerializer messageSerializer; - readonly ILogger logger; - - ImmutableArray> members; - IInMemoryStorage? inmemoryStorage; - - public string GroupName { get; } - - public ImmutableArrayGroup(string groupName, IGroupRepository parent, IMagicOnionSerializer messageSerializer, ILogger logger) - { - this.GroupName = groupName; - this.parent = parent; - this.messageSerializer = messageSerializer; - this.logger = logger; - this.members = ImmutableArray>.Empty; - } - - public ValueTask GetMemberCountAsync() - { - return new ValueTask(members.Length); - } - - public IInMemoryStorage GetInMemoryStorage() - where T : class - { - lock (gate) - { - if (inmemoryStorage == null) - { - inmemoryStorage = new DefaultInMemoryStorage(); - } - else if (!(inmemoryStorage is IInMemoryStorage)) - { - throw new ArgumentException("already initialized inmemory-storage by another type, inmemory-storage only use single type"); - } - - return (IInMemoryStorage)inmemoryStorage; - } - } - - public ValueTask AddAsync(ServiceContext context) - { - lock (gate) - { - members = members.Add((IServiceContextWithResponseStream)context); - } - return default(ValueTask); - } - - public ValueTask RemoveAsync(ServiceContext context) - { - lock (gate) - { - if (!members.IsEmpty) - { - members = members.Remove((IServiceContextWithResponseStream)context); - if (inmemoryStorage != null) - { - inmemoryStorage.Remove(context.ContextId); - } - - if (members.Length == 0) - { - if (parent.TryRemove(GroupName)) - { - return new ValueTask(true); - } - } - } - - return new ValueTask(false); - } - } - - // broadcast: [methodId, [argument]] - - public Task WriteAllAsync(int methodId, T value, bool fireAndForget) - { - var message = BuildMessage(methodId, value); - - var source = members; - - if (fireAndForget) - { - for (int i = 0; i < source.Length; i++) - { - source[i].QueueResponseStreamWrite(message); - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, source.Length); - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteExceptAsync(int methodId, T value, Guid connectionId, bool fireAndForget) - { - var message = BuildMessage(methodId, value); - - var source = members; - if (fireAndForget) - { - var writeCount = 0; - for (int i = 0; i < source.Length; i++) - { - if (source[i].ContextId != connectionId) - { - source[i].QueueResponseStreamWrite(message); - writeCount++; - } - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteExceptAsync(int methodId, T value, Guid[] connectionIds, bool fireAndForget) - { - var message = BuildMessage(methodId, value); - - var source = members; - if (fireAndForget) - { - var writeCount = 0; - for (int i = 0; i < source.Length; i++) - { - foreach (var item in connectionIds) - { - if (source[i].ContextId == item) - { - goto NEXT; - } - } - - source[i].QueueResponseStreamWrite(message); - writeCount++; - NEXT: - continue; - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteToAsync(int methodId, T value, Guid connectionId, bool fireAndForget) - { - var message = BuildMessage(methodId, value); - - var source = members; - - if (fireAndForget) - { - var writeCount = 0; - for (int i = 0; i < source.Length; i++) - { - if (source[i].ContextId == connectionId) - { - source[i].QueueResponseStreamWrite(message); - writeCount++; - break; - } - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteToAsync(int methodId, T value, Guid[] connectionIds, bool fireAndForget) - { - var message = BuildMessage(methodId, value); - - var source = members; - if (fireAndForget) - { - var writeCount = 0; - for (int i = 0; i < source.Length; i++) - { - foreach (var item in connectionIds) - { - if (source[i].ContextId == item) - { - source[i].QueueResponseStreamWrite(message); - writeCount++; - goto NEXT; - } - } - - NEXT: - continue; - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteExceptRawAsync(ArraySegment msg, Guid[] exceptConnectionIds, bool fireAndForget) - { - // oh, copy is bad but current gRPC interface only accepts byte[]... - var message = new byte[msg.Count]; - Array.Copy(msg.Array!, msg.Offset, message, 0, message.Length); - - var source = members; - if (fireAndForget) - { - var writeCount = 0; - if (exceptConnectionIds == null) - { - for (int i = 0; i < source.Length; i++) - { - source[i].QueueResponseStreamWrite(message); - writeCount++; - } - } - else - { - for (int i = 0; i < source.Length; i++) - { - foreach (var item in exceptConnectionIds) - { - if (source[i].ContextId == item) - { - goto NEXT; - } - } - source[i].QueueResponseStreamWrite(message); - writeCount++; - NEXT: - continue; - } - } - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - public Task WriteToRawAsync(ArraySegment msg, Guid[] connectionIds, bool fireAndForget) - { - // oh, copy is bad but current gRPC interface only accepts byte[]... - var message = new byte[msg.Count]; - Array.Copy(msg.Array!, msg.Offset, message, 0, message.Length); - - var source = members; - if (fireAndForget) - { - var writeCount = 0; - if (connectionIds != null) - { - for (int i = 0; i < source.Length; i++) - { - foreach (var item in connectionIds) - { - if (source[i].ContextId != item) - { - goto NEXT; - } - } - source[i].QueueResponseStreamWrite(message); - writeCount++; - NEXT: - continue; - } - - MagicOnionServerLog.InvokeHubBroadcast(logger, GroupName, message.Length, writeCount); - } - return Task.CompletedTask; - } - else - { - throw new NotSupportedException("The write operation must be called with Fire and Forget option"); - } - } - - byte[] BuildMessage(int methodId, T value) - { - using (var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter()) - { - var writer = new MessagePackWriter(buffer); - writer.WriteArrayHeader(2); - writer.WriteInt32(methodId); - writer.Flush(); - messageSerializer.Serialize(buffer, value); - return buffer.WrittenSpan.ToArray(); - } - } -} diff --git a/src/MagicOnion.Server/Hubs/Group.cs b/src/MagicOnion.Server/Hubs/Group.cs index f571ff821..5fbbcf722 100644 --- a/src/MagicOnion.Server/Hubs/Group.cs +++ b/src/MagicOnion.Server/Hubs/Group.cs @@ -1,278 +1,42 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using MagicOnion.Serialization; -using MagicOnion.Server.Diagnostics; +using System; +using System.Collections.Immutable; +using Cysharp.Runtime.Multicast; namespace MagicOnion.Server.Hubs; -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] -public class GroupConfigurationAttribute : Attribute +public interface IGroup : IMulticastGroup { - public Type FactoryType { get; } - - public GroupConfigurationAttribute(Type groupRepositoryFactoryType) - { - if (!typeof(IGroupRepositoryFactory).IsAssignableFrom(groupRepositoryFactoryType) && (groupRepositoryFactoryType.IsAbstract || groupRepositoryFactoryType.IsInterface)) - { - throw new ArgumentException("A Group repository factory must implement IGroupRepositoryFactory interface and must be a concrete class."); - } - - this.FactoryType = groupRepositoryFactoryType; - } -} - -public interface IGroupRepositoryFactory -{ - IGroupRepository CreateRepository(IMagicOnionSerializer messageSerializer); -} - -public interface IGroupRepository -{ - IGroup GetOrAdd(string groupName); - bool TryGet(string groupName, [NotNullWhen(true)] out IGroup? group); - bool TryRemove(string groupName); + ValueTask RemoveAsync(ServiceContext context); + ValueTask CountAsync(); } -public class HubGroupRepository +internal class Group : IGroup { - readonly ServiceContext serviceContext; - readonly IGroupRepository repository; - readonly ConcurrentBag addedGroups = new ConcurrentBag(); - - public IGroupRepository RawGroupRepository => repository; - - - public HubGroupRepository(ServiceContext serviceContext, IGroupRepository repository) - { - this.serviceContext = serviceContext; - this.repository = repository; - } - - /// - /// Add to group. - /// - public async ValueTask AddAsync(string groupName) - { - var group = repository.GetOrAdd(groupName); - await group.AddAsync(serviceContext).ConfigureAwait(false); - addedGroups.Add(group); - return group; - } - - /// - /// Add to group and use some limitations. - /// - public async ValueTask<(bool, IGroup?)> TryAddAsync(string groupName, int incluciveLimitCount, bool createIfEmpty) - { - // Note: require lock but currently not locked... - if (repository.TryGet(groupName, out var group)) - { - var memberCount = await group.GetMemberCountAsync().ConfigureAwait(false); - if (memberCount >= incluciveLimitCount) - { - return (false, null); - } - else - { - await group.AddAsync(serviceContext).ConfigureAwait(false); - addedGroups.Add(group); - return (true, group); - } - } + readonly IMulticastAsyncGroup group; - if (createIfEmpty) - { - group = repository.GetOrAdd(groupName); - await group.AddAsync(serviceContext).ConfigureAwait(false); - addedGroups.Add(group); - return (true, group); - } - else - { - return (false, null); - } - } + internal string Name { get; } - /// - /// Add to group and add data to inmemory storage per group. - /// - public async ValueTask<(IGroup, IInMemoryStorage)> AddAsync(string groupName, TStorage data) - where TStorage : class + public Group(IMulticastAsyncGroup group, string name) { - var group = repository.GetOrAdd(groupName); - await group.AddAsync(serviceContext).ConfigureAwait(false); - addedGroups.Add(group); - - var storage = group.GetInMemoryStorage(); - storage.Set(serviceContext.ContextId, data); - - return (group, storage); + this.group = group; + this.Name = name; } - /// - /// Add to group(with use some limitations) and add data to inmemory storage per group. - /// - public async ValueTask<(bool, IGroup?, IInMemoryStorage?)> TryAddAsync(string groupName, int incluciveLimitCount, bool createIfEmpty, TStorage data) - where TStorage : class - { - // Note: require lock but currently not locked... - if (repository.TryGet(groupName, out var group)) - { - var memberCount = await group.GetMemberCountAsync(); - if (memberCount >= incluciveLimitCount) - { - return (false, null, null); - } - else - { - await group.AddAsync(serviceContext).ConfigureAwait(false); - addedGroups.Add(group); - var storage = group.GetInMemoryStorage(); - storage.Set(serviceContext.ContextId, data); - return (true, group, storage); - } - } + public T All + => group.All; - if (createIfEmpty) - { - var (repo, stor) = await AddAsync(groupName, data); - return (true, repo, stor); - } - else - { - return (false, null, null); - } - } + public T Except(ImmutableArray excludes) + => group.Except(excludes); - internal async ValueTask DisposeAsync() - { - foreach (var item in addedGroups) - { - await item.RemoveAsync(serviceContext); - } - } -} + public T Only(ImmutableArray targets) + => group.Only(targets); -public interface IGroup -{ - string GroupName { get; } - IInMemoryStorage GetInMemoryStorage() where T : class; - ValueTask GetMemberCountAsync(); - ValueTask AddAsync(ServiceContext context); - /// Note: return bool is `removed from parent`. - ValueTask RemoveAsync(ServiceContext context); - Task WriteAllAsync(int methodId, T value, bool fireAndForget); - Task WriteExceptAsync(int methodId, T value, Guid connectionId, bool fireAndForget); - Task WriteExceptAsync(int methodId, T value, Guid[] connectionIds, bool fireAndForget); - Task WriteExceptRawAsync(ArraySegment message, Guid[] exceptConnectionIds, bool fireAndForget); - Task WriteToAsync(int methodId, T value, Guid connectionId, bool fireAndForget); - Task WriteToAsync(int methodId, T value, Guid[] connectionIds, bool fireAndForget); - Task WriteToRawAsync(ArraySegment message, Guid[] connectionIds, bool fireAndForget); -} + public T Single(Guid target) + => group.Single(target); -public interface IInMemoryStorage -{ - void Remove(Guid connectionId); -} + public ValueTask RemoveAsync(ServiceContext context) + => group.RemoveAsync(context.ContextId); -public interface IInMemoryStorage : IInMemoryStorage - where T : class -{ - ICollection AllValues { get; } - void Set(Guid connectionId, T value); - [return: MaybeNull] - T Get(Guid connectionId); -} - -public class DefaultInMemoryStorage : IInMemoryStorage - where T : class -{ - readonly ConcurrentDictionary storage = new ConcurrentDictionary(); - - public ICollection AllValues => storage.Values; - - public void Set(Guid id, T value) - { - storage[id] = value; - } - - [return: MaybeNull] - public T? Get(Guid id) - { - return storage.TryGetValue(id, out var value) - ? value - : null; - } - - public void Remove(Guid id) - { - storage.TryRemove(id, out _); - } -} - -public static class GroupBroadcastExtensions -{ - /// - /// Create a receiver proxy from the group. Can be use to broadcast messages to all clients. - /// - /// - /// - /// - public static TReceiver CreateBroadcaster(this IGroup group) - { - var type = DynamicBroadcasterBuilder.BroadcasterType; - return (TReceiver) Activator.CreateInstance(type, group)!; - } - - /// - /// Create a receiver proxy from the group. Can be use to broadcast messages to all clients excepts one. - /// - /// - /// - /// - /// - public static TReceiver CreateBroadcasterExcept(this IGroup group, Guid except) - { - var type = DynamicBroadcasterBuilder.BroadcasterType_ExceptOne; - return (TReceiver) Activator.CreateInstance(type, new object[] {group, except})!; - } - - /// - /// Create a receiver proxy from the group. Can be use to broadcast messages to all clients excepts some clients. - /// - /// - /// - /// - /// - public static TReceiver CreateBroadcasterExcept(this IGroup group, Guid[] excepts) - { - var type = DynamicBroadcasterBuilder.BroadcasterType_ExceptMany; - return (TReceiver) Activator.CreateInstance(type, new object[] {group, excepts})!; - } - - /// - /// Create a receiver proxy from the group. Can be use to broadcast messages to one client. - /// - /// - /// - /// - /// - public static TReceiver CreateBroadcasterTo(this IGroup group, Guid toConnectionId) - { - var type = DynamicBroadcasterBuilder.BroadcasterType_ToOne; - return (TReceiver) Activator.CreateInstance(type, new object[] { group, toConnectionId })!; - } - - /// - /// Create a receiver proxy from the group. Can be use to broadcast messages to some clients. - /// - /// - /// - /// - /// - public static TReceiver CreateBroadcasterTo(this IGroup group, Guid[] toConnectionIds) - { - var type = DynamicBroadcasterBuilder.BroadcasterType_ToMany; - return (TReceiver) Activator.CreateInstance(type, new object[] { group, toConnectionIds })!; - } + public ValueTask CountAsync() + => group.CountAsync(); } diff --git a/src/MagicOnion.Server/Hubs/GroupConfigurationAttribute.cs b/src/MagicOnion.Server/Hubs/GroupConfigurationAttribute.cs new file mode 100644 index 000000000..b22f3f47a --- /dev/null +++ b/src/MagicOnion.Server/Hubs/GroupConfigurationAttribute.cs @@ -0,0 +1,19 @@ +using Cysharp.Runtime.Multicast; + +namespace MagicOnion.Server.Hubs; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public class GroupConfigurationAttribute : Attribute +{ + public Type FactoryType { get; } + + public GroupConfigurationAttribute(Type groupProviderType) + { + if (!typeof(IMulticastGroupProvider).IsAssignableFrom(groupProviderType) && (groupProviderType.IsAbstract || groupProviderType.IsInterface)) + { + throw new ArgumentException("A Group provider must implement IMulticastGroupProvider interface and must be a concrete class."); + } + + this.FactoryType = groupProviderType; + } +} diff --git a/src/MagicOnion.Server/Hubs/HeartbeatAttribute.cs b/src/MagicOnion.Server/Hubs/HeartbeatAttribute.cs new file mode 100644 index 000000000..ce8c9aadf --- /dev/null +++ b/src/MagicOnion.Server/Hubs/HeartbeatAttribute.cs @@ -0,0 +1,38 @@ +namespace MagicOnion.Server.Hubs; + +/// +/// Attribute for configuring the heartbeat of StreamingHub. +/// +[AttributeUsage(AttributeTargets.Class)] +public class HeartbeatAttribute : Attribute +{ + /// + /// Gets or sets a value whether the heartbeat feature of StreamingHub is enabled. + /// + public bool Enable { get; set; } + + /// + /// Gets or sets a StreamingHub heartbeat interval (milliseconds). If the value is , the heartbeat is disabled. + /// + public int Interval { get; set; } = 0; + + /// + /// Gets or sets a StreamingHub heartbeat timeout (milliseconds). If the value is , the server does not disconnect a client due to timeout. + /// + public int Timeout { get; set; } = 0; + + /// + /// Gets or sets an implementation type of . + /// + public Type? MetadataProvider { get; set; } + + public HeartbeatAttribute(bool enable) + { + Enable = enable; + } + + public HeartbeatAttribute() + { + Enable = true; + } +} diff --git a/src/MagicOnion.Server/Hubs/HubGroupRepository.cs b/src/MagicOnion.Server/Hubs/HubGroupRepository.cs new file mode 100644 index 000000000..935d4307a --- /dev/null +++ b/src/MagicOnion.Server/Hubs/HubGroupRepository.cs @@ -0,0 +1,44 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using Cysharp.Runtime.Multicast; +using Cysharp.Runtime.Multicast.Remoting; +using MagicOnion.Internal; + +namespace MagicOnion.Server.Hubs; + +public class HubGroupRepository +{ + readonly StreamingServiceContext streamingContext; + readonly string prefix; + readonly MagicOnionManagedGroupProvider autoDisposeGroupProvider; + readonly ConcurrentBag> addedGroups = new(); + readonly T client; + + internal HubGroupRepository(T remoteClient, StreamingServiceContext streamingContext, MagicOnionManagedGroupProvider autoDisposeGroupProvider) + { + Debug.Assert(remoteClient is IRemoteProxy remoteProxy && remoteProxy.TryGetDirectWriter(out _)); + + this.client = remoteClient; + this.streamingContext = streamingContext; + this.autoDisposeGroupProvider = autoDisposeGroupProvider; + this.prefix = streamingContext.MethodHandler.ToString(); + } + + /// + /// Add to group. + /// + public async ValueTask> AddAsync(string groupName) + { + var group = await autoDisposeGroupProvider.GetOrAddAsync($"{prefix}/{groupName}", streamingContext.ContextId, client); + addedGroups.Add(group); + return group; + } + + internal async ValueTask DisposeAsync() + { + foreach (var item in addedGroups) + { + await autoDisposeGroupProvider.RemoveAsync(item, streamingContext.ContextId); + } + } +} diff --git a/src/MagicOnion.Server/Hubs/MagicOnionManagedGroupProvider.cs b/src/MagicOnion.Server/Hubs/MagicOnionManagedGroupProvider.cs new file mode 100644 index 000000000..3c7bebfe1 --- /dev/null +++ b/src/MagicOnion.Server/Hubs/MagicOnionManagedGroupProvider.cs @@ -0,0 +1,81 @@ +using Cysharp.Runtime.Multicast; +using System; + +namespace MagicOnion.Server.Hubs; + +internal class MagicOnionManagedGroupProvider +{ + readonly Dictionary<(string GroupName, Type ReceiverType), (object Group, Counter Counter)> groups = new(); + readonly IMulticastGroupProvider underlyingGroupProvider; + readonly SemaphoreSlim @lock = new(1); + + public MagicOnionManagedGroupProvider(IMulticastGroupProvider underlyingGroupProvider) + { + this.underlyingGroupProvider = underlyingGroupProvider; + } + + public async ValueTask> GetOrAddAsync(string groupName, Guid contextId, T client) + { + var added = false; + var underlyingGroup = default(IMulticastAsyncGroup); + + await @lock.WaitAsync(); + try + { + underlyingGroup = underlyingGroupProvider.GetOrAddGroup(groupName); + + if (!groups.TryGetValue((groupName, typeof(T)), out var groupAndCounter)) + { + groupAndCounter = groups[(groupName, typeof(T))] = (new Group(underlyingGroup, groupName), new Counter()); + } + + await underlyingGroup.AddAsync(contextId, client); + added = true; + groupAndCounter.Counter.CurrentValue += 1; + + return (Group)groupAndCounter.Group; + } + finally + { + if (!added && groups.TryGetValue((groupName, typeof(T)), out var groupAndCounter) && groupAndCounter.Counter.CurrentValue == 0) + { + // When failed to add a member to the group, and the group has no member. + // We need to remove the group from groups. + groups.Remove((groupName, typeof(T))); + underlyingGroup?.Dispose(); + } + + @lock.Release(); + } + } + + public async ValueTask RemoveAsync(Group group, Guid contextId) + { + await @lock.WaitAsync(); + try + { + var underlyingGroup = underlyingGroupProvider.GetOrAddGroup(group.Name); + + if (groups.TryGetValue((group.Name, typeof(T)), out var groupAndCounter)) + { + await underlyingGroup.RemoveAsync(contextId); + groupAndCounter.Counter.CurrentValue -= 1; + } + + if (groupAndCounter.Counter.CurrentValue == 0) + { + groups.Remove((group.Name, typeof(T))); + underlyingGroup.Dispose(); + } + } + finally + { + @lock.Release(); + } + } + + class Counter + { + public int CurrentValue; + } +} diff --git a/src/MagicOnion.Server/Hubs/MagicOnionRemoteReceiverWriter.cs b/src/MagicOnion.Server/Hubs/MagicOnionRemoteReceiverWriter.cs new file mode 100644 index 000000000..a8180ee9f --- /dev/null +++ b/src/MagicOnion.Server/Hubs/MagicOnionRemoteReceiverWriter.cs @@ -0,0 +1,19 @@ +using Cysharp.Runtime.Multicast.Remoting; +using MagicOnion.Internal; + +namespace MagicOnion.Server.Hubs; + +internal class MagicOnionRemoteReceiverWriter : IRemoteReceiverWriter +{ + readonly StreamingServiceContext writer; + + public MagicOnionRemoteReceiverWriter(StreamingServiceContext writer) + { + this.writer = writer; + } + + public void Write(ReadOnlyMemory payload) + { + writer.QueueResponseStreamWrite(StreamingHubPayloadPool.Shared.RentOrCreate(payload.Span)); + } +} diff --git a/src/MagicOnion.Server/Hubs/MagicOnionRemoteSerializer.cs b/src/MagicOnion.Server/Hubs/MagicOnionRemoteSerializer.cs new file mode 100644 index 000000000..43c3bcc48 --- /dev/null +++ b/src/MagicOnion.Server/Hubs/MagicOnionRemoteSerializer.cs @@ -0,0 +1,75 @@ +using System.Buffers; +using Cysharp.Runtime.Multicast.Remoting; +using Grpc.Core; +using MagicOnion.Internal; +using MagicOnion.Serialization; +using MessagePack; +using Microsoft.Extensions.Options; + +namespace MagicOnion.Server.Hubs; + +internal class MagicOnionRemoteSerializer : IRemoteSerializer +{ + readonly IMagicOnionSerializer serializer; + + public MagicOnionRemoteSerializer(IOptions options) + { + this.serializer = options.Value.MessageSerializer.Create(MethodType.DuplexStreaming, null); + } + + public void SerializeInvocation(IBufferWriter bufferWriter, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + { + if (ctx.MessageId is { } messageId) + { + StreamingHubMessageWriter.WriteClientResultRequestMessage(bufferWriter, ctx.MethodId, messageId, Nil.Default, serializer); + } + else + { + StreamingHubMessageWriter.WriteBroadcastMessage(bufferWriter, ctx.MethodId, Nil.Default, serializer); + } + } + + public void SerializeInvocation(IBufferWriter bufferWriter, T value, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + { + if (ctx.MessageId is { } messageId) + { + StreamingHubMessageWriter.WriteClientResultRequestMessage(bufferWriter, ctx.MethodId, messageId, value, serializer); + } + else + { + StreamingHubMessageWriter.WriteBroadcastMessage(bufferWriter, ctx.MethodId, value, serializer); + } + } + + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4, arg5), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4, arg5, arg6), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4, arg5, arg6, arg7), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14), ctx); + public void SerializeInvocation(IBufferWriter writer, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => SerializeInvocation(writer, new DynamicArgumentTuple(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15), ctx); + + public T DeserializeResult(ReadOnlySequence data, in Cysharp.Runtime.Multicast.Remoting.SerializationContext ctx) + => serializer.Deserialize(data); +} diff --git a/src/MagicOnion.Server/Hubs/StreamingHub.cs b/src/MagicOnion.Server/Hubs/StreamingHub.cs index bf48ba64f..0406edf62 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHub.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHub.cs @@ -1,16 +1,26 @@ using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading.Channels; +using Cysharp.Runtime.Multicast; +using Cysharp.Runtime.Multicast.Remoting; using Grpc.Core; +using MagicOnion.Internal; using MagicOnion.Server.Diagnostics; using MagicOnion.Server.Internal; using MessagePack; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace MagicOnion.Server.Hubs; public abstract class StreamingHubBase : ServiceBase, IStreamingHub where THubInterface : IStreamingHub { + IRemoteClientResultPendingTaskRegistry remoteClientResultPendingTasks = default!; + StreamingHubHeartbeatHandle heartbeatHandle = default!; + protected static readonly Task NilTask = Task.FromResult(Nil.Default); protected static readonly ValueTask CompletedTask = new ValueTask(); @@ -21,65 +31,26 @@ public abstract class StreamingHubBase : ServiceBase MarkerResponseBytes => [0x93, 0xff, 0x00, 0x0c]; // MsgPack: [-1, 0, nil] + + readonly Channel<(StreamingHubPayload Payload, UniqueHashDictionary Handlers, int MethodId, int MessageId, ReadOnlyMemory Body, bool HasResponse)> requests + = Channel.CreateBounded<(StreamingHubPayload, UniqueHashDictionary, int, int, ReadOnlyMemory, bool)>(new BoundedChannelOptions(capacity: 10) + { + AllowSynchronousContinuations = false, + FullMode = BoundedChannelFullMode.Wait, + SingleReader = true, + SingleWriter = false, + }); - public HubGroupRepository Group { get; private set; } = default!; /* lateinit */ + public HubGroupRepository Group { get; private set; } = default!; + public TReceiver Client { get; private set; } = default!; - internal StreamingServiceContext StreamingServiceContext - => (StreamingServiceContext)Context; + internal StreamingServiceContext StreamingServiceContext + => (StreamingServiceContext)Context; protected Guid ConnectionId => Context.ContextId; - - // Broadcast Commands - - [Ignore] - protected TReceiver Broadcast(IGroup group) - { - var type = DynamicBroadcasterBuilder.BroadcasterType; - return (TReceiver)Activator.CreateInstance(type, group)!; - } - - [Ignore] - protected TReceiver BroadcastExceptSelf(IGroup group) - { - return BroadcastExcept(group, Context.ContextId); - } - - [Ignore] - protected TReceiver BroadcastExcept(IGroup group, Guid except) - { - var type = DynamicBroadcasterBuilder.BroadcasterType_ExceptOne; - return (TReceiver)Activator.CreateInstance(type, new object[] { group, except })!; - } - - [Ignore] - protected TReceiver BroadcastExcept(IGroup group, Guid[] excepts) - { - var type = DynamicBroadcasterBuilder.BroadcasterType_ExceptMany; - return (TReceiver)Activator.CreateInstance(type, new object[] { group, excepts })!; - } - - [Ignore] - protected TReceiver BroadcastToSelf(IGroup group) - { - return BroadcastTo(group, Context.ContextId); - } - - [Ignore] - protected TReceiver BroadcastTo(IGroup group, Guid toConnectionId) - { - var type = DynamicBroadcasterBuilder.BroadcasterType_ToOne; - return (TReceiver)Activator.CreateInstance(type, new object[] { group, toConnectionId })!; - } - - [Ignore] - protected TReceiver BroadcastTo(IGroup group, Guid[] toConnectionIds) - { - var type = DynamicBroadcasterBuilder.BroadcasterType_ToMany; - return (TReceiver)Activator.CreateInstance(type, new object[] { group, toConnectionIds })!; - } - + /// /// Called before connect, instead of constructor. /// @@ -105,14 +76,24 @@ protected virtual ValueTask OnDisconnected() return CompletedTask; } - public async Task> Connect() + internal async Task> Connect() { Metrics.StreamingHubConnectionIncrement(Context.Metrics, Context.MethodHandler.ServiceName); - var streamingContext = GetDuplexStreamingContext(); + var streamingContext = GetDuplexStreamingContext(); + var serviceProvider = streamingContext.ServiceContext.ServiceProvider; + + var remoteProxyFactory = serviceProvider.GetRequiredService(); + var remoteSerializer = serviceProvider.GetRequiredService(); + this.remoteClientResultPendingTasks = new RemoteClientResultPendingTaskRegistry(serviceProvider.GetRequiredService>().Value.ClientResultsDefaultTimeout); + this.Client = remoteProxyFactory.CreateDirect(new MagicOnionRemoteReceiverWriter(StreamingServiceContext), remoteSerializer, remoteClientResultPendingTasks); - var group = StreamingHubHandlerRepository.GetGroupRepository(Context.MethodHandler); - this.Group = new HubGroupRepository(this.Context, group); + var handlerRepository = serviceProvider.GetRequiredService(); + var groupProvider = handlerRepository.GetGroupProvider(Context.MethodHandler); + this.Group = new HubGroupRepository(Client, StreamingServiceContext, groupProvider); + + var heartbeatManager = handlerRepository.GetHeartbeatManager(Context.MethodHandler); + heartbeatHandle = heartbeatManager.Register(StreamingServiceContext); try { await OnConnecting(); @@ -145,9 +126,11 @@ public async Task> Connect() { Metrics.StreamingHubConnectionDecrement(Context.Metrics, Context.MethodHandler.ServiceName); + heartbeatHandle.Dispose(); StreamingServiceContext.CompleteStreamingHub(); await OnDisconnected(); await this.Group.DisposeAsync(); + remoteClientResultPendingTasks.Dispose(); } return streamingContext.Result(); @@ -155,7 +138,7 @@ public async Task> Connect() async Task HandleMessageAsync() { - var ct = Context.CallContext.CancellationToken; + var ct = CancellationTokenSource.CreateLinkedTokenSource(Context.CallContext.CancellationToken, heartbeatHandle.TimeoutToken).Token; var reader = StreamingServiceContext.RequestStream!; var writer = StreamingServiceContext.ResponseStream!; @@ -165,103 +148,150 @@ async Task HandleMessageAsync() // Write a marker that is the beginning of the stream. // NOTE: To prevent buffering by AWS ALB or reverse-proxy. - await writer.WriteAsync(MarkerResponseBytes); + await writer.WriteAsync(StreamingHubPayloadPool.Shared.RentOrCreate(MarkerResponseBytes)); // Call OnConnected after sending the headers and marker. // The server can send messages or broadcast to client after OnConnected. // eg: Send the current game state to the client. await OnConnected(); - var handlers = StreamingHubHandlerRepository.GetHandlers(Context.MethodHandler); + var handlers = Context.ServiceProvider.GetRequiredService().GetHandlers(Context.MethodHandler); + + // Starts a loop that consumes the request queue. + var consumeRequestsTask = ConsumeRequestQueueAsync(ct); // Main loop of StreamingHub. // Be careful to allocation and performance. - while (await reader.MoveNext(ct)) // must keep SyncContext. + while (await reader.MoveNext(ct)) { - var data = reader.Current; - var (methodId, messageId, offset) = FetchHeader(data); - var hasResponse = messageId != -1; + var payload = reader.Current; + + await ProcessMessageAsync(payload, handlers, ct); + + // NOTE: DO NOT return the StreamingHubPayload to the pool here. + // Client requests may be pending at this point. + } + } - if (handlers.TryGetValue(methodId, out var handler)) + async ValueTask ConsumeRequestQueueAsync(CancellationToken cancellationToken) + { + // We need to process client requests sequentially. + await foreach (var request in requests.Reader.ReadAllAsync(cancellationToken)) + { + try { - // Create a context for each call to the hub method. - var context = new StreamingHubContext() + await ProcessRequestAsync(request.Handlers, request.MethodId, request.MessageId, request.Body, request.HasResponse); + } + finally + { + StreamingHubPayloadPool.Shared.Return(request.Payload); + } + } + } + + ValueTask ProcessMessageAsync(StreamingHubPayload payload, UniqueHashDictionary handlers, CancellationToken cancellationToken) + { + var reader = new StreamingHubServerMessageReader(payload.Memory); + var messageType = reader.ReadMessageType(); + + switch (messageType) + { + case StreamingHubMessageType.Request: { - HubInstance = this, - ServiceContext = (IStreamingServiceContext)Context, - Request = data.AsMemory(offset, data.Length - offset), - Path = handler.ToString(), - MethodId = handler.MethodId, - MessageId = messageId, - Timestamp = DateTime.UtcNow - }; - - var methodStartingTimestamp = Stopwatch.GetTimestamp(); - var isErrorOrInterrupted = false; - MagicOnionServerLog.BeginInvokeHubMethod(Context.MethodHandler.Logger, context, context.Request, handler.RequestType); - try + var requestMessage = reader.ReadRequest(); + return requests.Writer.WriteAsync((payload, handlers, requestMessage.MethodId, requestMessage.MessageId, requestMessage.Body, true), cancellationToken); + } + case StreamingHubMessageType.RequestFireAndForget: { - await handler.MethodBody.Invoke(context); + var requestMessage = reader.ReadRequestFireAndForget(); + return requests.Writer.WriteAsync((payload, handlers, requestMessage.MethodId, -1, requestMessage.Body, false), cancellationToken); } - catch (ReturnStatusException ex) + case StreamingHubMessageType.ClientResultResponse: { - if (hasResponse) + var responseMessage = reader.ReadClientResultResponse(); + if (remoteClientResultPendingTasks.TryGetAndUnregisterPendingTask(responseMessage.ClientResultMessageId, out var pendingMessage)) { - await context.WriteErrorMessage((int)ex.StatusCode, ex.Detail, null, false); + pendingMessage.TrySetResult(responseMessage.Body); } + return default; } - catch (Exception ex) + case StreamingHubMessageType.ClientResultResponseWithError: { - isErrorOrInterrupted = true; - MagicOnionServerLog.Error(Context.MethodHandler.Logger, ex, context); - Metrics.StreamingHubException(Context.Metrics, handler, ex); - - if (hasResponse) + var responseMessage = reader.ReadClientResultResponseForError(); + if (remoteClientResultPendingTasks.TryGetAndUnregisterPendingTask(responseMessage.ClientResultMessageId, out var pendingMessage)) { - await context.WriteErrorMessage((int)StatusCode.Internal, $"An error occurred while processing handler '{handler.ToString()}'.", ex, Context.MethodHandler.IsReturnExceptionStackTraceInErrorDetail); + pendingMessage.TrySetException(new RpcException(new Status((StatusCode)responseMessage.StatusCode, responseMessage.Message + (string.IsNullOrEmpty(responseMessage.Detail) ? string.Empty : Environment.NewLine + responseMessage.Detail)))); } + return default; } - finally + case StreamingHubMessageType.HeartbeatResponse: { - var methodEndingTimestamp = Stopwatch.GetTimestamp(); - MagicOnionServerLog.EndInvokeHubMethod(Context.MethodHandler.Logger, context, context.responseSize, context.responseType, StopwatchHelper.GetElapsedTime(methodStartingTimestamp, methodEndingTimestamp).TotalMilliseconds, isErrorOrInterrupted); - Metrics.StreamingHubMethodCompleted(Context.Metrics, handler, methodStartingTimestamp, methodEndingTimestamp, isErrorOrInterrupted); + heartbeatHandle.Ack(); + return default; } - } - else - { - throw new InvalidOperationException("Handler not found in received methodId, methodId:" + methodId); - } + default: + throw new InvalidOperationException($"Unknown MessageType: {messageType}"); } } - static (int methodId, int messageId, int offset) FetchHeader(byte[] msgData) + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + async ValueTask ProcessRequestAsync(UniqueHashDictionary handlers, int methodId, int messageId, ReadOnlyMemory body, bool hasResponse) { - var messagePackReader = new MessagePackReader(msgData); - - var length = messagePackReader.ReadArrayHeader(); - if (length == 2) + if (handlers.TryGetValue(methodId, out var handler)) { - // void: [methodId, [argument]] - var mid = messagePackReader.ReadInt32(); - var consumed = (int)messagePackReader.Consumed; + // Create a context for each call to the hub method. + var context = StreamingHubContextPool.Shared.Get(); + context.Initialize( + serviceContext: (IStreamingServiceContext)Context, + hubInstance: this, + request: body, + path: handler.ToString(), + methodId: methodId, + messageId: messageId, + timestamp: DateTime.UtcNow + ); + + var methodStartingTimestamp = Stopwatch.GetTimestamp(); + var isErrorOrInterrupted = false; + MagicOnionServerLog.BeginInvokeHubMethod(Context.MethodHandler.Logger, context, context.Request, handler.RequestType); + try + { + await handler.MethodBody.Invoke(context); + } + catch (ReturnStatusException ex) + { + if (hasResponse) + { + await context.WriteErrorMessage((int)ex.StatusCode, ex.Detail, null, false); + } + } + catch (Exception ex) + { + isErrorOrInterrupted = true; + MagicOnionServerLog.Error(Context.MethodHandler.Logger, ex, context); + Metrics.StreamingHubException(Context.Metrics, handler, ex); - return (mid, -1, consumed); - } - else if (length == 3) - { - // T: [messageId, methodId, [argument]] - var msgId = messagePackReader.ReadInt32(); - var metId = messagePackReader.ReadInt32(); - var consumed = (int)messagePackReader.Consumed; - return (metId, msgId, consumed); + if (hasResponse) + { + await context.WriteErrorMessage((int)StatusCode.Internal, $"An error occurred while processing handler '{handler.ToString()}'.", ex, Context.MethodHandler.IsReturnExceptionStackTraceInErrorDetail); + } + } + finally + { + var methodEndingTimestamp = Stopwatch.GetTimestamp(); + MagicOnionServerLog.EndInvokeHubMethod(Context.MethodHandler.Logger, context, context.ResponseSize, context.ResponseType, StopwatchHelper.GetElapsedTime(methodStartingTimestamp, methodEndingTimestamp).TotalMilliseconds, isErrorOrInterrupted); + Metrics.StreamingHubMethodCompleted(Context.Metrics, handler, methodStartingTimestamp, methodEndingTimestamp, isErrorOrInterrupted); + + StreamingHubContextPool.Shared.Return(context); + } } else { - throw new InvalidOperationException("Invalid data format."); + throw new InvalidOperationException("Handler not found in received methodId, methodId:" + methodId); } } + // Interface methods for Client THubInterface IStreamingHub.FireAndForget() diff --git a/src/MagicOnion.Server/Hubs/StreamingHubContext.cs b/src/MagicOnion.Server/Hubs/StreamingHubContext.cs index e96e0a41c..5307e00f9 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubContext.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubContext.cs @@ -1,11 +1,39 @@ using MagicOnion.Internal.Buffers; using MessagePack; using System.Collections.Concurrent; +using MagicOnion.Internal; +using Microsoft.Extensions.ObjectPool; namespace MagicOnion.Server.Hubs; +internal class StreamingHubContextPool +{ + const int MaxRetainedCount = 16; + readonly ObjectPool pool = new DefaultObjectPool(new Policy(), MaxRetainedCount); + + public static StreamingHubContextPool Shared { get; } = new(); + + public StreamingHubContext Get() => pool.Get(); + public void Return(StreamingHubContext ctx) => pool.Return(ctx); + + class Policy : IPooledObjectPolicy + { + public StreamingHubContext Create() + { + return new StreamingHubContext(); + } + + public bool Return(StreamingHubContext obj) + { + obj.Uninitialize(); + return true; + } + } +} + public class StreamingHubContext { + IStreamingServiceContext streamingServiceContext = default!; ConcurrentDictionary? items; /// Object storage per invoke. @@ -21,113 +49,121 @@ public ConcurrentDictionary Items } } - /// Raw gRPC Context. - public IStreamingServiceContext ServiceContext { get; internal set; } = default!; /* lateinit */ - public object HubInstance { get; internal set; } = default!; /* lateinit */ + public object HubInstance { get; private set; } = default!; + + public ReadOnlyMemory Request { get; private set; } + public string Path { get; private set; } = default!; + public DateTime Timestamp { get; private set; } + + public Guid ConnectionId => streamingServiceContext.ContextId; + + public IServiceContext ServiceContext => streamingServiceContext; - public ReadOnlyMemory Request { get; internal set; } - public string Path { get; internal set; } = default!; /* lateinit */ - public DateTime Timestamp { get; internal set; } + internal int MessageId { get; private set; } + internal int MethodId { get; private set; } - public Guid ConnectionId => ServiceContext.ContextId; + internal int ResponseSize { get; private set; } = -1; + internal Type? ResponseType { get; private set; } - // public AsyncLock AsyncWriterLock { get; internal set; } = default!; /* lateinit */ - internal int MessageId { get; set; } - internal int MethodId { get; set; } + internal void Initialize(IStreamingServiceContext serviceContext, object hubInstance, ReadOnlyMemory request, string path, DateTime timestamp, int messageId, int methodId) + { + streamingServiceContext = serviceContext; + HubInstance = hubInstance; + Request = request; + Path = path; + Timestamp = timestamp; + MessageId = messageId; + MethodId = methodId; + } - internal int responseSize = -1; - internal Type? responseType; + internal void Uninitialize() + { + streamingServiceContext = default!; + HubInstance = default!; + Request = default!; + Path = default!; + Timestamp = default!; + MessageId = default!; + MethodId = default!; + ResponseSize = -1; + items?.Clear(); + } // helper for reflection - internal async ValueTask WriteResponseMessageNil(ValueTask value) + internal ValueTask WriteResponseMessageNil(ValueTask value) { - if (MessageId == -1) // don't write. + if (MessageId == -1) // No need to write a response. We do not write response. { - return; + return default; } - // MessageFormat: - // response: [messageId, methodId, response] - byte[] BuildMessage() + ResponseType = typeof(Nil); + if (value.IsCompletedSuccessfully) { - using (var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter()) - { - var writer = new MessagePackWriter(buffer); - - writer.WriteArrayHeader(3); - writer.Write(MessageId); - writer.Write(MethodId); - writer.WriteNil(); - writer.Flush(); - return buffer.WrittenSpan.ToArray(); - } + WriteMessageCore(BuildMessage()); + return default; } + return Await(this, value); - await value.ConfigureAwait(false); - var result = BuildMessage(); - ServiceContext.QueueResponseStreamWrite(result); - responseSize = result.Length; - responseType = typeof(Nil); + static async ValueTask Await(StreamingHubContext ctx, ValueTask value) + { + await value.ConfigureAwait(false); + ctx.WriteMessageCore(ctx.BuildMessage()); + } } - internal async ValueTask WriteResponseMessage(ValueTask value) + internal ValueTask WriteResponseMessage(ValueTask value) { - if (MessageId == -1) // don't write. + if (MessageId == -1) // No need to write a response. We do not write response. { - return; + return default; } - // MessageFormat: - // response: [messageId, methodId, response] - byte[] BuildMessage(T v) + ResponseType = typeof(T); + if (value.IsCompletedSuccessfully) { - using (var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter()) - { - var writer = new MessagePackWriter(buffer); - writer.WriteArrayHeader(3); - writer.Write(MessageId); - writer.Write(MethodId); - writer.Flush(); - ServiceContext.MessageSerializer.Serialize(buffer, v); - return buffer.WrittenSpan.ToArray(); - } + WriteMessageCore(BuildMessage(value.Result)); + return default; } + return Await(this, value); - var vv = await value.ConfigureAwait(false); - byte[] result = BuildMessage(vv); - ServiceContext.QueueResponseStreamWrite(result); - responseSize = result.Length; - responseType = typeof(T); + static async ValueTask Await(StreamingHubContext ctx, ValueTask value) + { + var vv = await value.ConfigureAwait(false); + ctx.WriteMessageCore(ctx.BuildMessage(vv)); + } } internal ValueTask WriteErrorMessage(int statusCode, string detail, Exception? ex, bool isReturnExceptionStackTraceInErrorDetail) { - // MessageFormat: - // error-response: [messageId, statusCode, detail, StringMessage] - byte[] BuildMessage() - { - using (var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter()) - { - var writer = new MessagePackWriter(buffer); - writer.WriteArrayHeader(4); - writer.Write(MessageId); - writer.Write(statusCode); - writer.Write(detail); + WriteMessageCore(BuildMessageForError(statusCode, detail, ex, isReturnExceptionStackTraceInErrorDetail)); + return default; + } - var msg = (isReturnExceptionStackTraceInErrorDetail && ex != null) - ? ex.ToString() - : null; + void WriteMessageCore(StreamingHubPayload payload) + { + ResponseSize = payload.Length; // NOTE: We cannot use the payload after QueueResponseStreamWrite. + streamingServiceContext.QueueResponseStreamWrite(payload); + } - writer.Write(msg); - writer.Flush(); + StreamingHubPayload BuildMessage() + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteResponseMessage(buffer, MethodId, MessageId); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } - return buffer.WrittenSpan.ToArray(); - } - } + StreamingHubPayload BuildMessage(T v) + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteResponseMessage(buffer, MethodId, MessageId, v, streamingServiceContext.MessageSerializer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } - var result = BuildMessage(); - ServiceContext.QueueResponseStreamWrite(result); - responseSize = result.Length; - return default; + StreamingHubPayload BuildMessageForError(int statusCode, string detail, Exception? ex, bool isReturnExceptionStackTraceInErrorDetail) + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteResponseMessageForError(buffer, MessageId, statusCode, detail, ex, isReturnExceptionStackTraceInErrorDetail); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); } } diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs index 4d881b2db..c2d646961 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHandler.cs @@ -111,17 +111,38 @@ protected StreamingHubMethodInvoker(IMagicOnionSerializer messageSerializer) public static Type CreateInvokerTypeFromMetadata(in StreamingHubMethodHandlerMetadata metadata) { + var isVoid = metadata.InterfaceMethod.ReturnType == typeof(void); var isTaskOrTaskOfT = metadata.InterfaceMethod.ReturnType == typeof(Task) || (metadata.InterfaceMethod.ReturnType is { IsGenericType: true } t && t.BaseType == typeof(Task)); - return 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) - ); + return isVoid + ? typeof(StreamingHubMethodInvokerVoid<>).MakeGenericType(metadata.RequestType) + : isTaskOrTaskOfT + ? (metadata.ResponseType is null + ? typeof(StreamingHubMethodInvokerTask<>).MakeGenericType(metadata.RequestType) + : typeof(StreamingHubMethodInvokerTask<,>).MakeGenericType(metadata.RequestType, metadata.ResponseType) + ) + : (metadata.ResponseType is null + ? typeof(StreamingHubMethodInvokerValueTask<>).MakeGenericType(metadata.RequestType) + : typeof(StreamingHubMethodInvokerValueTask<,>).MakeGenericType(metadata.RequestType, metadata.ResponseType) + ); + } + + sealed class StreamingHubMethodInvokerVoid : StreamingHubMethodInvoker + { + readonly Action hubMethodFunc; + + public StreamingHubMethodInvokerVoid(IMagicOnionSerializer messageSerializer, Delegate hubMethodFunc) : base(messageSerializer) + { + this.hubMethodFunc = (Action)hubMethodFunc; + } + + public override ValueTask InvokeAsync(StreamingHubContext context) + { + var seq = new ReadOnlySequence(context.Request); + TRequest request = MessageSerializer.Deserialize(seq); + hubMethodFunc(context, request); + return context.WriteResponseMessageNil(default); + } } sealed class StreamingHubMethodInvokerTask : StreamingHubMethodInvoker diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs b/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs index d01d5f971..863c7cba0 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHandlerRepository.cs @@ -1,30 +1,70 @@ +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif +using Cysharp.Runtime.Multicast; using MagicOnion.Server.Internal; namespace MagicOnion.Server.Hubs; // Global cache of Streaming Handler -internal static class StreamingHubHandlerRepository +internal class StreamingHubHandlerRepository { - static Dictionary> cache - = new Dictionary>(new MethodHandler.UniqueEqualityComparer()); + bool frozen; + IDictionary> handlersCache = new Dictionary>(MethodHandler.UniqueEqualityComparer.Instance); + IDictionary groupCache = new Dictionary(MethodHandler.UniqueEqualityComparer.Instance); + IDictionary heartbeats = new Dictionary(MethodHandler.UniqueEqualityComparer.Instance); - static Dictionary cacheGroup - = new Dictionary(new MethodHandler.UniqueEqualityComparer()); - - public static void RegisterHandler(MethodHandler parent, StreamingHubHandler[] hubHandlers) + public void RegisterHandler(MethodHandler parent, StreamingHubHandler[] hubHandlers) { + ThrowIfFrozen(); + var handlers = VerifyDuplicate(hubHandlers); var hashDict = new UniqueHashDictionary(handlers); - lock (cache) - { - cache.Add(parent, hashDict); - } + handlersCache.Add(parent, hashDict); + } + + public UniqueHashDictionary GetHandlers(MethodHandler parent) + => handlersCache[parent]; + + + public void Freeze() + { + ThrowIfFrozen(); + frozen = true; + +#if NET8_0_OR_GREATER + handlersCache = handlersCache.ToFrozenDictionary(MethodHandler.UniqueEqualityComparer.Instance); + groupCache = groupCache.ToFrozenDictionary(MethodHandler.UniqueEqualityComparer.Instance); + heartbeats = heartbeats.ToFrozenDictionary(MethodHandler.UniqueEqualityComparer.Instance); +#endif + } + + void ThrowIfFrozen() + { + if (frozen) throw new InvalidOperationException($"Cannot modify the {nameof(StreamingHubHandlerRepository)}. The instance is already frozen."); + } + + public void RegisterGroupProvider(MethodHandler methodHandler, IMulticastGroupProvider groupProvider) + { + ThrowIfFrozen(); + groupCache[methodHandler] = new MagicOnionManagedGroupProvider(groupProvider); } - public static UniqueHashDictionary GetHandlers(MethodHandler parent) + public MagicOnionManagedGroupProvider GetGroupProvider(MethodHandler methodHandler) { - return cache[parent]; + return groupCache[methodHandler]; + } + + public void RegisterHeartbeatManager(MethodHandler methodHandler, IStreamingHubHeartbeatManager heartbeatManager) + { + ThrowIfFrozen(); + heartbeats[methodHandler] = heartbeatManager; + } + + public IStreamingHubHeartbeatManager GetHeartbeatManager(MethodHandler methodHandler) + { + return heartbeats[methodHandler]; } static (int, StreamingHubHandler)[] VerifyDuplicate(StreamingHubHandler[] hubHandlers) @@ -44,17 +84,4 @@ public static UniqueHashDictionary GetHandlers(MethodHandle return list.ToArray(); } - - public static void AddGroupRepository(MethodHandler parent, IGroupRepository repository) - { - lock (cacheGroup) - { - cacheGroup.Add(parent, repository); - } - } - - public static IGroupRepository GetGroupRepository(MethodHandler parent) - { - return cacheGroup[parent]; - } } diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs b/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs new file mode 100644 index 000000000..3fb2eaa0c --- /dev/null +++ b/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs @@ -0,0 +1,173 @@ +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using MagicOnion.Internal; +using MagicOnion.Server.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace MagicOnion.Server.Hubs; + +internal interface IStreamingHubHeartbeatManager : IDisposable +{ + StreamingHubHeartbeatHandle Register(IStreamingServiceContext serviceContext); + void Unregister(IStreamingServiceContext serviceContext); +} + +internal class StreamingHubHeartbeatHandle : IDisposable +{ + readonly IStreamingHubHeartbeatManager manager; + readonly CancellationTokenSource timeoutToken; + readonly TimeSpan timeoutDuration; + bool disposed; + + public IStreamingServiceContext ServiceContext { get; } + public CancellationToken TimeoutToken => timeoutToken.Token; + + public StreamingHubHeartbeatHandle(IStreamingHubHeartbeatManager manager, IStreamingServiceContext serviceContext, TimeSpan timeoutDuration) + { + this.manager = manager; + this.ServiceContext = serviceContext; + this.timeoutDuration = timeoutDuration; + this.timeoutToken = new CancellationTokenSource(); + } + + public void RestartTimeoutTimer() + { + if (disposed || timeoutDuration == Timeout.InfiniteTimeSpan) return; + timeoutToken.CancelAfter(timeoutDuration); + } + + public void Ack() + { + if (disposed || timeoutDuration == Timeout.InfiniteTimeSpan) return; + timeoutToken.CancelAfter(Timeout.InfiniteTimeSpan); + } + + public void Dispose() + { + if (disposed) return; + disposed = true; + manager.Unregister(ServiceContext); + timeoutToken.Dispose(); + } +} + +internal class NopStreamingHubHeartbeatManager : IStreamingHubHeartbeatManager +{ + public static IStreamingHubHeartbeatManager Instance { get; } = new NopStreamingHubHeartbeatManager(); + + NopStreamingHubHeartbeatManager() {} + + public StreamingHubHeartbeatHandle Register(IStreamingServiceContext serviceContext) + => new(this, serviceContext, Timeout.InfiniteTimeSpan); + public void Unregister(IStreamingServiceContext serviceContext) { } + public void Dispose() { } +} + +internal class StreamingHubHeartbeatManager : IStreamingHubHeartbeatManager +{ + static ReadOnlySpan Nil => [0xc0]; + + readonly object timerGate = new(); + readonly IStreamingHubHeartbeatMetadataProvider? heartbeatMetadataProvider; + readonly TimeSpan heartbeatInterval; + readonly TimeSpan timeoutDuration; + readonly ILogger logger; + + PeriodicTimer? timer; + int registeredCount; + ConcurrentDictionary contexts = new(); + + public StreamingHubHeartbeatManager(TimeSpan heartbeatInterval, TimeSpan timeoutDuration, IStreamingHubHeartbeatMetadataProvider? heartbeatMetadataProvider, ILogger logger) + { + this.heartbeatInterval = heartbeatInterval; + this.timeoutDuration = timeoutDuration; + this.heartbeatMetadataProvider = heartbeatMetadataProvider; + this.logger = logger; + } + + public StreamingHubHeartbeatHandle Register(IStreamingServiceContext serviceContext) + { + var handle = new StreamingHubHeartbeatHandle(this, serviceContext, timeoutDuration); + if (contexts.TryAdd(serviceContext.ContextId, handle)) + { + if (Interlocked.Increment(ref registeredCount) == 1) + { + lock (timerGate) + { + if (timer is null) + { + timer = new PeriodicTimer(heartbeatInterval); + MagicOnionServerLog.BeginHeartbeatTimer(this.logger, serviceContext.CallContext.Method, heartbeatInterval, timeoutDuration); + _ = StartHeartbeatAsync(timer, serviceContext.CallContext.Method); + } + } + } + + handle.TimeoutToken.UnsafeRegister(_ => MagicOnionServerLog.HeartbeatTimedOut(logger, serviceContext.CallContext.Method, serviceContext.ContextId), null); + + return handle; + } + + return contexts[serviceContext.ContextId]; + } + + async Task StartHeartbeatAsync(PeriodicTimer runningTimer, string method) + { + Debug.Assert(runningTimer != null); + + var writer = new ArrayBufferWriter(); + while (await runningTimer.WaitForNextTickAsync()) + { + StreamingHubMessageWriter.WriteHeartbeatMessageForServerToClientHeader(writer); + if (!(heartbeatMetadataProvider?.TryWriteMetadata(writer) ?? false)) + { + writer.Write(Nil); + } + + var payload = StreamingHubPayloadPool.Shared.RentOrCreate(writer.WrittenSpan); + + MagicOnionServerLog.SendHeartbeat(this.logger, method); + try + { + foreach (var (contextId, handle) in contexts) + { + handle.RestartTimeoutTimer(); + handle.ServiceContext.QueueResponseStreamWrite(payload); + } + } + catch { /* Ignore */ } + + writer.Clear(); + } + } + + public void Unregister(IStreamingServiceContext serviceContext) + { + if (contexts.TryRemove(serviceContext.ContextId, out _)) + { + if (Interlocked.Decrement(ref registeredCount) == 0) + { + lock (timerGate) + { + if (Volatile.Read(ref registeredCount) == 0 && timer is not null) + { + MagicOnionServerLog.ShutdownHeartbeatTimer(this.logger, serviceContext.CallContext.Method); + timer.Dispose(); + timer = null; + } + } + } + } + } + + public void Dispose() + { + timer?.Dispose(); + } +} + +public interface IStreamingHubHeartbeatMetadataProvider +{ + bool TryWriteMetadata(IBufferWriter writer); +} diff --git a/src/MagicOnion.Server/Hubs/StreamingHubMarshaller.cs b/src/MagicOnion.Server/Hubs/StreamingHubMarshaller.cs deleted file mode 100644 index a5651a8f6..000000000 --- a/src/MagicOnion.Server/Hubs/StreamingHubMarshaller.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Grpc.Core; -using MagicOnion.Serialization; -using MessagePack; - -namespace MagicOnion.Server.Hubs; - -internal class StreamingHubMarshaller -{ - public static Marshaller CreateForRequest(MethodHandler methodHandler, IMagicOnionSerializer messageSerializer) - => new Marshaller((data, ctx) => - { - var writer = ctx.GetBufferWriter(); - var buffer = writer.GetSpan(data.Length); - data.CopyTo(buffer); - writer.Advance(data.Length); - ctx.Complete(); - }, (ctx) => ctx.PayloadAsNewBuffer()); - - public static Marshaller CreateForResponse(MethodHandler methodHandler, IMagicOnionSerializer messageSerializer) - => new Marshaller((data, ctx) => - { - var writer = ctx.GetBufferWriter(); - var buffer = writer.GetSpan(data.Length); - data.CopyTo(buffer); - writer.Advance(data.Length); - ctx.Complete(); - }, (ctx) => ctx.PayloadAsNewBuffer()); -} diff --git a/src/MagicOnion.Server/Internal/MagicOnionMethodHandlerBinder.cs b/src/MagicOnion.Server/Internal/MagicOnionMethodHandlerBinder.cs index 7fc07364b..3e4de6548 100644 --- a/src/MagicOnion.Server/Internal/MagicOnionMethodHandlerBinder.cs +++ b/src/MagicOnion.Server/Internal/MagicOnionMethodHandlerBinder.cs @@ -62,14 +62,16 @@ public void BindUnaryParameterless(ServiceBinderBase binder, Func, IServerStreamWriter, ServerCallContext, Task> serverMethod, MethodHandler methodHandler, IMagicOnionSerializer messageSerializer) { + Debug.Assert(typeof(TRequest) == typeof(StreamingHubPayload)); + Debug.Assert(typeof(TResponse) == typeof(StreamingHubPayload)); // StreamingHub uses the special marshallers for streaming messages serialization. - // TODO: Currently, MagicOnion expects TRawRequest/TRawResponse to be raw-byte array (`bytes[]`). + // TODO: Currently, MagicOnion expects TRawRequest/TRawResponse to be raw-byte array (`StreamingHubPayload`). var method = new GrpcMethodHelper.MagicOnionMethod(new Method( MethodType.DuplexStreaming, methodHandler.ServiceName, methodHandler.MethodName, - (Marshaller)(object)Hubs.StreamingHubMarshaller.CreateForRequest(methodHandler, messageSerializer), - (Marshaller)(object)Hubs.StreamingHubMarshaller.CreateForResponse(methodHandler, messageSerializer) + (Marshaller)(object)MagicOnionMarshallers.StreamingHubMarshaller, + (Marshaller)(object)MagicOnionMarshallers.StreamingHubMarshaller )); binder.AddMethod(new MagicOnionServerMethod(method.Method, methodHandler), async (request, response, context) => await serverMethod( diff --git a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs index f28f03a9b..40311af37 100644 --- a/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs +++ b/src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs @@ -107,7 +107,7 @@ public static StreamingHubMethodHandlerMetadata CreateStreamingHubMethodHandlerM if (!responseIsTaskOrValueTask) { - throw new InvalidOperationException($"A type of the StreamingHub method must be Task, Task, ValueTask or ValueTask. (Member:{serviceClass.Name}.{methodInfo.Name})"); + throw new InvalidOperationException($"A type of the StreamingHub method must be void, Task, Task, ValueTask or ValueTask. (Member:{serviceClass.Name}.{methodInfo.Name})"); } var methodId = interfaceMethodInfo.GetCustomAttribute()?.MethodId ?? FNV1A32.GetHashCode(interfaceMethodInfo.Name); @@ -187,19 +187,24 @@ 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 responseIsTaskOrValueTask) + static Type? UnwrapStreamingHubResponseType(MethodInfo methodInfo, out bool responseIsVoidOrTaskOrValueTask) { var t = methodInfo.ReturnType; // Task if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(Task<>) || t.GetGenericTypeDefinition() == typeof(ValueTask<>))) { - responseIsTaskOrValueTask = true; + responseIsVoidOrTaskOrValueTask = true; return t.GetGenericArguments()[0]; } else if (t == typeof(Task) || t == typeof(ValueTask)) { - responseIsTaskOrValueTask = true; + responseIsVoidOrTaskOrValueTask = true; + return null; + } + else if (t == typeof(void)) + { + responseIsVoidOrTaskOrValueTask = true; return null; } diff --git a/src/MagicOnion.Server/MagicOnion.Server.csproj b/src/MagicOnion.Server/MagicOnion.Server.csproj index 4bc84fde0..69db4fa3e 100644 --- a/src/MagicOnion.Server/MagicOnion.Server.csproj +++ b/src/MagicOnion.Server/MagicOnion.Server.csproj @@ -1,13 +1,13 @@ - net6.0;net7.0;net8.0 + net6.0;net8.0 enable enable latest - $(DefineConstants);NON_UNITY + $(DefineConstants);NON_UNITY;USE_OBJECTPOOL_STREAMINGHUBPAYLOADPOOL MagicOnion.Server @@ -21,9 +21,10 @@ + - + diff --git a/src/MagicOnion.Server/MagicOnionEngine.cs b/src/MagicOnion.Server/MagicOnionEngine.cs index b1fa8df44..871ad4e0f 100644 --- a/src/MagicOnion.Server/MagicOnionEngine.cs +++ b/src/MagicOnion.Server/MagicOnionEngine.cs @@ -2,10 +2,12 @@ using System.Diagnostics; using System.Reflection; using System.Runtime.ExceptionServices; +using Cysharp.Runtime.Multicast; using Grpc.Core; using Microsoft.Extensions.DependencyInjection; using MagicOnion.Server.Diagnostics; using Microsoft.Extensions.Logging; +using System; namespace MagicOnion.Server; @@ -118,6 +120,9 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP var loggerFactory = serviceProvider.GetRequiredService(); var loggerMagicOnionEngine = loggerFactory.CreateLogger(LoggerNameMagicOnionEngine); var loggerMethodHandler = loggerFactory.CreateLogger(LoggerNameMethodHandler); + + var streamingHubHandlerRepository = serviceProvider.GetRequiredService(); + MagicOnionServerLog.BeginBuildServiceDefinition(loggerMagicOnionEngine); var sw = Stopwatch.StartNew(); @@ -193,25 +198,66 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP if (isStreamingHub) { - var connectHandler = new MethodHandler(classType, classType.GetMethod("Connect")!, "Connect", methodHandlerOptions, serviceProvider, loggerMethodHandler, isStreamingHub: true); + var connectHandler = new MethodHandler(classType, classType.GetMethod("Connect", BindingFlags.NonPublic | BindingFlags.Instance)!, "Connect", methodHandlerOptions, serviceProvider, loggerMethodHandler, isStreamingHub: true); if (!handlers.Add(connectHandler)) { throw new InvalidOperationException($"Method does not allow overload, {className}.Connect"); } streamingHubHandlers.AddRange(tempStreamingHubHandlers!); - StreamingHubHandlerRepository.RegisterHandler(connectHandler, tempStreamingHubHandlers!.ToArray()); - IGroupRepositoryFactory factory; + streamingHubHandlerRepository.RegisterHandler(connectHandler, tempStreamingHubHandlers!.ToArray()); + + // Group Provider + IMulticastGroupProvider groupProvider; var attr = classType.GetCustomAttribute(true); if (attr != null) { - factory = (IGroupRepositoryFactory)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, attr.FactoryType); + groupProvider = (IMulticastGroupProvider)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, attr.FactoryType); + } + else + { + groupProvider = serviceProvider.GetRequiredService(); + } + + streamingHubHandlerRepository.RegisterGroupProvider(connectHandler, groupProvider); + + // Heartbeat + var heartbeatEnable = options.EnableStreamingHubHeartbeat; + var heartbeatInterval = options.StreamingHubHeartbeatInterval; + var heartbeatTimeout = options.StreamingHubHeartbeatTimeout; + var heartbeatMetadataProvider = default(IStreamingHubHeartbeatMetadataProvider); + if (classType.GetCustomAttribute(inherit: true) is { } heartbeatAttr) + { + heartbeatEnable = heartbeatAttr.Enable; + if (heartbeatAttr.Timeout != 0) + { + heartbeatTimeout = TimeSpan.FromMilliseconds(heartbeatAttr.Timeout); + } + if (heartbeatAttr.Interval != 0) + { + heartbeatInterval = TimeSpan.FromMilliseconds(heartbeatAttr.Interval); + } + if (heartbeatAttr.MetadataProvider != null) + { + heartbeatMetadataProvider = (IStreamingHubHeartbeatMetadataProvider)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, heartbeatAttr.MetadataProvider); + } + } + + IStreamingHubHeartbeatManager heartbeatManager; + if (!heartbeatEnable || heartbeatInterval is null) + { + heartbeatManager = NopStreamingHubHeartbeatManager.Instance; } else { - factory = serviceProvider.GetRequiredService(); + heartbeatManager = new StreamingHubHeartbeatManager( + heartbeatInterval.Value, + heartbeatTimeout ?? Timeout.InfiniteTimeSpan, + heartbeatMetadataProvider ?? serviceProvider.GetService(), + serviceProvider.GetRequiredService>() + ); } - StreamingHubHandlerRepository.AddGroupRepository(connectHandler, factory.CreateRepository(options.MessageSerializer.Create(MethodType.DuplexStreaming, null))); + streamingHubHandlerRepository.RegisterHeartbeatManager(connectHandler, heartbeatManager); } } } @@ -220,6 +266,8 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP ExceptionDispatchInfo.Capture(agex.InnerExceptions[0]).Throw(); } + streamingHubHandlerRepository.Freeze(); + var result = new MagicOnionServiceDefinition(handlers.ToArray(), streamingHubHandlers.ToArray()); sw.Stop(); diff --git a/src/MagicOnion.Server/MagicOnionOptions.cs b/src/MagicOnion.Server/MagicOnionOptions.cs index e227c3315..74d953ccf 100644 --- a/src/MagicOnion.Server/MagicOnionOptions.cs +++ b/src/MagicOnion.Server/MagicOnionOptions.cs @@ -32,6 +32,26 @@ public class MagicOnionOptions /// public IList GlobalStreamingHubFilters { get; set; } + /// + /// Gets or sets the default timeout duration of client results. Default is 5 seconds. + /// + public TimeSpan ClientResultsDefaultTimeout { get; set; } + + /// + /// Gets or sets a value whether the heartbeat feature of StreamingHub is enabled. Default is . + /// + public bool EnableStreamingHubHeartbeat { get; set; } + + /// + /// Gets or sets a StreamingHub heartbeat interval. Default is . If the value is , the heartbeat is disabled. + /// + public TimeSpan? StreamingHubHeartbeatInterval { get; set; } + + /// + /// Gets or sets a StreamingHub heartbeat timeout. Default is . If the value is , the server does not disconnect a client due to timeout. + /// + public TimeSpan? StreamingHubHeartbeatTimeout { get; set; } + /// /// Constructor can handle only error detail. If you want to set the other options, you can use object initializer. /// @@ -44,5 +64,9 @@ public MagicOnionOptions() this.GlobalFilters = new List(); this.GlobalStreamingHubFilters = new List(); this.EnableCurrentContext = false; + this.ClientResultsDefaultTimeout = TimeSpan.FromSeconds(5); + this.EnableStreamingHubHeartbeat = false; + this.StreamingHubHeartbeatInterval = null; + this.StreamingHubHeartbeatTimeout = null; } } diff --git a/src/MagicOnion.Server/MethodHandler.cs b/src/MagicOnion.Server/MethodHandler.cs index 4ec2a702e..74852cfef 100644 --- a/src/MagicOnion.Server/MethodHandler.cs +++ b/src/MagicOnion.Server/MethodHandler.cs @@ -39,6 +39,7 @@ public class MethodHandler : IEquatable internal ILogger Logger { get; } internal bool IsReturnExceptionStackTraceInErrorDetail { get; } + public IMagicOnionSerializer MessageSerializer => messageSerializer; public string ServiceName => metadata.ServiceInterface.Name; @@ -491,6 +492,8 @@ public bool Equals(MethodHandler? other) public class UniqueEqualityComparer : IEqualityComparer { + public static UniqueEqualityComparer Instance { get; } = new(); + public bool Equals(MethodHandler? x, MethodHandler? y) { return (x == null && y == null) || (x != null && y != null && x.methodHandlerId.Equals(y.methodHandlerId)); diff --git a/src/MagicOnion/MagicOnion.csproj b/src/MagicOnion/MagicOnion.csproj index a5030d537..27af095b5 100644 --- a/src/MagicOnion/MagicOnion.csproj +++ b/src/MagicOnion/MagicOnion.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net6.0;net8.0 Library False diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Collector/MethodCollectorStreamingHubsTest.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Collector/MethodCollectorStreamingHubsTest.cs index 44ab43d47..6fddd0c3e 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Collector/MethodCollectorStreamingHubsTest.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Collector/MethodCollectorStreamingHubsTest.cs @@ -339,7 +339,7 @@ public interface IMyHubReceiver } [Fact] - public void ReturnType_NotSupported_Void() + public void ReturnType_NotSupported_NotTaskOfT() { // Arrange var source = @" @@ -352,7 +352,7 @@ namespace MyNamespace; public interface IMyHub : IStreamingHub { - void MethodA(); + string MethodA(); } public interface IMyHubReceiver @@ -373,7 +373,7 @@ public interface IMyHubReceiver } [Fact] - public void ReturnType_NotSupported_NotTaskOfT() + public void ReturnType_Task() { // Arrange var source = @" @@ -386,7 +386,7 @@ namespace MyNamespace; public interface IMyHub : IStreamingHub { - string MethodA(); + Task MethodA(); } public interface IMyHubReceiver @@ -402,12 +402,13 @@ public interface IMyHubReceiver var (serviceCollection, diagnostics) = MethodCollector.Collect(interfaceSymbols, referenceSymbols, CancellationToken.None); // Assert - diagnostics.Should().HaveCount(1); - diagnostics[0].Id.Should().Be(MagicOnionDiagnosticDescriptors.StreamingHubUnsupportedMethodReturnType.Id); + 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_Task); } [Fact] - public void ReturnType_Task() + public void ReturnType_TaskOfT() { // Arrange var source = @" @@ -420,7 +421,7 @@ namespace MyNamespace; public interface IMyHub : IStreamingHub { - Task MethodA(); + Task MethodA(); } public interface IMyHubReceiver @@ -437,12 +438,12 @@ public interface IMyHubReceiver // 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_Task); + 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 ReturnType_TaskOfT() + public void ReturnType_ValueTask() { // Arrange var source = @" @@ -455,7 +456,7 @@ namespace MyNamespace; public interface IMyHub : IStreamingHub { - Task MethodA(); + ValueTask MethodA(); } public interface IMyHubReceiver @@ -472,12 +473,12 @@ public interface IMyHubReceiver // 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>()); + 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_ValueTask() + public void ReturnType_ValueTaskOfT() { // Arrange var source = @" @@ -490,7 +491,7 @@ namespace MyNamespace; public interface IMyHub : IStreamingHub { - ValueTask MethodA(); + ValueTask MethodA(); } public interface IMyHubReceiver @@ -507,12 +508,12 @@ public interface IMyHubReceiver // 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); + 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 ReturnType_ValueTaskOfT() + public void ReturnType_Void() { // Arrange var source = @" @@ -525,7 +526,7 @@ namespace MyNamespace; public interface IMyHub : IStreamingHub { - ValueTask MethodA(); + void MethodA(); } public interface IMyHubReceiver @@ -542,8 +543,8 @@ public interface IMyHubReceiver // 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>()); + serviceCollection.Hubs[0].Methods[0].ResponseType.Should().Be(MagicOnionTypeInfo.KnownTypes.MessagePack_Nil); + serviceCollection.Hubs[0].Methods[0].MethodReturnType.Should().Be(MagicOnionTypeInfo.KnownTypes.System_Void); } [Fact] @@ -585,18 +586,21 @@ public interface IMyHubReceiver serviceCollection.Hubs[0].Receiver.Methods.Should().HaveCount(3); // void EventA(); serviceCollection.Hubs[0].Receiver.Methods[0].MethodName.Should().Be("EventA"); + serviceCollection.Hubs[0].Receiver.Methods[0].IsClientResult.Should().BeFalse(); serviceCollection.Hubs[0].Receiver.Methods[0].Parameters.Should().BeEmpty(); serviceCollection.Hubs[0].Receiver.Methods[0].RequestType.Should().Be(MagicOnionTypeInfo.KnownTypes.MessagePack_Nil); serviceCollection.Hubs[0].Receiver.Methods[0].ResponseType.Should().Be(MagicOnionTypeInfo.KnownTypes.MessagePack_Nil); serviceCollection.Hubs[0].Receiver.Methods[0].MethodReturnType.Should().Be(MagicOnionTypeInfo.KnownTypes.System_Void); // void EventB(Nil nil); serviceCollection.Hubs[0].Receiver.Methods[1].MethodName.Should().Be("EventB"); + serviceCollection.Hubs[0].Receiver.Methods[1].IsClientResult.Should().BeFalse(); serviceCollection.Hubs[0].Receiver.Methods[1].Parameters.Should().HaveCount(1); serviceCollection.Hubs[0].Receiver.Methods[1].RequestType.Should().Be(MagicOnionTypeInfo.KnownTypes.MessagePack_Nil); serviceCollection.Hubs[0].Receiver.Methods[1].ResponseType.Should().Be(MagicOnionTypeInfo.KnownTypes.MessagePack_Nil); serviceCollection.Hubs[0].Receiver.Methods[1].MethodReturnType.Should().Be(MagicOnionTypeInfo.KnownTypes.System_Void); // void EventB(Nil nil); serviceCollection.Hubs[0].Receiver.Methods[2].MethodName.Should().Be("EventC"); + serviceCollection.Hubs[0].Receiver.Methods[2].IsClientResult.Should().BeFalse(); serviceCollection.Hubs[0].Receiver.Methods[2].Parameters.Should().HaveCount(2); serviceCollection.Hubs[0].Receiver.Methods[2].RequestType.Should().Be(MagicOnionTypeInfo.CreateFromType>()); serviceCollection.Hubs[0].Receiver.Methods[2].ResponseType.Should().Be(MagicOnionTypeInfo.KnownTypes.MessagePack_Nil); @@ -604,7 +608,76 @@ public interface IMyHubReceiver } [Fact] - public void Receiver_NonVoidReturnType() + public void Receiver_ClientResult() + { + // Arrange + var source = @" +using System; +using System.Threading; +using System.Threading.Tasks; +using MagicOnion; +using MessagePack; + +namespace MyNamespace; + +public interface IMyHub : IStreamingHub +{ + Task MethodA(); +} + +public interface IMyHubReceiver +{ + Task ClientResultA(); + Task ClientResultB(Nil nil); + Task ClientResultC(string arg1, int arg2); + Task ClientResultD(string arg1, int arg2, CancellationToken cancellationToken); +} +"; + var (compilation, semModel) = CompilationHelper.Create(source); + if (!ReferenceSymbols.TryCreate(compilation, out var referenceSymbols)) throw new InvalidOperationException("Cannot create the reference symbols."); + var interfaceSymbols = MethodCollectorTestHelper.Traverse(compilation.Assembly.GlobalNamespace).ToImmutableArray(); + + // Act + var (serviceCollection, diagnostics) = MethodCollector.Collect(interfaceSymbols, referenceSymbols, CancellationToken.None); + + // Assert + serviceCollection.Should().NotBeNull(); + serviceCollection.Hubs.Should().HaveCount(1); + serviceCollection.Hubs[0].Methods.Should().HaveCount(1); + serviceCollection.Hubs[0].Receiver.Should().NotBeNull(); + serviceCollection.Hubs[0].Receiver.Methods.Should().HaveCount(4); + // Task ClientResultA(); + serviceCollection.Hubs[0].Receiver.Methods[0].MethodName.Should().Be("ClientResultA"); + serviceCollection.Hubs[0].Receiver.Methods[0].IsClientResult.Should().BeTrue(); + serviceCollection.Hubs[0].Receiver.Methods[0].Parameters.Should().BeEmpty(); + serviceCollection.Hubs[0].Receiver.Methods[0].RequestType.Should().Be(MagicOnionTypeInfo.KnownTypes.MessagePack_Nil); + serviceCollection.Hubs[0].Receiver.Methods[0].ResponseType.Should().Be(MagicOnionTypeInfo.KnownTypes.MessagePack_Nil); + serviceCollection.Hubs[0].Receiver.Methods[0].MethodReturnType.Should().Be(MagicOnionTypeInfo.KnownTypes.System_Threading_Tasks_Task); + // Task ClientResultB(Nil nil); + serviceCollection.Hubs[0].Receiver.Methods[1].MethodName.Should().Be("ClientResultB"); + serviceCollection.Hubs[0].Receiver.Methods[1].IsClientResult.Should().BeTrue(); + serviceCollection.Hubs[0].Receiver.Methods[1].Parameters.Should().HaveCount(1); + serviceCollection.Hubs[0].Receiver.Methods[1].RequestType.Should().Be(MagicOnionTypeInfo.KnownTypes.MessagePack_Nil); + serviceCollection.Hubs[0].Receiver.Methods[1].ResponseType.Should().Be(MagicOnionTypeInfo.CreateFromType()); + serviceCollection.Hubs[0].Receiver.Methods[1].MethodReturnType.Should().Be(MagicOnionTypeInfo.CreateFromType>()); + // Task ClientResultC(string arg1, int arg2); + serviceCollection.Hubs[0].Receiver.Methods[2].MethodName.Should().Be("ClientResultC"); + serviceCollection.Hubs[0].Receiver.Methods[2].IsClientResult.Should().BeTrue(); + serviceCollection.Hubs[0].Receiver.Methods[2].Parameters.Should().HaveCount(2); + serviceCollection.Hubs[0].Receiver.Methods[2].RequestType.Should().Be(MagicOnionTypeInfo.CreateFromType>()); + serviceCollection.Hubs[0].Receiver.Methods[2].ResponseType.Should().Be(MagicOnionTypeInfo.CreateFromType()); + serviceCollection.Hubs[0].Receiver.Methods[2].MethodReturnType.Should().Be(MagicOnionTypeInfo.CreateFromType>()); + // Task ClientResultD(string arg1, int arg2, CancellationToken cancellationToken); + serviceCollection.Hubs[0].Receiver.Methods[3].MethodName.Should().Be("ClientResultD"); + serviceCollection.Hubs[0].Receiver.Methods[3].IsClientResult.Should().BeTrue(); + serviceCollection.Hubs[0].Receiver.Methods[3].Parameters.Should().HaveCount(3); + serviceCollection.Hubs[0].Receiver.Methods[3].RequestType.Should().Be(MagicOnionTypeInfo.CreateFromType>()); // Skip CancellationToken + serviceCollection.Hubs[0].Receiver.Methods[3].ResponseType.Should().Be(MagicOnionTypeInfo.CreateFromType()); + serviceCollection.Hubs[0].Receiver.Methods[3].MethodReturnType.Should().Be(MagicOnionTypeInfo.CreateFromType>()); + } + + [Fact] + public void Receiver_ReturnTypeIsNotVoidOrTask() { // Arrange var source = @" @@ -637,6 +710,42 @@ public interface IMyHubReceiver diagnostics[0].Id.Should().Be(MagicOnionDiagnosticDescriptors.StreamingHubUnsupportedReceiverMethodReturnType.Id); } + [Fact] + public void Receiver_ReturnTypeIsTask() + { + // Arrange + var source = @" +using System; +using System.Threading.Tasks; +using MagicOnion; +using MessagePack; + +namespace MyNamespace; + +public interface IMyHub : IStreamingHub +{ + Task MethodA(); +} + +public interface IMyHubReceiver +{ + Task EventA(); + ValueTask EventB(); + Task EventC(); + ValueTask EventD(); +} +"; + var (compilation, semModel) = CompilationHelper.Create(source); + if (!ReferenceSymbols.TryCreate(compilation, out var referenceSymbols)) throw new InvalidOperationException("Cannot create the reference symbols."); + var interfaceSymbols = MethodCollectorTestHelper.Traverse(compilation.Assembly.GlobalNamespace).ToImmutableArray(); + + // Act + var (serviceCollection, diagnostics) = MethodCollector.Collect(interfaceSymbols, referenceSymbols, CancellationToken.None); + + // Assert + diagnostics.Should().BeEmpty(); + } + [Fact] public void StreamingHubInterfaces_TwoOrMore() { diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/GenerateStreamingHubTest.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/GenerateStreamingHubTest.cs index 2098f024a..5f27e0ff5 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/GenerateStreamingHubTest.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/GenerateStreamingHubTest.cs @@ -275,7 +275,7 @@ partial class MagicOnionInitializer {} } [Fact] - public async Task Invalid_Return_Void() + public async Task Return_Void() { var source = """ using System; @@ -289,7 +289,7 @@ namespace TempProject public interface IMyHubReceiver { } public interface IMyHub : IStreamingHub { - void {|#0:A|}(); + void A(); } [MagicOnionClientGeneration(typeof(IMyHub))] @@ -297,15 +297,7 @@ partial class MagicOnionInitializer {} } """; - var verifierOptions = VerifierOptions.Default with - { - TestBehaviorsOverride = TestBehaviors.SkipGeneratedSourcesCheck, - ExpectedDiagnostics = new[] - { - new DiagnosticResult(MagicOnionDiagnosticDescriptors.StreamingHubUnsupportedMethodReturnType.Id, DiagnosticSeverity.Error).WithLocation(0), - } - }; - await MagicOnionSourceGeneratorVerifier.RunAsync(source, verifierOptions: verifierOptions); + await MagicOnionSourceGeneratorVerifier.RunAsync(source); } [Fact] @@ -322,7 +314,7 @@ namespace TempProject { public interface IMyHubReceiver { - Task {|#0:B|}(); + int {|#0:B|}(); } public interface IMyHub : IStreamingHub { @@ -496,4 +488,268 @@ partial class MagicOnionInitializer {} await MagicOnionSourceGeneratorVerifier.RunAsync(source); } + + [Fact] + public async Task ClientResult_Parameter_Zero_NoReturnValue() + { + var source = """ + using System; + using System.Threading.Tasks; + using MessagePack; + using MagicOnion; + using MagicOnion.Client; + + namespace TempProject + { + public interface IMyHubReceiver + { + Task A(); + } + + public interface IMyHub : IStreamingHub + { + } + + [MagicOnionClientGeneration(typeof(IMyHub))] + partial class MagicOnionInitializer {} + } + """; + + await MagicOnionSourceGeneratorVerifier.RunAsync(source); + } + + [Fact] + public async Task ClientResult_Parameter_One_NoReturnValue() + { + var source = """ + using System; + using System.Threading.Tasks; + using MessagePack; + using MagicOnion; + using MagicOnion.Client; + + namespace TempProject + { + public interface IMyHubReceiver + { + Task A(string arg1); + } + + public interface IMyHub : IStreamingHub + { + } + + [MagicOnionClientGeneration(typeof(IMyHub))] + partial class MagicOnionInitializer {} + } + """; + + await MagicOnionSourceGeneratorVerifier.RunAsync(source); + } + + [Fact] + public async Task ClientResult_Parameter_Many_NoReturnValue() + { + var source = """ + using System; + using System.Threading.Tasks; + using MessagePack; + using MagicOnion; + using MagicOnion.Client; + + namespace TempProject + { + public interface IMyHubReceiver + { + Task A(string arg1, int arg2, bool arg3); + } + + public interface IMyHub : IStreamingHub + { + } + + [MagicOnionClientGeneration(typeof(IMyHub))] + partial class MagicOnionInitializer {} + } + """; + + await MagicOnionSourceGeneratorVerifier.RunAsync(source); + } + + [Fact] + public async Task ClientResult_Parameter_Zero() + { + var source = """ + using System; + using System.Threading.Tasks; + using MessagePack; + using MagicOnion; + using MagicOnion.Client; + + namespace TempProject + { + public interface IMyHubReceiver + { + Task A(); + } + + public interface IMyHub : IStreamingHub + { + } + + [MagicOnionClientGeneration(typeof(IMyHub))] + partial class MagicOnionInitializer {} + } + """; + + await MagicOnionSourceGeneratorVerifier.RunAsync(source); + } + + [Fact] + public async Task ClientResult_Parameter_One() + { + var source = """ + using System; + using System.Threading.Tasks; + using MessagePack; + using MagicOnion; + using MagicOnion.Client; + + namespace TempProject + { + public interface IMyHubReceiver + { + Task A(string arg1); + } + + public interface IMyHub : IStreamingHub + { + } + + [MagicOnionClientGeneration(typeof(IMyHub))] + partial class MagicOnionInitializer {} + } + """; + + await MagicOnionSourceGeneratorVerifier.RunAsync(source); + } + + [Fact] + public async Task ClientResult_Parameter_Many() + { + var source = """ + using System; + using System.Threading.Tasks; + using MessagePack; + using MagicOnion; + using MagicOnion.Client; + + namespace TempProject + { + public interface IMyHubReceiver + { + Task A(string arg1, int arg2, bool arg3); + } + + public interface IMyHub : IStreamingHub + { + } + + [MagicOnionClientGeneration(typeof(IMyHub))] + partial class MagicOnionInitializer {} + } + """; + + await MagicOnionSourceGeneratorVerifier.RunAsync(source); + } + + [Fact] + public async Task ClientResult_Parameter_Zero_With_Cancellation() + { + var source = """ + using System; + using System.Threading; + using System.Threading.Tasks; + using MessagePack; + using MagicOnion; + using MagicOnion.Client; + + namespace TempProject + { + public interface IMyHubReceiver + { + Task A(CancellationToken cancellationToken); + } + + public interface IMyHub : IStreamingHub + { + } + + [MagicOnionClientGeneration(typeof(IMyHub))] + partial class MagicOnionInitializer {} + } + """; + + await MagicOnionSourceGeneratorVerifier.RunAsync(source); + } + + [Fact] + public async Task ClientResult_Parameter_One_With_Cancellation() + { + var source = """ + using System; + using System.Threading; + using System.Threading.Tasks; + using MessagePack; + using MagicOnion; + using MagicOnion.Client; + + namespace TempProject + { + public interface IMyHubReceiver + { + Task A(string arg1, CancellationToken cancellationToken); + } + + public interface IMyHub : IStreamingHub + { + } + + [MagicOnionClientGeneration(typeof(IMyHub))] + partial class MagicOnionInitializer {} + } + """; + + await MagicOnionSourceGeneratorVerifier.RunAsync(source); + } + + [Fact] + public async Task ClientResult_Parameter_Many_With_Cancellation() + { + var source = """ + using System; + using System.Threading; + using System.Threading.Tasks; + using MessagePack; + using MagicOnion; + using MagicOnion.Client; + + namespace TempProject + { + public interface IMyHubReceiver + { + Task A(string arg1, int arg2, bool arg3, CancellationToken cancellationToken); + } + + public interface IMyHub : IStreamingHub + { + } + + [MagicOnionClientGeneration(typeof(IMyHub))] + partial class MagicOnionInitializer {} + } + """; + + await MagicOnionSourceGeneratorVerifier.RunAsync(source); + } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver/0003_TempProject_MyHubClient.g.cs index 3f215c7c3..677b6e9e3 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,15 +15,15 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -38,7 +39,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -57,13 +58,16 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs index da5defbc2..483411f3a 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,15 +15,15 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -38,7 +39,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -75,13 +76,16 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs index 3ff85ba8b..3879e511e 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,15 +15,15 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -38,7 +39,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -51,13 +52,16 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Enum/0003_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Enum/0003_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Enum/0003_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Enum/0003_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Enum/0004_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Enum/0004_TempProject_MyHubClient.g.cs index b3087f131..fec15f00e 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Enum/0004_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Enum/0004_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,15 +15,15 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -38,7 +39,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -51,13 +52,16 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs index 36725eda8..11d471cac 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,15 +15,15 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -38,7 +39,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -57,13 +58,16 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs index e0523a9c0..1adb4e3e3 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,15 +15,15 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -38,7 +39,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -51,13 +52,16 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs index e2b000fb3..ba049d815 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,15 +15,15 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -38,7 +39,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -57,13 +58,16 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested/0003_TempProject_MyHubClient.g.cs index 6269ae50f..e93afc026 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,15 +15,15 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -38,7 +39,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -63,13 +64,16 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Array/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Array/0003_TempProject_MyHubClient.g.cs index 338446145..df0c06eff 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Array/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Array/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,15 +15,15 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -38,7 +39,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -51,13 +52,16 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Enum/0004_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Enum/0004_TempProject_MyHubClient.g.cs index 671dfdd94..a94813684 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Enum/0004_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/HubReceiver_Nested_Enum/0004_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,15 +15,15 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -38,7 +39,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -51,13 +52,16 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters/0003_TempProject_MyHubClient.g.cs index 469f51796..bd880fc94 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,19 +15,19 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A(global::TempProject.MyGenericObject a) - => base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-1005848884, a); + => this.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-1005848884, a); public global::System.Threading.Tasks.Task B(global::TempProject.MyGenericObject a) - => base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-955516027, a); + => this.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-955516027, a); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -46,14 +47,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -66,6 +67,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs index 1e0d0e3b2..0f59ab118 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,25 +15,25 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task GetStringValuesAsync(global::System.String[] arg0) - => base.WriteMessageWithResponseAsync(1774317884, arg0); + => this.WriteMessageWithResponseAsync(1774317884, arg0); public global::System.Threading.Tasks.Task GetIntValuesAsync(global::System.Int32[] arg0) - => base.WriteMessageWithResponseAsync(-400881550, arg0); + => this.WriteMessageWithResponseAsync(-400881550, arg0); public global::System.Threading.Tasks.Task GetInt32ValuesAsync(global::System.Int32[] arg0) - => base.WriteMessageWithResponseAsync(309063297, arg0); + => this.WriteMessageWithResponseAsync(309063297, arg0); public global::System.Threading.Tasks.Task GetSingleValuesAsync(global::System.Single[] arg0) - => base.WriteMessageWithResponseAsync(702446639, arg0); + => this.WriteMessageWithResponseAsync(702446639, arg0); public global::System.Threading.Tasks.Task GetBooleanValuesAsync(global::System.Boolean[] arg0) - => base.WriteMessageWithResponseAsync(2082077357, arg0); + => this.WriteMessageWithResponseAsync(2082077357, arg0); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -58,14 +59,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -87,6 +88,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs index d4d564120..80e66d5b6 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task GetValuesAsync(global::TempProject.MyResponse[] arg0) - => base.WriteMessageWithResponseAsync(-209315513, arg0); + => this.WriteMessageWithResponseAsync(-209315513, arg0); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs index 6f8ff1d94..b7ea2d5c9 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,19 +15,19 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task GetStringValuesAsync(global::System.Collections.Generic.List arg0) - => base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(1774317884, arg0); + => this.WriteMessageWithResponseAsync, global::MessagePack.Nil>(1774317884, arg0); public global::System.Threading.Tasks.Task GetIntValuesAsync(global::System.Collections.Generic.List arg0) - => base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-400881550, arg0); + => this.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-400881550, arg0); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -46,14 +47,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -66,6 +67,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs index efe54efeb..0a54b0716 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task GetValuesAsync(global::System.Collections.Generic.List arg0) - => base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-209315513, arg0); + => this.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-209315513, arg0); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs index 16728fc25..76e746b8b 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,19 +15,19 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A(global::TempProject.MyGenericObject a) - => base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-1005848884, a); + => this.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-1005848884, a); public global::System.Threading.Tasks.Task B(global::TempProject.MyGenericObject a) - => base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-955516027, a); + => this.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-955516027, a); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -46,14 +47,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -66,6 +67,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested/0003_TempProject_MyHubClient.g.cs index ee6736522..1bd1d511a 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,21 +15,21 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A(global::TempProject.MyGenericObject> a) - => base.WriteMessageWithResponseAsync>, global::MessagePack.Nil>(-1005848884, a); + => this.WriteMessageWithResponseAsync>, global::MessagePack.Nil>(-1005848884, a); public global::System.Threading.Tasks.Task B(global::TempProject.MyGenericObject>> a) - => base.WriteMessageWithResponseAsync>>, global::MessagePack.Nil>(-955516027, a); + => this.WriteMessageWithResponseAsync>>, global::MessagePack.Nil>(-955516027, a); public global::System.Threading.Tasks.Task C(global::TempProject.MyGenericObject>> a) - => base.WriteMessageWithResponseAsync>>, global::MessagePack.Nil>(-972293646, a); + => this.WriteMessageWithResponseAsync>>, global::MessagePack.Nil>(-972293646, a); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -50,14 +51,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -73,6 +74,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Array/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Array/0003_TempProject_MyHubClient.g.cs index af905ee1b..bfaf60418 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Array/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Array/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task GetValuesAsync(global::TempProject.MyGenericObject arg0) - => base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-209315513, arg0); + => this.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-209315513, arg0); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Enum/0004_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Enum/0004_TempProject_MyHubClient.g.cs index fce0e6c50..41068dab3 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Enum/0004_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Parameters_Nested_Enum/0004_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task GetEnumAsync(global::TempProject.MyGenericObject> arg0) - => base.WriteMessageWithResponseAsync>, global::MessagePack.Nil>(-1221306238, arg0); + => this.WriteMessageWithResponseAsync>, global::MessagePack.Nil>(-1221306238, arg0); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return/0003_TempProject_MyHubClient.g.cs index 0b2c0893a..e372c377d 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,19 +15,19 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task> A() - => base.WriteMessageWithResponseAsync>(-1005848884, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>(-1005848884, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task> B() - => base.WriteMessageWithResponseAsync>(-955516027, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>(-955516027, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -46,14 +47,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -66,6 +67,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs index 91f75663c..497149524 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_KnownType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,25 +15,25 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task GetStringValuesAsync() - => base.WriteMessageWithResponseAsync(1774317884, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync(1774317884, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task GetIntValuesAsync() - => base.WriteMessageWithResponseAsync(-400881550, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync(-400881550, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task GetInt32ValuesAsync() - => base.WriteMessageWithResponseAsync(309063297, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync(309063297, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task GetSingleValuesAsync() - => base.WriteMessageWithResponseAsync(702446639, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync(702446639, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task GetBooleanValuesAsync() - => base.WriteMessageWithResponseAsync(2082077357, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync(2082077357, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -58,14 +59,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -87,6 +88,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs index 860cb3b9d..179eb255a 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ArrayFormatter_UserType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task GetValuesAsync() - => base.WriteMessageWithResponseAsync(-209315513, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync(-209315513, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Enum/0003_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Enum/0003_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Enum/0003_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Enum/0003_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Enum/0004_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Enum/0004_TempProject_MyHubClient.g.cs index d3dc1a9fc..9be0d785b 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Enum/0004_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Enum/0004_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task> GetEnumAsync() - => base.WriteMessageWithResponseAsync>(-1221306238, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>(-1221306238, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_KnownType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs index 14b0ffac0..84745714c 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_KnownType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,19 +15,19 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task> GetStringValuesAsync() - => base.WriteMessageWithResponseAsync>(1774317884, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>(1774317884, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task> GetIntValuesAsync() - => base.WriteMessageWithResponseAsync>(-400881550, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>(-400881550, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -46,14 +47,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -66,6 +67,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_UserType/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs index 5619401ae..1e84cbece 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_ListFormatter_UserType/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task> GetValuesAsync() - => base.WriteMessageWithResponseAsync>(-209315513, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>(-209315513, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_MultipleTypeArgs/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs index 81f4f205e..a4393e284 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_MultipleTypeArgs/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,19 +15,19 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task> A() - => base.WriteMessageWithResponseAsync>(-1005848884, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>(-1005848884, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task> B() - => base.WriteMessageWithResponseAsync>(-955516027, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>(-955516027, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -46,14 +47,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -66,6 +67,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested/0003_TempProject_MyHubClient.g.cs index 5fbdbc3c1..a6076ca21 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,21 +15,21 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task>> A() - => base.WriteMessageWithResponseAsync>>(-1005848884, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>>(-1005848884, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task>>> B() - => base.WriteMessageWithResponseAsync>>>(-955516027, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>>>(-955516027, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task>>> C() - => base.WriteMessageWithResponseAsync>>>(-972293646, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>>>(-972293646, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -50,14 +51,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -73,6 +74,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Array/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Array/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Array/0003_TempProject_MyHubClient.g.cs index 81c4a5076..66e3b1a08 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Array/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Array/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task> GetValuesAsync() - => base.WriteMessageWithResponseAsync>(-209315513, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>(-209315513, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Enum/0003_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Enum/0004_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Enum/0004_TempProject_MyHubClient.g.cs index 5e7e0b036..eb9873153 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Enum/0004_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateGenericsStreamingHubTest/Return_Nested_Enum/0004_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task>> GetEnumAsync() - => base.WriteMessageWithResponseAsync>>(-1221306238, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync>>(-1221306238, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubDiagnosticHandlerTest/Generate/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubDiagnosticHandlerTest/Generate/0002_TempProject_MagicOnionInitializer.g.cs index f243d412e..372064b64 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubDiagnosticHandlerTest/Generate/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubDiagnosticHandlerTest/Generate/0002_TempProject_MagicOnionInitializer.g.cs @@ -88,7 +88,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e, StreamingHubDiagnosticHandler))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, StreamingHubDiagnosticHandler))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubDiagnosticHandlerTest/Generate/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubDiagnosticHandlerTest/Generate/0003_TempProject_MyHubClient.g.cs index 3a6e816e3..b785a314d 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubDiagnosticHandlerTest/Generate/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubDiagnosticHandlerTest/Generate/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -16,8 +17,8 @@ public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubCli { readonly global::MagicOnion.Client.IStreamingHubDiagnosticHandler diagnosticHandler; - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger, global::MagicOnion.Client.IStreamingHubDiagnosticHandler diagnosticHandler) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options, global::MagicOnion.Client.IStreamingHubDiagnosticHandler diagnosticHandler) + : base("IMyHub", receiver, callInvoker, options) { this.diagnosticHandler = diagnosticHandler; } @@ -57,7 +58,7 @@ public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -85,7 +86,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -106,7 +107,7 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -131,6 +132,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0000_MagicOnionClientSourceGeneratorAttributes.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0000_MagicOnionClientSourceGeneratorAttributes.g.cs new file mode 100644 index 000000000..8faa1aa7e --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0000_MagicOnionClientSourceGeneratorAttributes.g.cs @@ -0,0 +1,49 @@ +// +namespace MagicOnion.Client +{ + /// + /// Marker attribute for generating clients of MagicOnion. + /// The source generator collects the classes specified by this attribute and uses them to generate source. + /// + [global::System.Diagnostics.Conditional("__MagicOnion_Client_SourceGenerator__DesignTimeOnly__")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)] + internal class MagicOnionClientGenerationAttribute : global::System.Attribute + { + /// + /// Gets or sets whether to disable automatically calling `Register` during start-up. (Automatic registration requires .NET 5+ or Unity) + /// + public bool DisableAutoRegistration { get; set; } + + /// + /// Gets or set the serializer used for message serialization. The default value is . + /// + public global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType Serializer { get; set; } = global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType.MessagePack; + + /// + /// Gets or set the namespace of pre-generated MessagePackFormatters. The default value is MessagePack.Formatters. + /// + public string MessagePackFormatterNamespace { get; set; } = "MessagePack.Formatters"; + + /// + /// Gets or set whether to enable the StreamingHandler diagnostic handler. This is for debugging purpose. The default value is . + /// + public bool EnableStreamingHubDiagnosticHandler { get; set; } = false; + + public string GenerateFileHintNamePrefix { get; set; } = string.Empty; + + public global::System.Type[] TypesContainedInTargetAssembly { get; } + + /// Types contained in the scan target assembly + public MagicOnionClientGenerationAttribute(params global::System.Type[] typesContainedInTargetAssembly) + { + TypesContainedInTargetAssembly = typesContainedInTargetAssembly; + } + + // This enum must be mirror of `SerializerType` (MagicOnionClientSourceGenerator) + internal enum GenerateSerializerType + { + MessagePack = 0, + MemoryPack = 1, + } + } +} \ No newline at end of file diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs new file mode 100644 index 000000000..1986ef1b3 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs @@ -0,0 +1,81 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block + +namespace TempProject +{ + using global::System; + using global::MessagePack; + + partial class MagicOnionInitializer + { + /// + /// Gets the generated MessagePack formatter resolver. + /// + public static global::MessagePack.IFormatterResolver Resolver => MessagePackGeneratedResolver.Instance; + class MessagePackGeneratedResolver : global::MessagePack.IFormatterResolver + { + public static readonly global::MessagePack.IFormatterResolver Instance = new MessagePackGeneratedResolver(); + + MessagePackGeneratedResolver() {} + + public global::MessagePack.Formatters.IMessagePackFormatter GetFormatter() + => FormatterCache.formatter; + + static class FormatterCache + { + public static readonly global::MessagePack.Formatters.IMessagePackFormatter formatter; + + static FormatterCache() + { + var f = MessagePackGeneratedGetFormatterHelper.GetFormatter(typeof(T)); + if (f != null) + { + formatter = (global::MessagePack.Formatters.IMessagePackFormatter)f; + } + } + } + } + static class MessagePackGeneratedGetFormatterHelper + { + static readonly global::System.Collections.Generic.Dictionary lookup; + + static MessagePackGeneratedGetFormatterHelper() + { + lookup = new global::System.Collections.Generic.Dictionary(1) + { + {typeof(global::MagicOnion.DynamicArgumentTuple), 0}, + }; + } + internal static object GetFormatter(global::System.Type t) + { + int key; + if (!lookup.TryGetValue(t, out key)) + { + return null; + } + + switch (key) + { + case 0: return new global::MagicOnion.Serialization.MessagePack.DynamicArgumentTupleFormatter(default(global::System.String), default(global::System.Int32), default(global::System.Boolean)); + default: return null; + } + } + } + /// Type hints for Ahead-of-Time compilation. + [Preserve] + static class TypeHints + { + [Preserve] + internal static void Register() + { + _ = MessagePackGeneratedResolver.Instance.GetFormatter>(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0002_TempProject_MagicOnionInitializer.g.cs new file mode 100644 index 000000000..ec5b0b223 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0002_TempProject_MagicOnionInitializer.g.cs @@ -0,0 +1,95 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +namespace TempProject +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::MagicOnion; + using global::MagicOnion.Client; + + partial class PreserveAttribute : global::System.Attribute {} + + partial class MagicOnionInitializer + { + static bool isRegistered = false; + readonly static MagicOnionGeneratedClientFactoryProvider provider = new(); + + /// + /// Gets the generated MagicOnionClientFactoryProvider. + /// + public static global::MagicOnion.Client.IMagicOnionClientFactoryProvider ClientFactoryProvider => provider; + + /// + /// Gets the generated StreamingHubClientFactoryProvider. + /// + public static global::MagicOnion.Client.IStreamingHubClientFactoryProvider StreamingHubClientFactoryProvider => provider; +#if UNITY_2019_4_OR_NEWER + [global::UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] +#elif NET5_0_OR_GREATER + [global::System.Runtime.CompilerServices.ModuleInitializer] +#endif + internal static void Register() => TryRegisterProviderFactory(); + + /// + /// Register the generated client factory providers if it's not registered yet. This method will register only once. + /// + public static bool TryRegisterProviderFactory() + { + if (isRegistered) return false; + isRegistered = true; + + global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default = + (global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider immutableMagicOnionClientFactoryProvider) + ? immutableMagicOnionClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider(provider); + + global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default = + (global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider immutableStreamingHubClientFactoryProvider) + ? immutableStreamingHubClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider(provider); + + return true; + } + + class MagicOnionGeneratedClientFactoryProvider : global::MagicOnion.Client.IMagicOnionClientFactoryProvider, global::MagicOnion.Client.IStreamingHubClientFactoryProvider + { + bool global::MagicOnion.Client.IMagicOnionClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.MagicOnionClientFactoryDelegate factory) + => (factory = MagicOnionClientFactoryCache.Factory) != null; + + bool global::MagicOnion.Client.IStreamingHubClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.StreamingHubClientFactoryDelegate factory) + => (factory = StreamingHubClientFactoryCache.Factory) != null; + + static class MagicOnionClientFactoryCache where T : global::MagicOnion.IService + { + public readonly static global::MagicOnion.Client.MagicOnionClientFactoryDelegate Factory; + + static MagicOnionClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.MagicOnionClientFactoryDelegate); + + Factory = (global::MagicOnion.Client.MagicOnionClientFactoryDelegate)factory; + } + } + + static class StreamingHubClientFactoryCache where TStreamingHub : global::MagicOnion.IStreamingHub + { + public readonly static global::MagicOnion.Client.StreamingHubClientFactoryDelegate Factory; + + static StreamingHubClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.StreamingHubClientFactoryDelegate); + + if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) + { + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); + } + + Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; + } + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0003_TempProject_MyHubClient.g.cs new file mode 100644 index 000000000..a09c79378 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many/0003_TempProject_MyHubClient.g.cs @@ -0,0 +1,79 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. + +namespace TempProject +{ + partial class MagicOnionInitializer + { + static partial class MagicOnionGeneratedClient + { + [global::MagicOnion.Ignore] + public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub + { + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) + { + } + + + public global::TempProject.IMyHub FireAndForget() + => new FireAndForgetClient(this); + + [global::MagicOnion.Ignore] + class FireAndForgetClient : global::TempProject.IMyHub + { + readonly TempProject_MyHubClient parent; + + public FireAndForgetClient(TempProject_MyHubClient parent) + => this.parent = parent; + + public global::TempProject.IMyHub FireAndForget() => this; + public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException(); + public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException(); + + + } + + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + try + { + switch (methodId) + { + case -1005848884: // Task A(global::System.String arg1, global::System.Int32 arg2, global::System.Boolean arg3) + { + var value = base.Deserialize>(data); + base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, receiver.A(value.Item1, value.Item2, value.Item3)); + } + break; + } + } + catch (global::System.Exception ex) + { + base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + } + } + + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0000_MagicOnionClientSourceGeneratorAttributes.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0000_MagicOnionClientSourceGeneratorAttributes.g.cs new file mode 100644 index 000000000..8faa1aa7e --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0000_MagicOnionClientSourceGeneratorAttributes.g.cs @@ -0,0 +1,49 @@ +// +namespace MagicOnion.Client +{ + /// + /// Marker attribute for generating clients of MagicOnion. + /// The source generator collects the classes specified by this attribute and uses them to generate source. + /// + [global::System.Diagnostics.Conditional("__MagicOnion_Client_SourceGenerator__DesignTimeOnly__")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)] + internal class MagicOnionClientGenerationAttribute : global::System.Attribute + { + /// + /// Gets or sets whether to disable automatically calling `Register` during start-up. (Automatic registration requires .NET 5+ or Unity) + /// + public bool DisableAutoRegistration { get; set; } + + /// + /// Gets or set the serializer used for message serialization. The default value is . + /// + public global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType Serializer { get; set; } = global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType.MessagePack; + + /// + /// Gets or set the namespace of pre-generated MessagePackFormatters. The default value is MessagePack.Formatters. + /// + public string MessagePackFormatterNamespace { get; set; } = "MessagePack.Formatters"; + + /// + /// Gets or set whether to enable the StreamingHandler diagnostic handler. This is for debugging purpose. The default value is . + /// + public bool EnableStreamingHubDiagnosticHandler { get; set; } = false; + + public string GenerateFileHintNamePrefix { get; set; } = string.Empty; + + public global::System.Type[] TypesContainedInTargetAssembly { get; } + + /// Types contained in the scan target assembly + public MagicOnionClientGenerationAttribute(params global::System.Type[] typesContainedInTargetAssembly) + { + TypesContainedInTargetAssembly = typesContainedInTargetAssembly; + } + + // This enum must be mirror of `SerializerType` (MagicOnionClientSourceGenerator) + internal enum GenerateSerializerType + { + MessagePack = 0, + MemoryPack = 1, + } + } +} \ No newline at end of file diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs new file mode 100644 index 000000000..ed61f4a33 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs @@ -0,0 +1,82 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block + +namespace TempProject +{ + using global::System; + using global::MessagePack; + + partial class MagicOnionInitializer + { + /// + /// Gets the generated MessagePack formatter resolver. + /// + public static global::MessagePack.IFormatterResolver Resolver => MessagePackGeneratedResolver.Instance; + class MessagePackGeneratedResolver : global::MessagePack.IFormatterResolver + { + public static readonly global::MessagePack.IFormatterResolver Instance = new MessagePackGeneratedResolver(); + + MessagePackGeneratedResolver() {} + + public global::MessagePack.Formatters.IMessagePackFormatter GetFormatter() + => FormatterCache.formatter; + + static class FormatterCache + { + public static readonly global::MessagePack.Formatters.IMessagePackFormatter formatter; + + static FormatterCache() + { + var f = MessagePackGeneratedGetFormatterHelper.GetFormatter(typeof(T)); + if (f != null) + { + formatter = (global::MessagePack.Formatters.IMessagePackFormatter)f; + } + } + } + } + static class MessagePackGeneratedGetFormatterHelper + { + static readonly global::System.Collections.Generic.Dictionary lookup; + + static MessagePackGeneratedGetFormatterHelper() + { + lookup = new global::System.Collections.Generic.Dictionary(1) + { + {typeof(global::MagicOnion.DynamicArgumentTuple), 0}, + }; + } + internal static object GetFormatter(global::System.Type t) + { + int key; + if (!lookup.TryGetValue(t, out key)) + { + return null; + } + + switch (key) + { + case 0: return new global::MagicOnion.Serialization.MessagePack.DynamicArgumentTupleFormatter(default(global::System.String), default(global::System.Int32), default(global::System.Boolean)); + default: return null; + } + } + } + /// Type hints for Ahead-of-Time compilation. + [Preserve] + static class TypeHints + { + [Preserve] + internal static void Register() + { + _ = MessagePackGeneratedResolver.Instance.GetFormatter>(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0002_TempProject_MagicOnionInitializer.g.cs new file mode 100644 index 000000000..ec5b0b223 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0002_TempProject_MagicOnionInitializer.g.cs @@ -0,0 +1,95 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +namespace TempProject +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::MagicOnion; + using global::MagicOnion.Client; + + partial class PreserveAttribute : global::System.Attribute {} + + partial class MagicOnionInitializer + { + static bool isRegistered = false; + readonly static MagicOnionGeneratedClientFactoryProvider provider = new(); + + /// + /// Gets the generated MagicOnionClientFactoryProvider. + /// + public static global::MagicOnion.Client.IMagicOnionClientFactoryProvider ClientFactoryProvider => provider; + + /// + /// Gets the generated StreamingHubClientFactoryProvider. + /// + public static global::MagicOnion.Client.IStreamingHubClientFactoryProvider StreamingHubClientFactoryProvider => provider; +#if UNITY_2019_4_OR_NEWER + [global::UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] +#elif NET5_0_OR_GREATER + [global::System.Runtime.CompilerServices.ModuleInitializer] +#endif + internal static void Register() => TryRegisterProviderFactory(); + + /// + /// Register the generated client factory providers if it's not registered yet. This method will register only once. + /// + public static bool TryRegisterProviderFactory() + { + if (isRegistered) return false; + isRegistered = true; + + global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default = + (global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider immutableMagicOnionClientFactoryProvider) + ? immutableMagicOnionClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider(provider); + + global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default = + (global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider immutableStreamingHubClientFactoryProvider) + ? immutableStreamingHubClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider(provider); + + return true; + } + + class MagicOnionGeneratedClientFactoryProvider : global::MagicOnion.Client.IMagicOnionClientFactoryProvider, global::MagicOnion.Client.IStreamingHubClientFactoryProvider + { + bool global::MagicOnion.Client.IMagicOnionClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.MagicOnionClientFactoryDelegate factory) + => (factory = MagicOnionClientFactoryCache.Factory) != null; + + bool global::MagicOnion.Client.IStreamingHubClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.StreamingHubClientFactoryDelegate factory) + => (factory = StreamingHubClientFactoryCache.Factory) != null; + + static class MagicOnionClientFactoryCache where T : global::MagicOnion.IService + { + public readonly static global::MagicOnion.Client.MagicOnionClientFactoryDelegate Factory; + + static MagicOnionClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.MagicOnionClientFactoryDelegate); + + Factory = (global::MagicOnion.Client.MagicOnionClientFactoryDelegate)factory; + } + } + + static class StreamingHubClientFactoryCache where TStreamingHub : global::MagicOnion.IStreamingHub + { + public readonly static global::MagicOnion.Client.StreamingHubClientFactoryDelegate Factory; + + static StreamingHubClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.StreamingHubClientFactoryDelegate); + + if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) + { + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); + } + + Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; + } + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0003_TempProject_MyHubClient.g.cs new file mode 100644 index 000000000..bf38bbd35 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_NoReturnValue/0003_TempProject_MyHubClient.g.cs @@ -0,0 +1,79 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. + +namespace TempProject +{ + partial class MagicOnionInitializer + { + static partial class MagicOnionGeneratedClient + { + [global::MagicOnion.Ignore] + public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub + { + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) + { + } + + + public global::TempProject.IMyHub FireAndForget() + => new FireAndForgetClient(this); + + [global::MagicOnion.Ignore] + class FireAndForgetClient : global::TempProject.IMyHub + { + readonly TempProject_MyHubClient parent; + + public FireAndForgetClient(TempProject_MyHubClient parent) + => this.parent = parent; + + public global::TempProject.IMyHub FireAndForget() => this; + public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException(); + public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException(); + + + } + + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + try + { + switch (methodId) + { + case -1005848884: // Task A(global::System.String arg1, global::System.Int32 arg2, global::System.Boolean arg3) + { + var value = base.Deserialize>(data); + base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, receiver.A(value.Item1, value.Item2, value.Item3)); + } + break; + } + } + catch (global::System.Exception ex) + { + base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + } + } + + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0000_MagicOnionClientSourceGeneratorAttributes.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0000_MagicOnionClientSourceGeneratorAttributes.g.cs new file mode 100644 index 000000000..8faa1aa7e --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0000_MagicOnionClientSourceGeneratorAttributes.g.cs @@ -0,0 +1,49 @@ +// +namespace MagicOnion.Client +{ + /// + /// Marker attribute for generating clients of MagicOnion. + /// The source generator collects the classes specified by this attribute and uses them to generate source. + /// + [global::System.Diagnostics.Conditional("__MagicOnion_Client_SourceGenerator__DesignTimeOnly__")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)] + internal class MagicOnionClientGenerationAttribute : global::System.Attribute + { + /// + /// Gets or sets whether to disable automatically calling `Register` during start-up. (Automatic registration requires .NET 5+ or Unity) + /// + public bool DisableAutoRegistration { get; set; } + + /// + /// Gets or set the serializer used for message serialization. The default value is . + /// + public global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType Serializer { get; set; } = global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType.MessagePack; + + /// + /// Gets or set the namespace of pre-generated MessagePackFormatters. The default value is MessagePack.Formatters. + /// + public string MessagePackFormatterNamespace { get; set; } = "MessagePack.Formatters"; + + /// + /// Gets or set whether to enable the StreamingHandler diagnostic handler. This is for debugging purpose. The default value is . + /// + public bool EnableStreamingHubDiagnosticHandler { get; set; } = false; + + public string GenerateFileHintNamePrefix { get; set; } = string.Empty; + + public global::System.Type[] TypesContainedInTargetAssembly { get; } + + /// Types contained in the scan target assembly + public MagicOnionClientGenerationAttribute(params global::System.Type[] typesContainedInTargetAssembly) + { + TypesContainedInTargetAssembly = typesContainedInTargetAssembly; + } + + // This enum must be mirror of `SerializerType` (MagicOnionClientSourceGenerator) + internal enum GenerateSerializerType + { + MessagePack = 0, + MemoryPack = 1, + } + } +} \ No newline at end of file diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs new file mode 100644 index 000000000..1986ef1b3 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs @@ -0,0 +1,81 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block + +namespace TempProject +{ + using global::System; + using global::MessagePack; + + partial class MagicOnionInitializer + { + /// + /// Gets the generated MessagePack formatter resolver. + /// + public static global::MessagePack.IFormatterResolver Resolver => MessagePackGeneratedResolver.Instance; + class MessagePackGeneratedResolver : global::MessagePack.IFormatterResolver + { + public static readonly global::MessagePack.IFormatterResolver Instance = new MessagePackGeneratedResolver(); + + MessagePackGeneratedResolver() {} + + public global::MessagePack.Formatters.IMessagePackFormatter GetFormatter() + => FormatterCache.formatter; + + static class FormatterCache + { + public static readonly global::MessagePack.Formatters.IMessagePackFormatter formatter; + + static FormatterCache() + { + var f = MessagePackGeneratedGetFormatterHelper.GetFormatter(typeof(T)); + if (f != null) + { + formatter = (global::MessagePack.Formatters.IMessagePackFormatter)f; + } + } + } + } + static class MessagePackGeneratedGetFormatterHelper + { + static readonly global::System.Collections.Generic.Dictionary lookup; + + static MessagePackGeneratedGetFormatterHelper() + { + lookup = new global::System.Collections.Generic.Dictionary(1) + { + {typeof(global::MagicOnion.DynamicArgumentTuple), 0}, + }; + } + internal static object GetFormatter(global::System.Type t) + { + int key; + if (!lookup.TryGetValue(t, out key)) + { + return null; + } + + switch (key) + { + case 0: return new global::MagicOnion.Serialization.MessagePack.DynamicArgumentTupleFormatter(default(global::System.String), default(global::System.Int32), default(global::System.Boolean)); + default: return null; + } + } + } + /// Type hints for Ahead-of-Time compilation. + [Preserve] + static class TypeHints + { + [Preserve] + internal static void Register() + { + _ = MessagePackGeneratedResolver.Instance.GetFormatter>(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0002_TempProject_MagicOnionInitializer.g.cs new file mode 100644 index 000000000..ec5b0b223 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0002_TempProject_MagicOnionInitializer.g.cs @@ -0,0 +1,95 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +namespace TempProject +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::MagicOnion; + using global::MagicOnion.Client; + + partial class PreserveAttribute : global::System.Attribute {} + + partial class MagicOnionInitializer + { + static bool isRegistered = false; + readonly static MagicOnionGeneratedClientFactoryProvider provider = new(); + + /// + /// Gets the generated MagicOnionClientFactoryProvider. + /// + public static global::MagicOnion.Client.IMagicOnionClientFactoryProvider ClientFactoryProvider => provider; + + /// + /// Gets the generated StreamingHubClientFactoryProvider. + /// + public static global::MagicOnion.Client.IStreamingHubClientFactoryProvider StreamingHubClientFactoryProvider => provider; +#if UNITY_2019_4_OR_NEWER + [global::UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] +#elif NET5_0_OR_GREATER + [global::System.Runtime.CompilerServices.ModuleInitializer] +#endif + internal static void Register() => TryRegisterProviderFactory(); + + /// + /// Register the generated client factory providers if it's not registered yet. This method will register only once. + /// + public static bool TryRegisterProviderFactory() + { + if (isRegistered) return false; + isRegistered = true; + + global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default = + (global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider immutableMagicOnionClientFactoryProvider) + ? immutableMagicOnionClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider(provider); + + global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default = + (global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider immutableStreamingHubClientFactoryProvider) + ? immutableStreamingHubClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider(provider); + + return true; + } + + class MagicOnionGeneratedClientFactoryProvider : global::MagicOnion.Client.IMagicOnionClientFactoryProvider, global::MagicOnion.Client.IStreamingHubClientFactoryProvider + { + bool global::MagicOnion.Client.IMagicOnionClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.MagicOnionClientFactoryDelegate factory) + => (factory = MagicOnionClientFactoryCache.Factory) != null; + + bool global::MagicOnion.Client.IStreamingHubClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.StreamingHubClientFactoryDelegate factory) + => (factory = StreamingHubClientFactoryCache.Factory) != null; + + static class MagicOnionClientFactoryCache where T : global::MagicOnion.IService + { + public readonly static global::MagicOnion.Client.MagicOnionClientFactoryDelegate Factory; + + static MagicOnionClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.MagicOnionClientFactoryDelegate); + + Factory = (global::MagicOnion.Client.MagicOnionClientFactoryDelegate)factory; + } + } + + static class StreamingHubClientFactoryCache where TStreamingHub : global::MagicOnion.IStreamingHub + { + public readonly static global::MagicOnion.Client.StreamingHubClientFactoryDelegate Factory; + + static StreamingHubClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.StreamingHubClientFactoryDelegate); + + if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) + { + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); + } + + Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; + } + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0003_TempProject_MyHubClient.g.cs new file mode 100644 index 000000000..ffe51a96d --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Many_With_Cancellation/0003_TempProject_MyHubClient.g.cs @@ -0,0 +1,79 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. + +namespace TempProject +{ + partial class MagicOnionInitializer + { + static partial class MagicOnionGeneratedClient + { + [global::MagicOnion.Ignore] + public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub + { + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) + { + } + + + public global::TempProject.IMyHub FireAndForget() + => new FireAndForgetClient(this); + + [global::MagicOnion.Ignore] + class FireAndForgetClient : global::TempProject.IMyHub + { + readonly TempProject_MyHubClient parent; + + public FireAndForgetClient(TempProject_MyHubClient parent) + => this.parent = parent; + + public global::TempProject.IMyHub FireAndForget() => this; + public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException(); + public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException(); + + + } + + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + try + { + switch (methodId) + { + case -1005848884: // Task A(global::System.String arg1, global::System.Int32 arg2, global::System.Boolean arg3, global::System.Threading.CancellationToken cancellationToken) + { + var value = base.Deserialize>(data); + base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, receiver.A(value.Item1, value.Item2, value.Item3, default)); + } + break; + } + } + catch (global::System.Exception ex) + { + base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + } + } + + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0000_MagicOnionClientSourceGeneratorAttributes.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0000_MagicOnionClientSourceGeneratorAttributes.g.cs new file mode 100644 index 000000000..8faa1aa7e --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0000_MagicOnionClientSourceGeneratorAttributes.g.cs @@ -0,0 +1,49 @@ +// +namespace MagicOnion.Client +{ + /// + /// Marker attribute for generating clients of MagicOnion. + /// The source generator collects the classes specified by this attribute and uses them to generate source. + /// + [global::System.Diagnostics.Conditional("__MagicOnion_Client_SourceGenerator__DesignTimeOnly__")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)] + internal class MagicOnionClientGenerationAttribute : global::System.Attribute + { + /// + /// Gets or sets whether to disable automatically calling `Register` during start-up. (Automatic registration requires .NET 5+ or Unity) + /// + public bool DisableAutoRegistration { get; set; } + + /// + /// Gets or set the serializer used for message serialization. The default value is . + /// + public global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType Serializer { get; set; } = global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType.MessagePack; + + /// + /// Gets or set the namespace of pre-generated MessagePackFormatters. The default value is MessagePack.Formatters. + /// + public string MessagePackFormatterNamespace { get; set; } = "MessagePack.Formatters"; + + /// + /// Gets or set whether to enable the StreamingHandler diagnostic handler. This is for debugging purpose. The default value is . + /// + public bool EnableStreamingHubDiagnosticHandler { get; set; } = false; + + public string GenerateFileHintNamePrefix { get; set; } = string.Empty; + + public global::System.Type[] TypesContainedInTargetAssembly { get; } + + /// Types contained in the scan target assembly + public MagicOnionClientGenerationAttribute(params global::System.Type[] typesContainedInTargetAssembly) + { + TypesContainedInTargetAssembly = typesContainedInTargetAssembly; + } + + // This enum must be mirror of `SerializerType` (MagicOnionClientSourceGenerator) + internal enum GenerateSerializerType + { + MessagePack = 0, + MemoryPack = 1, + } + } +} \ No newline at end of file diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs new file mode 100644 index 000000000..11af1e740 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs @@ -0,0 +1,76 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block + +namespace TempProject +{ + using global::System; + using global::MessagePack; + + partial class MagicOnionInitializer + { + /// + /// Gets the generated MessagePack formatter resolver. + /// + public static global::MessagePack.IFormatterResolver Resolver => MessagePackGeneratedResolver.Instance; + class MessagePackGeneratedResolver : global::MessagePack.IFormatterResolver + { + public static readonly global::MessagePack.IFormatterResolver Instance = new MessagePackGeneratedResolver(); + + MessagePackGeneratedResolver() {} + + public global::MessagePack.Formatters.IMessagePackFormatter GetFormatter() + => FormatterCache.formatter; + + static class FormatterCache + { + public static readonly global::MessagePack.Formatters.IMessagePackFormatter formatter; + + static FormatterCache() + { + var f = MessagePackGeneratedGetFormatterHelper.GetFormatter(typeof(T)); + if (f != null) + { + formatter = (global::MessagePack.Formatters.IMessagePackFormatter)f; + } + } + } + } + static class MessagePackGeneratedGetFormatterHelper + { + static readonly global::System.Collections.Generic.Dictionary lookup; + + static MessagePackGeneratedGetFormatterHelper() + { + lookup = new global::System.Collections.Generic.Dictionary(0) + { + }; + } + internal static object GetFormatter(global::System.Type t) + { + int key; + if (!lookup.TryGetValue(t, out key)) + { + return null; + } + + switch (key) + { + default: return null; + } + } + } + /// Type hints for Ahead-of-Time compilation. + [Preserve] + static class TypeHints + { + [Preserve] + internal static void Register() + { + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0002_TempProject_MagicOnionInitializer.g.cs new file mode 100644 index 000000000..ec5b0b223 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0002_TempProject_MagicOnionInitializer.g.cs @@ -0,0 +1,95 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +namespace TempProject +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::MagicOnion; + using global::MagicOnion.Client; + + partial class PreserveAttribute : global::System.Attribute {} + + partial class MagicOnionInitializer + { + static bool isRegistered = false; + readonly static MagicOnionGeneratedClientFactoryProvider provider = new(); + + /// + /// Gets the generated MagicOnionClientFactoryProvider. + /// + public static global::MagicOnion.Client.IMagicOnionClientFactoryProvider ClientFactoryProvider => provider; + + /// + /// Gets the generated StreamingHubClientFactoryProvider. + /// + public static global::MagicOnion.Client.IStreamingHubClientFactoryProvider StreamingHubClientFactoryProvider => provider; +#if UNITY_2019_4_OR_NEWER + [global::UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] +#elif NET5_0_OR_GREATER + [global::System.Runtime.CompilerServices.ModuleInitializer] +#endif + internal static void Register() => TryRegisterProviderFactory(); + + /// + /// Register the generated client factory providers if it's not registered yet. This method will register only once. + /// + public static bool TryRegisterProviderFactory() + { + if (isRegistered) return false; + isRegistered = true; + + global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default = + (global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider immutableMagicOnionClientFactoryProvider) + ? immutableMagicOnionClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider(provider); + + global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default = + (global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider immutableStreamingHubClientFactoryProvider) + ? immutableStreamingHubClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider(provider); + + return true; + } + + class MagicOnionGeneratedClientFactoryProvider : global::MagicOnion.Client.IMagicOnionClientFactoryProvider, global::MagicOnion.Client.IStreamingHubClientFactoryProvider + { + bool global::MagicOnion.Client.IMagicOnionClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.MagicOnionClientFactoryDelegate factory) + => (factory = MagicOnionClientFactoryCache.Factory) != null; + + bool global::MagicOnion.Client.IStreamingHubClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.StreamingHubClientFactoryDelegate factory) + => (factory = StreamingHubClientFactoryCache.Factory) != null; + + static class MagicOnionClientFactoryCache where T : global::MagicOnion.IService + { + public readonly static global::MagicOnion.Client.MagicOnionClientFactoryDelegate Factory; + + static MagicOnionClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.MagicOnionClientFactoryDelegate); + + Factory = (global::MagicOnion.Client.MagicOnionClientFactoryDelegate)factory; + } + } + + static class StreamingHubClientFactoryCache where TStreamingHub : global::MagicOnion.IStreamingHub + { + public readonly static global::MagicOnion.Client.StreamingHubClientFactoryDelegate Factory; + + static StreamingHubClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.StreamingHubClientFactoryDelegate); + + if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) + { + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); + } + + Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; + } + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0003_TempProject_MyHubClient.g.cs new file mode 100644 index 000000000..f7c21cc11 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One/0003_TempProject_MyHubClient.g.cs @@ -0,0 +1,79 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. + +namespace TempProject +{ + partial class MagicOnionInitializer + { + static partial class MagicOnionGeneratedClient + { + [global::MagicOnion.Ignore] + public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub + { + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) + { + } + + + public global::TempProject.IMyHub FireAndForget() + => new FireAndForgetClient(this); + + [global::MagicOnion.Ignore] + class FireAndForgetClient : global::TempProject.IMyHub + { + readonly TempProject_MyHubClient parent; + + public FireAndForgetClient(TempProject_MyHubClient parent) + => this.parent = parent; + + public global::TempProject.IMyHub FireAndForget() => this; + public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException(); + public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException(); + + + } + + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + try + { + switch (methodId) + { + case -1005848884: // Task A(global::System.String arg1) + { + var value = base.Deserialize(data); + base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, receiver.A(value)); + } + break; + } + } + catch (global::System.Exception ex) + { + base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + } + } + + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0000_MagicOnionClientSourceGeneratorAttributes.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0000_MagicOnionClientSourceGeneratorAttributes.g.cs new file mode 100644 index 000000000..8faa1aa7e --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0000_MagicOnionClientSourceGeneratorAttributes.g.cs @@ -0,0 +1,49 @@ +// +namespace MagicOnion.Client +{ + /// + /// Marker attribute for generating clients of MagicOnion. + /// The source generator collects the classes specified by this attribute and uses them to generate source. + /// + [global::System.Diagnostics.Conditional("__MagicOnion_Client_SourceGenerator__DesignTimeOnly__")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)] + internal class MagicOnionClientGenerationAttribute : global::System.Attribute + { + /// + /// Gets or sets whether to disable automatically calling `Register` during start-up. (Automatic registration requires .NET 5+ or Unity) + /// + public bool DisableAutoRegistration { get; set; } + + /// + /// Gets or set the serializer used for message serialization. The default value is . + /// + public global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType Serializer { get; set; } = global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType.MessagePack; + + /// + /// Gets or set the namespace of pre-generated MessagePackFormatters. The default value is MessagePack.Formatters. + /// + public string MessagePackFormatterNamespace { get; set; } = "MessagePack.Formatters"; + + /// + /// Gets or set whether to enable the StreamingHandler diagnostic handler. This is for debugging purpose. The default value is . + /// + public bool EnableStreamingHubDiagnosticHandler { get; set; } = false; + + public string GenerateFileHintNamePrefix { get; set; } = string.Empty; + + public global::System.Type[] TypesContainedInTargetAssembly { get; } + + /// Types contained in the scan target assembly + public MagicOnionClientGenerationAttribute(params global::System.Type[] typesContainedInTargetAssembly) + { + TypesContainedInTargetAssembly = typesContainedInTargetAssembly; + } + + // This enum must be mirror of `SerializerType` (MagicOnionClientSourceGenerator) + internal enum GenerateSerializerType + { + MessagePack = 0, + MemoryPack = 1, + } + } +} \ No newline at end of file diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs new file mode 100644 index 000000000..5bbd9ab2b --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs @@ -0,0 +1,77 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block + +namespace TempProject +{ + using global::System; + using global::MessagePack; + + partial class MagicOnionInitializer + { + /// + /// Gets the generated MessagePack formatter resolver. + /// + public static global::MessagePack.IFormatterResolver Resolver => MessagePackGeneratedResolver.Instance; + class MessagePackGeneratedResolver : global::MessagePack.IFormatterResolver + { + public static readonly global::MessagePack.IFormatterResolver Instance = new MessagePackGeneratedResolver(); + + MessagePackGeneratedResolver() {} + + public global::MessagePack.Formatters.IMessagePackFormatter GetFormatter() + => FormatterCache.formatter; + + static class FormatterCache + { + public static readonly global::MessagePack.Formatters.IMessagePackFormatter formatter; + + static FormatterCache() + { + var f = MessagePackGeneratedGetFormatterHelper.GetFormatter(typeof(T)); + if (f != null) + { + formatter = (global::MessagePack.Formatters.IMessagePackFormatter)f; + } + } + } + } + static class MessagePackGeneratedGetFormatterHelper + { + static readonly global::System.Collections.Generic.Dictionary lookup; + + static MessagePackGeneratedGetFormatterHelper() + { + lookup = new global::System.Collections.Generic.Dictionary(0) + { + }; + } + internal static object GetFormatter(global::System.Type t) + { + int key; + if (!lookup.TryGetValue(t, out key)) + { + return null; + } + + switch (key) + { + default: return null; + } + } + } + /// Type hints for Ahead-of-Time compilation. + [Preserve] + static class TypeHints + { + [Preserve] + internal static void Register() + { + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0002_TempProject_MagicOnionInitializer.g.cs new file mode 100644 index 000000000..ec5b0b223 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0002_TempProject_MagicOnionInitializer.g.cs @@ -0,0 +1,95 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +namespace TempProject +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::MagicOnion; + using global::MagicOnion.Client; + + partial class PreserveAttribute : global::System.Attribute {} + + partial class MagicOnionInitializer + { + static bool isRegistered = false; + readonly static MagicOnionGeneratedClientFactoryProvider provider = new(); + + /// + /// Gets the generated MagicOnionClientFactoryProvider. + /// + public static global::MagicOnion.Client.IMagicOnionClientFactoryProvider ClientFactoryProvider => provider; + + /// + /// Gets the generated StreamingHubClientFactoryProvider. + /// + public static global::MagicOnion.Client.IStreamingHubClientFactoryProvider StreamingHubClientFactoryProvider => provider; +#if UNITY_2019_4_OR_NEWER + [global::UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] +#elif NET5_0_OR_GREATER + [global::System.Runtime.CompilerServices.ModuleInitializer] +#endif + internal static void Register() => TryRegisterProviderFactory(); + + /// + /// Register the generated client factory providers if it's not registered yet. This method will register only once. + /// + public static bool TryRegisterProviderFactory() + { + if (isRegistered) return false; + isRegistered = true; + + global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default = + (global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider immutableMagicOnionClientFactoryProvider) + ? immutableMagicOnionClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider(provider); + + global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default = + (global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider immutableStreamingHubClientFactoryProvider) + ? immutableStreamingHubClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider(provider); + + return true; + } + + class MagicOnionGeneratedClientFactoryProvider : global::MagicOnion.Client.IMagicOnionClientFactoryProvider, global::MagicOnion.Client.IStreamingHubClientFactoryProvider + { + bool global::MagicOnion.Client.IMagicOnionClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.MagicOnionClientFactoryDelegate factory) + => (factory = MagicOnionClientFactoryCache.Factory) != null; + + bool global::MagicOnion.Client.IStreamingHubClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.StreamingHubClientFactoryDelegate factory) + => (factory = StreamingHubClientFactoryCache.Factory) != null; + + static class MagicOnionClientFactoryCache where T : global::MagicOnion.IService + { + public readonly static global::MagicOnion.Client.MagicOnionClientFactoryDelegate Factory; + + static MagicOnionClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.MagicOnionClientFactoryDelegate); + + Factory = (global::MagicOnion.Client.MagicOnionClientFactoryDelegate)factory; + } + } + + static class StreamingHubClientFactoryCache where TStreamingHub : global::MagicOnion.IStreamingHub + { + public readonly static global::MagicOnion.Client.StreamingHubClientFactoryDelegate Factory; + + static StreamingHubClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.StreamingHubClientFactoryDelegate); + + if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) + { + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); + } + + Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; + } + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0003_TempProject_MyHubClient.g.cs new file mode 100644 index 000000000..121b3b386 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_NoReturnValue/0003_TempProject_MyHubClient.g.cs @@ -0,0 +1,79 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. + +namespace TempProject +{ + partial class MagicOnionInitializer + { + static partial class MagicOnionGeneratedClient + { + [global::MagicOnion.Ignore] + public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub + { + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) + { + } + + + public global::TempProject.IMyHub FireAndForget() + => new FireAndForgetClient(this); + + [global::MagicOnion.Ignore] + class FireAndForgetClient : global::TempProject.IMyHub + { + readonly TempProject_MyHubClient parent; + + public FireAndForgetClient(TempProject_MyHubClient parent) + => this.parent = parent; + + public global::TempProject.IMyHub FireAndForget() => this; + public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException(); + public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException(); + + + } + + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + try + { + switch (methodId) + { + case -1005848884: // Task A(global::System.String arg1) + { + var value = base.Deserialize(data); + base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, receiver.A(value)); + } + break; + } + } + catch (global::System.Exception ex) + { + base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + } + } + + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0000_MagicOnionClientSourceGeneratorAttributes.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0000_MagicOnionClientSourceGeneratorAttributes.g.cs new file mode 100644 index 000000000..8faa1aa7e --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0000_MagicOnionClientSourceGeneratorAttributes.g.cs @@ -0,0 +1,49 @@ +// +namespace MagicOnion.Client +{ + /// + /// Marker attribute for generating clients of MagicOnion. + /// The source generator collects the classes specified by this attribute and uses them to generate source. + /// + [global::System.Diagnostics.Conditional("__MagicOnion_Client_SourceGenerator__DesignTimeOnly__")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)] + internal class MagicOnionClientGenerationAttribute : global::System.Attribute + { + /// + /// Gets or sets whether to disable automatically calling `Register` during start-up. (Automatic registration requires .NET 5+ or Unity) + /// + public bool DisableAutoRegistration { get; set; } + + /// + /// Gets or set the serializer used for message serialization. The default value is . + /// + public global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType Serializer { get; set; } = global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType.MessagePack; + + /// + /// Gets or set the namespace of pre-generated MessagePackFormatters. The default value is MessagePack.Formatters. + /// + public string MessagePackFormatterNamespace { get; set; } = "MessagePack.Formatters"; + + /// + /// Gets or set whether to enable the StreamingHandler diagnostic handler. This is for debugging purpose. The default value is . + /// + public bool EnableStreamingHubDiagnosticHandler { get; set; } = false; + + public string GenerateFileHintNamePrefix { get; set; } = string.Empty; + + public global::System.Type[] TypesContainedInTargetAssembly { get; } + + /// Types contained in the scan target assembly + public MagicOnionClientGenerationAttribute(params global::System.Type[] typesContainedInTargetAssembly) + { + TypesContainedInTargetAssembly = typesContainedInTargetAssembly; + } + + // This enum must be mirror of `SerializerType` (MagicOnionClientSourceGenerator) + internal enum GenerateSerializerType + { + MessagePack = 0, + MemoryPack = 1, + } + } +} \ No newline at end of file diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs new file mode 100644 index 000000000..11af1e740 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs @@ -0,0 +1,76 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block + +namespace TempProject +{ + using global::System; + using global::MessagePack; + + partial class MagicOnionInitializer + { + /// + /// Gets the generated MessagePack formatter resolver. + /// + public static global::MessagePack.IFormatterResolver Resolver => MessagePackGeneratedResolver.Instance; + class MessagePackGeneratedResolver : global::MessagePack.IFormatterResolver + { + public static readonly global::MessagePack.IFormatterResolver Instance = new MessagePackGeneratedResolver(); + + MessagePackGeneratedResolver() {} + + public global::MessagePack.Formatters.IMessagePackFormatter GetFormatter() + => FormatterCache.formatter; + + static class FormatterCache + { + public static readonly global::MessagePack.Formatters.IMessagePackFormatter formatter; + + static FormatterCache() + { + var f = MessagePackGeneratedGetFormatterHelper.GetFormatter(typeof(T)); + if (f != null) + { + formatter = (global::MessagePack.Formatters.IMessagePackFormatter)f; + } + } + } + } + static class MessagePackGeneratedGetFormatterHelper + { + static readonly global::System.Collections.Generic.Dictionary lookup; + + static MessagePackGeneratedGetFormatterHelper() + { + lookup = new global::System.Collections.Generic.Dictionary(0) + { + }; + } + internal static object GetFormatter(global::System.Type t) + { + int key; + if (!lookup.TryGetValue(t, out key)) + { + return null; + } + + switch (key) + { + default: return null; + } + } + } + /// Type hints for Ahead-of-Time compilation. + [Preserve] + static class TypeHints + { + [Preserve] + internal static void Register() + { + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0002_TempProject_MagicOnionInitializer.g.cs new file mode 100644 index 000000000..ec5b0b223 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0002_TempProject_MagicOnionInitializer.g.cs @@ -0,0 +1,95 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +namespace TempProject +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::MagicOnion; + using global::MagicOnion.Client; + + partial class PreserveAttribute : global::System.Attribute {} + + partial class MagicOnionInitializer + { + static bool isRegistered = false; + readonly static MagicOnionGeneratedClientFactoryProvider provider = new(); + + /// + /// Gets the generated MagicOnionClientFactoryProvider. + /// + public static global::MagicOnion.Client.IMagicOnionClientFactoryProvider ClientFactoryProvider => provider; + + /// + /// Gets the generated StreamingHubClientFactoryProvider. + /// + public static global::MagicOnion.Client.IStreamingHubClientFactoryProvider StreamingHubClientFactoryProvider => provider; +#if UNITY_2019_4_OR_NEWER + [global::UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] +#elif NET5_0_OR_GREATER + [global::System.Runtime.CompilerServices.ModuleInitializer] +#endif + internal static void Register() => TryRegisterProviderFactory(); + + /// + /// Register the generated client factory providers if it's not registered yet. This method will register only once. + /// + public static bool TryRegisterProviderFactory() + { + if (isRegistered) return false; + isRegistered = true; + + global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default = + (global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider immutableMagicOnionClientFactoryProvider) + ? immutableMagicOnionClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider(provider); + + global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default = + (global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider immutableStreamingHubClientFactoryProvider) + ? immutableStreamingHubClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider(provider); + + return true; + } + + class MagicOnionGeneratedClientFactoryProvider : global::MagicOnion.Client.IMagicOnionClientFactoryProvider, global::MagicOnion.Client.IStreamingHubClientFactoryProvider + { + bool global::MagicOnion.Client.IMagicOnionClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.MagicOnionClientFactoryDelegate factory) + => (factory = MagicOnionClientFactoryCache.Factory) != null; + + bool global::MagicOnion.Client.IStreamingHubClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.StreamingHubClientFactoryDelegate factory) + => (factory = StreamingHubClientFactoryCache.Factory) != null; + + static class MagicOnionClientFactoryCache where T : global::MagicOnion.IService + { + public readonly static global::MagicOnion.Client.MagicOnionClientFactoryDelegate Factory; + + static MagicOnionClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.MagicOnionClientFactoryDelegate); + + Factory = (global::MagicOnion.Client.MagicOnionClientFactoryDelegate)factory; + } + } + + static class StreamingHubClientFactoryCache where TStreamingHub : global::MagicOnion.IStreamingHub + { + public readonly static global::MagicOnion.Client.StreamingHubClientFactoryDelegate Factory; + + static StreamingHubClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.StreamingHubClientFactoryDelegate); + + if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) + { + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); + } + + Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; + } + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0003_TempProject_MyHubClient.g.cs new file mode 100644 index 000000000..03e7acbe8 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_One_With_Cancellation/0003_TempProject_MyHubClient.g.cs @@ -0,0 +1,79 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. + +namespace TempProject +{ + partial class MagicOnionInitializer + { + static partial class MagicOnionGeneratedClient + { + [global::MagicOnion.Ignore] + public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub + { + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) + { + } + + + public global::TempProject.IMyHub FireAndForget() + => new FireAndForgetClient(this); + + [global::MagicOnion.Ignore] + class FireAndForgetClient : global::TempProject.IMyHub + { + readonly TempProject_MyHubClient parent; + + public FireAndForgetClient(TempProject_MyHubClient parent) + => this.parent = parent; + + public global::TempProject.IMyHub FireAndForget() => this; + public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException(); + public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException(); + + + } + + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + try + { + switch (methodId) + { + case -1005848884: // Task A(global::System.String arg1, global::System.Threading.CancellationToken cancellationToken) + { + var value = base.Deserialize(data); + base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, receiver.A(value, default)); + } + break; + } + } + catch (global::System.Exception ex) + { + base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + } + } + + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0000_MagicOnionClientSourceGeneratorAttributes.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0000_MagicOnionClientSourceGeneratorAttributes.g.cs new file mode 100644 index 000000000..8faa1aa7e --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0000_MagicOnionClientSourceGeneratorAttributes.g.cs @@ -0,0 +1,49 @@ +// +namespace MagicOnion.Client +{ + /// + /// Marker attribute for generating clients of MagicOnion. + /// The source generator collects the classes specified by this attribute and uses them to generate source. + /// + [global::System.Diagnostics.Conditional("__MagicOnion_Client_SourceGenerator__DesignTimeOnly__")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)] + internal class MagicOnionClientGenerationAttribute : global::System.Attribute + { + /// + /// Gets or sets whether to disable automatically calling `Register` during start-up. (Automatic registration requires .NET 5+ or Unity) + /// + public bool DisableAutoRegistration { get; set; } + + /// + /// Gets or set the serializer used for message serialization. The default value is . + /// + public global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType Serializer { get; set; } = global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType.MessagePack; + + /// + /// Gets or set the namespace of pre-generated MessagePackFormatters. The default value is MessagePack.Formatters. + /// + public string MessagePackFormatterNamespace { get; set; } = "MessagePack.Formatters"; + + /// + /// Gets or set whether to enable the StreamingHandler diagnostic handler. This is for debugging purpose. The default value is . + /// + public bool EnableStreamingHubDiagnosticHandler { get; set; } = false; + + public string GenerateFileHintNamePrefix { get; set; } = string.Empty; + + public global::System.Type[] TypesContainedInTargetAssembly { get; } + + /// Types contained in the scan target assembly + public MagicOnionClientGenerationAttribute(params global::System.Type[] typesContainedInTargetAssembly) + { + TypesContainedInTargetAssembly = typesContainedInTargetAssembly; + } + + // This enum must be mirror of `SerializerType` (MagicOnionClientSourceGenerator) + internal enum GenerateSerializerType + { + MessagePack = 0, + MemoryPack = 1, + } + } +} \ No newline at end of file diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs new file mode 100644 index 000000000..5bbd9ab2b --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs @@ -0,0 +1,77 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block + +namespace TempProject +{ + using global::System; + using global::MessagePack; + + partial class MagicOnionInitializer + { + /// + /// Gets the generated MessagePack formatter resolver. + /// + public static global::MessagePack.IFormatterResolver Resolver => MessagePackGeneratedResolver.Instance; + class MessagePackGeneratedResolver : global::MessagePack.IFormatterResolver + { + public static readonly global::MessagePack.IFormatterResolver Instance = new MessagePackGeneratedResolver(); + + MessagePackGeneratedResolver() {} + + public global::MessagePack.Formatters.IMessagePackFormatter GetFormatter() + => FormatterCache.formatter; + + static class FormatterCache + { + public static readonly global::MessagePack.Formatters.IMessagePackFormatter formatter; + + static FormatterCache() + { + var f = MessagePackGeneratedGetFormatterHelper.GetFormatter(typeof(T)); + if (f != null) + { + formatter = (global::MessagePack.Formatters.IMessagePackFormatter)f; + } + } + } + } + static class MessagePackGeneratedGetFormatterHelper + { + static readonly global::System.Collections.Generic.Dictionary lookup; + + static MessagePackGeneratedGetFormatterHelper() + { + lookup = new global::System.Collections.Generic.Dictionary(0) + { + }; + } + internal static object GetFormatter(global::System.Type t) + { + int key; + if (!lookup.TryGetValue(t, out key)) + { + return null; + } + + switch (key) + { + default: return null; + } + } + } + /// Type hints for Ahead-of-Time compilation. + [Preserve] + static class TypeHints + { + [Preserve] + internal static void Register() + { + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0002_TempProject_MagicOnionInitializer.g.cs new file mode 100644 index 000000000..ec5b0b223 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0002_TempProject_MagicOnionInitializer.g.cs @@ -0,0 +1,95 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +namespace TempProject +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::MagicOnion; + using global::MagicOnion.Client; + + partial class PreserveAttribute : global::System.Attribute {} + + partial class MagicOnionInitializer + { + static bool isRegistered = false; + readonly static MagicOnionGeneratedClientFactoryProvider provider = new(); + + /// + /// Gets the generated MagicOnionClientFactoryProvider. + /// + public static global::MagicOnion.Client.IMagicOnionClientFactoryProvider ClientFactoryProvider => provider; + + /// + /// Gets the generated StreamingHubClientFactoryProvider. + /// + public static global::MagicOnion.Client.IStreamingHubClientFactoryProvider StreamingHubClientFactoryProvider => provider; +#if UNITY_2019_4_OR_NEWER + [global::UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] +#elif NET5_0_OR_GREATER + [global::System.Runtime.CompilerServices.ModuleInitializer] +#endif + internal static void Register() => TryRegisterProviderFactory(); + + /// + /// Register the generated client factory providers if it's not registered yet. This method will register only once. + /// + public static bool TryRegisterProviderFactory() + { + if (isRegistered) return false; + isRegistered = true; + + global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default = + (global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider immutableMagicOnionClientFactoryProvider) + ? immutableMagicOnionClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider(provider); + + global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default = + (global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider immutableStreamingHubClientFactoryProvider) + ? immutableStreamingHubClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider(provider); + + return true; + } + + class MagicOnionGeneratedClientFactoryProvider : global::MagicOnion.Client.IMagicOnionClientFactoryProvider, global::MagicOnion.Client.IStreamingHubClientFactoryProvider + { + bool global::MagicOnion.Client.IMagicOnionClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.MagicOnionClientFactoryDelegate factory) + => (factory = MagicOnionClientFactoryCache.Factory) != null; + + bool global::MagicOnion.Client.IStreamingHubClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.StreamingHubClientFactoryDelegate factory) + => (factory = StreamingHubClientFactoryCache.Factory) != null; + + static class MagicOnionClientFactoryCache where T : global::MagicOnion.IService + { + public readonly static global::MagicOnion.Client.MagicOnionClientFactoryDelegate Factory; + + static MagicOnionClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.MagicOnionClientFactoryDelegate); + + Factory = (global::MagicOnion.Client.MagicOnionClientFactoryDelegate)factory; + } + } + + static class StreamingHubClientFactoryCache where TStreamingHub : global::MagicOnion.IStreamingHub + { + public readonly static global::MagicOnion.Client.StreamingHubClientFactoryDelegate Factory; + + static StreamingHubClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.StreamingHubClientFactoryDelegate); + + if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) + { + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); + } + + Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; + } + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0003_TempProject_MyHubClient.g.cs new file mode 100644 index 000000000..4e019aed6 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero/0003_TempProject_MyHubClient.g.cs @@ -0,0 +1,79 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. + +namespace TempProject +{ + partial class MagicOnionInitializer + { + static partial class MagicOnionGeneratedClient + { + [global::MagicOnion.Ignore] + public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub + { + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) + { + } + + + public global::TempProject.IMyHub FireAndForget() + => new FireAndForgetClient(this); + + [global::MagicOnion.Ignore] + class FireAndForgetClient : global::TempProject.IMyHub + { + readonly TempProject_MyHubClient parent; + + public FireAndForgetClient(TempProject_MyHubClient parent) + => this.parent = parent; + + public global::TempProject.IMyHub FireAndForget() => this; + public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException(); + public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException(); + + + } + + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + try + { + switch (methodId) + { + case -1005848884: // Task A() + { + var value = base.Deserialize(data); + base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, receiver.A()); + } + break; + } + } + catch (global::System.Exception ex) + { + base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + } + } + + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0000_MagicOnionClientSourceGeneratorAttributes.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0000_MagicOnionClientSourceGeneratorAttributes.g.cs new file mode 100644 index 000000000..8faa1aa7e --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0000_MagicOnionClientSourceGeneratorAttributes.g.cs @@ -0,0 +1,49 @@ +// +namespace MagicOnion.Client +{ + /// + /// Marker attribute for generating clients of MagicOnion. + /// The source generator collects the classes specified by this attribute and uses them to generate source. + /// + [global::System.Diagnostics.Conditional("__MagicOnion_Client_SourceGenerator__DesignTimeOnly__")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)] + internal class MagicOnionClientGenerationAttribute : global::System.Attribute + { + /// + /// Gets or sets whether to disable automatically calling `Register` during start-up. (Automatic registration requires .NET 5+ or Unity) + /// + public bool DisableAutoRegistration { get; set; } + + /// + /// Gets or set the serializer used for message serialization. The default value is . + /// + public global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType Serializer { get; set; } = global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType.MessagePack; + + /// + /// Gets or set the namespace of pre-generated MessagePackFormatters. The default value is MessagePack.Formatters. + /// + public string MessagePackFormatterNamespace { get; set; } = "MessagePack.Formatters"; + + /// + /// Gets or set whether to enable the StreamingHandler diagnostic handler. This is for debugging purpose. The default value is . + /// + public bool EnableStreamingHubDiagnosticHandler { get; set; } = false; + + public string GenerateFileHintNamePrefix { get; set; } = string.Empty; + + public global::System.Type[] TypesContainedInTargetAssembly { get; } + + /// Types contained in the scan target assembly + public MagicOnionClientGenerationAttribute(params global::System.Type[] typesContainedInTargetAssembly) + { + TypesContainedInTargetAssembly = typesContainedInTargetAssembly; + } + + // This enum must be mirror of `SerializerType` (MagicOnionClientSourceGenerator) + internal enum GenerateSerializerType + { + MessagePack = 0, + MemoryPack = 1, + } + } +} \ No newline at end of file diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs new file mode 100644 index 000000000..653917804 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs @@ -0,0 +1,76 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block + +namespace TempProject +{ + using global::System; + using global::MessagePack; + + partial class MagicOnionInitializer + { + /// + /// Gets the generated MessagePack formatter resolver. + /// + public static global::MessagePack.IFormatterResolver Resolver => MessagePackGeneratedResolver.Instance; + class MessagePackGeneratedResolver : global::MessagePack.IFormatterResolver + { + public static readonly global::MessagePack.IFormatterResolver Instance = new MessagePackGeneratedResolver(); + + MessagePackGeneratedResolver() {} + + public global::MessagePack.Formatters.IMessagePackFormatter GetFormatter() + => FormatterCache.formatter; + + static class FormatterCache + { + public static readonly global::MessagePack.Formatters.IMessagePackFormatter formatter; + + static FormatterCache() + { + var f = MessagePackGeneratedGetFormatterHelper.GetFormatter(typeof(T)); + if (f != null) + { + formatter = (global::MessagePack.Formatters.IMessagePackFormatter)f; + } + } + } + } + static class MessagePackGeneratedGetFormatterHelper + { + static readonly global::System.Collections.Generic.Dictionary lookup; + + static MessagePackGeneratedGetFormatterHelper() + { + lookup = new global::System.Collections.Generic.Dictionary(0) + { + }; + } + internal static object GetFormatter(global::System.Type t) + { + int key; + if (!lookup.TryGetValue(t, out key)) + { + return null; + } + + switch (key) + { + default: return null; + } + } + } + /// Type hints for Ahead-of-Time compilation. + [Preserve] + static class TypeHints + { + [Preserve] + internal static void Register() + { + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0002_TempProject_MagicOnionInitializer.g.cs new file mode 100644 index 000000000..ec5b0b223 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0002_TempProject_MagicOnionInitializer.g.cs @@ -0,0 +1,95 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +namespace TempProject +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::MagicOnion; + using global::MagicOnion.Client; + + partial class PreserveAttribute : global::System.Attribute {} + + partial class MagicOnionInitializer + { + static bool isRegistered = false; + readonly static MagicOnionGeneratedClientFactoryProvider provider = new(); + + /// + /// Gets the generated MagicOnionClientFactoryProvider. + /// + public static global::MagicOnion.Client.IMagicOnionClientFactoryProvider ClientFactoryProvider => provider; + + /// + /// Gets the generated StreamingHubClientFactoryProvider. + /// + public static global::MagicOnion.Client.IStreamingHubClientFactoryProvider StreamingHubClientFactoryProvider => provider; +#if UNITY_2019_4_OR_NEWER + [global::UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] +#elif NET5_0_OR_GREATER + [global::System.Runtime.CompilerServices.ModuleInitializer] +#endif + internal static void Register() => TryRegisterProviderFactory(); + + /// + /// Register the generated client factory providers if it's not registered yet. This method will register only once. + /// + public static bool TryRegisterProviderFactory() + { + if (isRegistered) return false; + isRegistered = true; + + global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default = + (global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider immutableMagicOnionClientFactoryProvider) + ? immutableMagicOnionClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider(provider); + + global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default = + (global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider immutableStreamingHubClientFactoryProvider) + ? immutableStreamingHubClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider(provider); + + return true; + } + + class MagicOnionGeneratedClientFactoryProvider : global::MagicOnion.Client.IMagicOnionClientFactoryProvider, global::MagicOnion.Client.IStreamingHubClientFactoryProvider + { + bool global::MagicOnion.Client.IMagicOnionClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.MagicOnionClientFactoryDelegate factory) + => (factory = MagicOnionClientFactoryCache.Factory) != null; + + bool global::MagicOnion.Client.IStreamingHubClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.StreamingHubClientFactoryDelegate factory) + => (factory = StreamingHubClientFactoryCache.Factory) != null; + + static class MagicOnionClientFactoryCache where T : global::MagicOnion.IService + { + public readonly static global::MagicOnion.Client.MagicOnionClientFactoryDelegate Factory; + + static MagicOnionClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.MagicOnionClientFactoryDelegate); + + Factory = (global::MagicOnion.Client.MagicOnionClientFactoryDelegate)factory; + } + } + + static class StreamingHubClientFactoryCache where TStreamingHub : global::MagicOnion.IStreamingHub + { + public readonly static global::MagicOnion.Client.StreamingHubClientFactoryDelegate Factory; + + static StreamingHubClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.StreamingHubClientFactoryDelegate); + + if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) + { + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); + } + + Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; + } + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0003_TempProject_MyHubClient.g.cs new file mode 100644 index 000000000..1291c58a6 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_NoReturnValue/0003_TempProject_MyHubClient.g.cs @@ -0,0 +1,79 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. + +namespace TempProject +{ + partial class MagicOnionInitializer + { + static partial class MagicOnionGeneratedClient + { + [global::MagicOnion.Ignore] + public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub + { + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) + { + } + + + public global::TempProject.IMyHub FireAndForget() + => new FireAndForgetClient(this); + + [global::MagicOnion.Ignore] + class FireAndForgetClient : global::TempProject.IMyHub + { + readonly TempProject_MyHubClient parent; + + public FireAndForgetClient(TempProject_MyHubClient parent) + => this.parent = parent; + + public global::TempProject.IMyHub FireAndForget() => this; + public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException(); + public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException(); + + + } + + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + try + { + switch (methodId) + { + case -1005848884: // Task A() + { + var value = base.Deserialize(data); + base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, receiver.A()); + } + break; + } + } + catch (global::System.Exception ex) + { + base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + } + } + + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0000_MagicOnionClientSourceGeneratorAttributes.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0000_MagicOnionClientSourceGeneratorAttributes.g.cs new file mode 100644 index 000000000..8faa1aa7e --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0000_MagicOnionClientSourceGeneratorAttributes.g.cs @@ -0,0 +1,49 @@ +// +namespace MagicOnion.Client +{ + /// + /// Marker attribute for generating clients of MagicOnion. + /// The source generator collects the classes specified by this attribute and uses them to generate source. + /// + [global::System.Diagnostics.Conditional("__MagicOnion_Client_SourceGenerator__DesignTimeOnly__")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)] + internal class MagicOnionClientGenerationAttribute : global::System.Attribute + { + /// + /// Gets or sets whether to disable automatically calling `Register` during start-up. (Automatic registration requires .NET 5+ or Unity) + /// + public bool DisableAutoRegistration { get; set; } + + /// + /// Gets or set the serializer used for message serialization. The default value is . + /// + public global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType Serializer { get; set; } = global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType.MessagePack; + + /// + /// Gets or set the namespace of pre-generated MessagePackFormatters. The default value is MessagePack.Formatters. + /// + public string MessagePackFormatterNamespace { get; set; } = "MessagePack.Formatters"; + + /// + /// Gets or set whether to enable the StreamingHandler diagnostic handler. This is for debugging purpose. The default value is . + /// + public bool EnableStreamingHubDiagnosticHandler { get; set; } = false; + + public string GenerateFileHintNamePrefix { get; set; } = string.Empty; + + public global::System.Type[] TypesContainedInTargetAssembly { get; } + + /// Types contained in the scan target assembly + public MagicOnionClientGenerationAttribute(params global::System.Type[] typesContainedInTargetAssembly) + { + TypesContainedInTargetAssembly = typesContainedInTargetAssembly; + } + + // This enum must be mirror of `SerializerType` (MagicOnionClientSourceGenerator) + internal enum GenerateSerializerType + { + MessagePack = 0, + MemoryPack = 1, + } + } +} \ No newline at end of file diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs new file mode 100644 index 000000000..5bbd9ab2b --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs @@ -0,0 +1,77 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block + +namespace TempProject +{ + using global::System; + using global::MessagePack; + + partial class MagicOnionInitializer + { + /// + /// Gets the generated MessagePack formatter resolver. + /// + public static global::MessagePack.IFormatterResolver Resolver => MessagePackGeneratedResolver.Instance; + class MessagePackGeneratedResolver : global::MessagePack.IFormatterResolver + { + public static readonly global::MessagePack.IFormatterResolver Instance = new MessagePackGeneratedResolver(); + + MessagePackGeneratedResolver() {} + + public global::MessagePack.Formatters.IMessagePackFormatter GetFormatter() + => FormatterCache.formatter; + + static class FormatterCache + { + public static readonly global::MessagePack.Formatters.IMessagePackFormatter formatter; + + static FormatterCache() + { + var f = MessagePackGeneratedGetFormatterHelper.GetFormatter(typeof(T)); + if (f != null) + { + formatter = (global::MessagePack.Formatters.IMessagePackFormatter)f; + } + } + } + } + static class MessagePackGeneratedGetFormatterHelper + { + static readonly global::System.Collections.Generic.Dictionary lookup; + + static MessagePackGeneratedGetFormatterHelper() + { + lookup = new global::System.Collections.Generic.Dictionary(0) + { + }; + } + internal static object GetFormatter(global::System.Type t) + { + int key; + if (!lookup.TryGetValue(t, out key)) + { + return null; + } + + switch (key) + { + default: return null; + } + } + } + /// Type hints for Ahead-of-Time compilation. + [Preserve] + static class TypeHints + { + [Preserve] + internal static void Register() + { + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0002_TempProject_MagicOnionInitializer.g.cs new file mode 100644 index 000000000..ec5b0b223 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0002_TempProject_MagicOnionInitializer.g.cs @@ -0,0 +1,95 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +namespace TempProject +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::MagicOnion; + using global::MagicOnion.Client; + + partial class PreserveAttribute : global::System.Attribute {} + + partial class MagicOnionInitializer + { + static bool isRegistered = false; + readonly static MagicOnionGeneratedClientFactoryProvider provider = new(); + + /// + /// Gets the generated MagicOnionClientFactoryProvider. + /// + public static global::MagicOnion.Client.IMagicOnionClientFactoryProvider ClientFactoryProvider => provider; + + /// + /// Gets the generated StreamingHubClientFactoryProvider. + /// + public static global::MagicOnion.Client.IStreamingHubClientFactoryProvider StreamingHubClientFactoryProvider => provider; +#if UNITY_2019_4_OR_NEWER + [global::UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] +#elif NET5_0_OR_GREATER + [global::System.Runtime.CompilerServices.ModuleInitializer] +#endif + internal static void Register() => TryRegisterProviderFactory(); + + /// + /// Register the generated client factory providers if it's not registered yet. This method will register only once. + /// + public static bool TryRegisterProviderFactory() + { + if (isRegistered) return false; + isRegistered = true; + + global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default = + (global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider immutableMagicOnionClientFactoryProvider) + ? immutableMagicOnionClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider(provider); + + global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default = + (global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider immutableStreamingHubClientFactoryProvider) + ? immutableStreamingHubClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider(provider); + + return true; + } + + class MagicOnionGeneratedClientFactoryProvider : global::MagicOnion.Client.IMagicOnionClientFactoryProvider, global::MagicOnion.Client.IStreamingHubClientFactoryProvider + { + bool global::MagicOnion.Client.IMagicOnionClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.MagicOnionClientFactoryDelegate factory) + => (factory = MagicOnionClientFactoryCache.Factory) != null; + + bool global::MagicOnion.Client.IStreamingHubClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.StreamingHubClientFactoryDelegate factory) + => (factory = StreamingHubClientFactoryCache.Factory) != null; + + static class MagicOnionClientFactoryCache where T : global::MagicOnion.IService + { + public readonly static global::MagicOnion.Client.MagicOnionClientFactoryDelegate Factory; + + static MagicOnionClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.MagicOnionClientFactoryDelegate); + + Factory = (global::MagicOnion.Client.MagicOnionClientFactoryDelegate)factory; + } + } + + static class StreamingHubClientFactoryCache where TStreamingHub : global::MagicOnion.IStreamingHub + { + public readonly static global::MagicOnion.Client.StreamingHubClientFactoryDelegate Factory; + + static StreamingHubClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.StreamingHubClientFactoryDelegate); + + if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) + { + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); + } + + Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; + } + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0003_TempProject_MyHubClient.g.cs new file mode 100644 index 000000000..5971df598 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/ClientResult_Parameter_Zero_With_Cancellation/0003_TempProject_MyHubClient.g.cs @@ -0,0 +1,79 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. + +namespace TempProject +{ + partial class MagicOnionInitializer + { + static partial class MagicOnionGeneratedClient + { + [global::MagicOnion.Ignore] + public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub + { + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) + { + } + + + public global::TempProject.IMyHub FireAndForget() + => new FireAndForgetClient(this); + + [global::MagicOnion.Ignore] + class FireAndForgetClient : global::TempProject.IMyHub + { + readonly TempProject_MyHubClient parent; + + public FireAndForgetClient(TempProject_MyHubClient parent) + => this.parent = parent; + + public global::TempProject.IMyHub FireAndForget() => this; + public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException(); + public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException(); + + + } + + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + try + { + switch (methodId) + { + case -1005848884: // Task A(global::System.Threading.CancellationToken cancellationToken) + { + var value = base.Deserialize(data); + base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, receiver.A(default)); + } + break; + } + } + catch (global::System.Exception ex) + { + base.WriteClientResultResponseMessageForError(methodId, messageId, ex); + } + } + + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Complex/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Complex/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Complex/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Complex/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Complex/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Complex/0003_TempProject_MyHubClient.g.cs index cab696332..452016dfe 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Complex/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Complex/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,25 +15,25 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A() - => base.WriteMessageWithResponseAsync(-1005848884, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync(-1005848884, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task B(global::TempProject.MyObject a) - => base.WriteMessageWithResponseAsync(-955516027, a); + => this.WriteMessageWithResponseAsync(-955516027, a); public global::System.Threading.Tasks.Task C(global::TempProject.MyObject a, global::System.String b) - => base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-972293646, new global::MagicOnion.DynamicArgumentTuple(a, b)); + => this.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-972293646, new global::MagicOnion.DynamicArgumentTuple(a, b)); public global::System.Threading.Tasks.Task D(global::TempProject.MyObject a, global::System.String b, global::System.Int32 c) - => base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-1056181741, new global::MagicOnion.DynamicArgumentTuple(a, b, c)); + => this.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-1056181741, new global::MagicOnion.DynamicArgumentTuple(a, b, c)); public global::System.Threading.Tasks.Task E(global::TempProject.MyObject a, global::System.String b, global::System.Int32 c) - => base.WriteMessageWithResponseAsync, global::System.Int32>(-1072959360, new global::MagicOnion.DynamicArgumentTuple(a, b, c)); + => this.WriteMessageWithResponseAsync, global::System.Int32>(-1072959360, new global::MagicOnion.DynamicArgumentTuple(a, b, c)); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -58,13 +59,12 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { case -1262822265: // Void OnMessage() { - var value = base.Deserialize(data); receiver.OnMessage(); } break; @@ -83,7 +83,7 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -105,6 +105,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Many/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Many/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Many/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Many/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Many/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Many/0003_TempProject_MyHubClient.g.cs index f7a1ed9aa..b1e922667 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Many/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Many/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A(global::TempProject.MyObject a) - => base.WriteMessageWithResponseAsync(-1005848884, a); + => this.WriteMessageWithResponseAsync(-1005848884, a); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,7 +43,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -55,7 +56,7 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -65,6 +66,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_One/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_One/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_One/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_One/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_One/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_One/0003_TempProject_MyHubClient.g.cs index 684b0384f..684020a02 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_One/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_One/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A(global::TempProject.MyObject a) - => base.WriteMessageWithResponseAsync(-1005848884, a); + => this.WriteMessageWithResponseAsync(-1005848884, a); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,7 +43,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -55,7 +56,7 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -65,6 +66,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Zero/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Zero/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Zero/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Zero/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Zero/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Zero/0003_TempProject_MyHubClient.g.cs index 1c2df0ac4..4357607ad 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Zero/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/HubReceiver_Parameter_Zero/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A(global::TempProject.MyObject a) - => base.WriteMessageWithResponseAsync(-1005848884, a); + => this.WriteMessageWithResponseAsync(-1005848884, a); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,20 +43,19 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { case -1262822265: // Void OnMessage() { - var value = base.Deserialize(data); receiver.OnMessage(); } break; } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -65,6 +65,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance/0003_TempProject_MyHubClient.g.cs index 46290e96a..6647d8d9d 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,21 +15,21 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A() - => base.WriteMessageWithResponseAsync(-1005848884, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync(-1005848884, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task C() - => base.WriteMessageWithResponseAsync(-972293646, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync(-972293646, global::MessagePack.Nil.Default); public global::System.Threading.Tasks.Task B() - => base.WriteMessageWithResponseAsync(-955516027, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync(-955516027, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -50,14 +51,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -73,6 +74,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance_Receiver/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance_Receiver/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance_Receiver/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance_Receiver/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance_Receiver/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance_Receiver/0003_TempProject_MyHubClient.g.cs index 65a09a5b5..9acecb504 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance_Receiver/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/InterfaceInheritance_Receiver/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,15 +15,15 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -38,38 +39,38 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { case -1005848884: // Void A() { - var value = base.Deserialize(data); receiver.A(); } break; case -972293646: // Void C() { - var value = base.Deserialize(data); receiver.C(); } break; case -955516027: // Void B() { - var value = base.Deserialize(data); receiver.B(); } break; } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Invalid_HubReceiver_ReturnsNotVoid/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Invalid_HubReceiver_ReturnsNotVoid/0003_TempProject_MyHubClient.g.cs new file mode 100644 index 000000000..c742ade6f --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Invalid_HubReceiver_ReturnsNotVoid/0003_TempProject_MyHubClient.g.cs @@ -0,0 +1,80 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. + +namespace TempProject +{ + partial class MagicOnionInitializer + { + static partial class MagicOnionGeneratedClient + { + [global::MagicOnion.Ignore] + public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub + { + public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) + : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + { + } + + + public global::TempProject.IMyHub FireAndForget() + => new FireAndForgetClient(this); + + [global::MagicOnion.Ignore] + class FireAndForgetClient : global::TempProject.IMyHub + { + readonly TempProject_MyHubClient parent; + + public FireAndForgetClient(TempProject_MyHubClient parent) + => this.parent = parent; + + public global::TempProject.IMyHub FireAndForget() => this; + public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException(); + public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException(); + + + } + + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override async void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + case -955516027: // Task B() + { + try + { + var value = base.Deserialize(data); + var result = await receiver.B().ConfigureAwait(false); + await base.WriteClientResultResponseMessageAsync(methodId, messageId, result).ConfigureAwait(false); + } + catch (global::System.Exception ex) + { + await base.WriteClientResultResponseMessageForErrorAsync(methodId, messageId, ex).ConfigureAwait(false); + } + } + break; + } + } + + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Many/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Many/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Many/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Many/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Many/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Many/0003_TempProject_MyHubClient.g.cs index 7d74f14dd..a2018c27d 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Many/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Many/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A(global::System.String arg0, global::System.Int32 arg1, global::System.Boolean arg2) - => base.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-1005848884, new global::MagicOnion.DynamicArgumentTuple(arg0, arg1, arg2)); + => this.WriteMessageWithResponseAsync, global::MessagePack.Nil>(-1005848884, new global::MagicOnion.DynamicArgumentTuple(arg0, arg1, arg2)); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_One/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_One/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_One/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_One/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_One/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_One/0003_TempProject_MyHubClient.g.cs index 9e49654d4..eea526a3e 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_One/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_One/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A(global::System.String arg0) - => base.WriteMessageWithResponseAsync(-1005848884, arg0); + => this.WriteMessageWithResponseAsync(-1005848884, arg0); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Zero/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Zero/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Zero/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Zero/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Zero/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Zero/0003_TempProject_MyHubClient.g.cs index 2087d3a83..16ddcee32 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Zero/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Parameter_Zero/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A() - => base.WriteMessageWithResponseAsync(-1005848884, global::MessagePack.Nil.Default); + => this.WriteMessageWithResponseAsync(-1005848884, global::MessagePack.Nil.Default); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Task/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Task/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Task/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Task/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Task/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Task/0003_TempProject_MyHubClient.g.cs index f2908c80e..fc12d1403 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Task/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Task/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A(global::TempProject.MyObject a) - => base.WriteMessageWithResponseAsync(-1005848884, a); + => this.WriteMessageWithResponseAsync(-1005848884, a); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_TaskOfT/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_TaskOfT/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_TaskOfT/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_TaskOfT/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_TaskOfT/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_TaskOfT/0003_TempProject_MyHubClient.g.cs index d198eb5b2..a8c8c0506 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_TaskOfT/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_TaskOfT/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A(global::TempProject.MyObject a) - => base.WriteMessageWithResponseAsync(-1005848884, a); + => this.WriteMessageWithResponseAsync(-1005848884, a); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTask/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTask/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTask/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTask/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTask/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTask/0003_TempProject_MyHubClient.g.cs index 5bd7a300b..482358b1e 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTask/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTask/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.ValueTask A(global::TempProject.MyObject a) - => new global::System.Threading.Tasks.ValueTask(base.WriteMessageWithResponseAsync(-1005848884, a)); + => new global::System.Threading.Tasks.ValueTask(this.WriteMessageWithResponseAsync(-1005848884, a)); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTaskOfT/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTaskOfT/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTaskOfT/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTaskOfT/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTaskOfT/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTaskOfT/0003_TempProject_MyHubClient.g.cs index 33e4ab9e4..c39674a49 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTaskOfT/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_ValueTaskOfT/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.ValueTask A(global::TempProject.MyObject a) - => new global::System.Threading.Tasks.ValueTask(base.WriteMessageWithResponseAsync(-1005848884, a)); + => new global::System.Threading.Tasks.ValueTask(this.WriteMessageWithResponseAsync(-1005848884, a)); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,14 +43,14 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -59,6 +60,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0000_MagicOnionClientSourceGeneratorAttributes.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0000_MagicOnionClientSourceGeneratorAttributes.g.cs new file mode 100644 index 000000000..8faa1aa7e --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0000_MagicOnionClientSourceGeneratorAttributes.g.cs @@ -0,0 +1,49 @@ +// +namespace MagicOnion.Client +{ + /// + /// Marker attribute for generating clients of MagicOnion. + /// The source generator collects the classes specified by this attribute and uses them to generate source. + /// + [global::System.Diagnostics.Conditional("__MagicOnion_Client_SourceGenerator__DesignTimeOnly__")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)] + internal class MagicOnionClientGenerationAttribute : global::System.Attribute + { + /// + /// Gets or sets whether to disable automatically calling `Register` during start-up. (Automatic registration requires .NET 5+ or Unity) + /// + public bool DisableAutoRegistration { get; set; } + + /// + /// Gets or set the serializer used for message serialization. The default value is . + /// + public global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType Serializer { get; set; } = global::MagicOnion.Client.MagicOnionClientGenerationAttribute.GenerateSerializerType.MessagePack; + + /// + /// Gets or set the namespace of pre-generated MessagePackFormatters. The default value is MessagePack.Formatters. + /// + public string MessagePackFormatterNamespace { get; set; } = "MessagePack.Formatters"; + + /// + /// Gets or set whether to enable the StreamingHandler diagnostic handler. This is for debugging purpose. The default value is . + /// + public bool EnableStreamingHubDiagnosticHandler { get; set; } = false; + + public string GenerateFileHintNamePrefix { get; set; } = string.Empty; + + public global::System.Type[] TypesContainedInTargetAssembly { get; } + + /// Types contained in the scan target assembly + public MagicOnionClientGenerationAttribute(params global::System.Type[] typesContainedInTargetAssembly) + { + TypesContainedInTargetAssembly = typesContainedInTargetAssembly; + } + + // This enum must be mirror of `SerializerType` (MagicOnionClientSourceGenerator) + internal enum GenerateSerializerType + { + MessagePack = 0, + MemoryPack = 1, + } + } +} \ No newline at end of file diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs new file mode 100644 index 000000000..653917804 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0001_TempProject_MagicOnionInitializer_MessagePack.g.cs @@ -0,0 +1,76 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block + +namespace TempProject +{ + using global::System; + using global::MessagePack; + + partial class MagicOnionInitializer + { + /// + /// Gets the generated MessagePack formatter resolver. + /// + public static global::MessagePack.IFormatterResolver Resolver => MessagePackGeneratedResolver.Instance; + class MessagePackGeneratedResolver : global::MessagePack.IFormatterResolver + { + public static readonly global::MessagePack.IFormatterResolver Instance = new MessagePackGeneratedResolver(); + + MessagePackGeneratedResolver() {} + + public global::MessagePack.Formatters.IMessagePackFormatter GetFormatter() + => FormatterCache.formatter; + + static class FormatterCache + { + public static readonly global::MessagePack.Formatters.IMessagePackFormatter formatter; + + static FormatterCache() + { + var f = MessagePackGeneratedGetFormatterHelper.GetFormatter(typeof(T)); + if (f != null) + { + formatter = (global::MessagePack.Formatters.IMessagePackFormatter)f; + } + } + } + } + static class MessagePackGeneratedGetFormatterHelper + { + static readonly global::System.Collections.Generic.Dictionary lookup; + + static MessagePackGeneratedGetFormatterHelper() + { + lookup = new global::System.Collections.Generic.Dictionary(0) + { + }; + } + internal static object GetFormatter(global::System.Type t) + { + int key; + if (!lookup.TryGetValue(t, out key)) + { + return null; + } + + switch (key) + { + default: return null; + } + } + } + /// Type hints for Ahead-of-Time compilation. + [Preserve] + static class TypeHints + { + [Preserve] + internal static void Register() + { + _ = MessagePackGeneratedResolver.Instance.GetFormatter(); + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0002_TempProject_MagicOnionInitializer.g.cs new file mode 100644 index 000000000..ec5b0b223 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0002_TempProject_MagicOnionInitializer.g.cs @@ -0,0 +1,95 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS8019 // Unnecessary using directive. +namespace TempProject +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::MagicOnion; + using global::MagicOnion.Client; + + partial class PreserveAttribute : global::System.Attribute {} + + partial class MagicOnionInitializer + { + static bool isRegistered = false; + readonly static MagicOnionGeneratedClientFactoryProvider provider = new(); + + /// + /// Gets the generated MagicOnionClientFactoryProvider. + /// + public static global::MagicOnion.Client.IMagicOnionClientFactoryProvider ClientFactoryProvider => provider; + + /// + /// Gets the generated StreamingHubClientFactoryProvider. + /// + public static global::MagicOnion.Client.IStreamingHubClientFactoryProvider StreamingHubClientFactoryProvider => provider; +#if UNITY_2019_4_OR_NEWER + [global::UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] +#elif NET5_0_OR_GREATER + [global::System.Runtime.CompilerServices.ModuleInitializer] +#endif + internal static void Register() => TryRegisterProviderFactory(); + + /// + /// Register the generated client factory providers if it's not registered yet. This method will register only once. + /// + public static bool TryRegisterProviderFactory() + { + if (isRegistered) return false; + isRegistered = true; + + global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default = + (global::MagicOnion.Client.MagicOnionClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider immutableMagicOnionClientFactoryProvider) + ? immutableMagicOnionClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableMagicOnionClientFactoryProvider(provider); + + global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default = + (global::MagicOnion.Client.StreamingHubClientFactoryProvider.Default is global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider immutableStreamingHubClientFactoryProvider) + ? immutableStreamingHubClientFactoryProvider.Add(provider) + : new global::MagicOnion.Client.ImmutableStreamingHubClientFactoryProvider(provider); + + return true; + } + + class MagicOnionGeneratedClientFactoryProvider : global::MagicOnion.Client.IMagicOnionClientFactoryProvider, global::MagicOnion.Client.IStreamingHubClientFactoryProvider + { + bool global::MagicOnion.Client.IMagicOnionClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.MagicOnionClientFactoryDelegate factory) + => (factory = MagicOnionClientFactoryCache.Factory) != null; + + bool global::MagicOnion.Client.IStreamingHubClientFactoryProvider.TryGetFactory(out global::MagicOnion.Client.StreamingHubClientFactoryDelegate factory) + => (factory = StreamingHubClientFactoryCache.Factory) != null; + + static class MagicOnionClientFactoryCache where T : global::MagicOnion.IService + { + public readonly static global::MagicOnion.Client.MagicOnionClientFactoryDelegate Factory; + + static MagicOnionClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.MagicOnionClientFactoryDelegate); + + Factory = (global::MagicOnion.Client.MagicOnionClientFactoryDelegate)factory; + } + } + + static class StreamingHubClientFactoryCache where TStreamingHub : global::MagicOnion.IStreamingHub + { + public readonly static global::MagicOnion.Client.StreamingHubClientFactoryDelegate Factory; + + static StreamingHubClientFactoryCache() + { + object factory = default(global::MagicOnion.Client.StreamingHubClientFactoryDelegate); + + if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) + { + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); + } + + Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; + } + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0003_TempProject_MyHubClient.g.cs new file mode 100644 index 000000000..de516dd83 --- /dev/null +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateStreamingHubTest/Return_Void/0003_TempProject_MyHubClient.g.cs @@ -0,0 +1,69 @@ +// +#pragma warning disable CS0618 // 'member' is obsolete: 'text' +#pragma warning disable CS0612 // 'member' is obsolete +#pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used +#pragma warning disable CS8019 // Unnecessary using directive. +#pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. + +namespace TempProject +{ + partial class MagicOnionInitializer + { + static partial class MagicOnionGeneratedClient + { + [global::MagicOnion.Ignore] + public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub + { + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) + { + } + + public void A() + => this.WriteMessageFireAndForgetAsync(-1005848884, global::MessagePack.Nil.Default); + + public global::TempProject.IMyHub FireAndForget() + => new FireAndForgetClient(this); + + [global::MagicOnion.Ignore] + class FireAndForgetClient : global::TempProject.IMyHub + { + readonly TempProject_MyHubClient parent; + + public FireAndForgetClient(TempProject_MyHubClient parent) + => this.parent = parent; + + public global::TempProject.IMyHub FireAndForget() => this; + public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException(); + public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException(); + + public void A() + => parent.WriteMessageFireAndForgetAsync(-1005848884, global::MessagePack.Nil.Default); + + } + + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + } + } + + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) + { + switch (methodId) + { + case -1005848884: // Void A() + base.SetResultForResponse(taskCompletionSource, data); + break; + } + } + + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } + } + } + } +} diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateWithIfDirectiveTest/Skip_Generation_StreamingHub_Interface/0002_TempProject_MagicOnionInitializer.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateWithIfDirectiveTest/Skip_Generation_StreamingHub_Interface/0002_TempProject_MagicOnionInitializer.g.cs index 161a3a7c6..ec5b0b223 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateWithIfDirectiveTest/Skip_Generation_StreamingHub_Interface/0002_TempProject_MagicOnionInitializer.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateWithIfDirectiveTest/Skip_Generation_StreamingHub_Interface/0002_TempProject_MagicOnionInitializer.g.cs @@ -84,7 +84,7 @@ static StreamingHubClientFactoryCache() if (typeof(TStreamingHub) == typeof(global::TempProject.IMyHub) && typeof(TReceiver) == typeof(global::TempProject.IMyHubReceiver)) { - factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, _, b, c, d, e) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c, d, e))); + factory = ((global::MagicOnion.Client.StreamingHubClientFactoryDelegate)((a, b, c) => new MagicOnionGeneratedClient.TempProject_MyHubClient(a, b, c))); } Factory = (global::MagicOnion.Client.StreamingHubClientFactoryDelegate)factory; diff --git a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateWithIfDirectiveTest/Skip_Generation_StreamingHub_Interface/0003_TempProject_MyHubClient.g.cs b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateWithIfDirectiveTest/Skip_Generation_StreamingHub_Interface/0003_TempProject_MyHubClient.g.cs index ea8deb923..022f8c2bb 100644 --- a/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateWithIfDirectiveTest/Skip_Generation_StreamingHub_Interface/0003_TempProject_MyHubClient.g.cs +++ b/tests/MagicOnion.Client.SourceGenerator.Tests/Resources/GenerateWithIfDirectiveTest/Skip_Generation_StreamingHub_Interface/0003_TempProject_MyHubClient.g.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used #pragma warning disable CS8019 // Unnecessary using directive. #pragma warning disable CS1522 // Empty switch block +#pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously. namespace TempProject { @@ -14,17 +15,17 @@ static partial class MagicOnionGeneratedClient [global::MagicOnion.Ignore] public class TempProject_MyHubClient : global::MagicOnion.Client.StreamingHubClientBase, global::TempProject.IMyHub { - public TempProject_MyHubClient(global::Grpc.Core.CallInvoker callInvoker, global::System.String host, global::Grpc.Core.CallOptions options, global::MagicOnion.Serialization.IMagicOnionSerializerProvider serializerProvider, global::MagicOnion.Client.IMagicOnionClientLogger logger) - : base("IMyHub", callInvoker, host, options, serializerProvider, logger) + public TempProject_MyHubClient(global::TempProject.IMyHubReceiver receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options) + : base("IMyHub", receiver, callInvoker, options) { } public global::System.Threading.Tasks.Task A(global::System.Int32 a) - => base.WriteMessageWithResponseAsync(-1005848884, a); + => this.WriteMessageWithResponseAsync(-1005848884, a); public global::TempProject.IMyHub FireAndForget() => new FireAndForgetClient(this); - + [global::MagicOnion.Ignore] class FireAndForgetClient : global::TempProject.IMyHub { @@ -42,7 +43,7 @@ public FireAndForgetClient(TempProject_MyHubClient parent) } - protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ArraySegment data) + protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -55,7 +56,7 @@ protected override void OnBroadcastEvent(global::System.Int32 methodId, global:: } } - protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ArraySegment data) + protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskCompletionSource, global::System.ReadOnlyMemory data) { switch (methodId) { @@ -65,6 +66,9 @@ protected override void OnResponseEvent(global::System.Int32 methodId, global::S } } + protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory data) + { + } } } } diff --git a/tests/MagicOnion.Client.Tests/DynamicClient/SameInterfaceNameTest.cs b/tests/MagicOnion.Client.Tests/DynamicClient/SameInterfaceNameTest.cs index 2ff46a157..49a9a3f0d 100644 --- a/tests/MagicOnion.Client.Tests/DynamicClient/SameInterfaceNameTest.cs +++ b/tests/MagicOnion.Client.Tests/DynamicClient/SameInterfaceNameTest.cs @@ -22,11 +22,11 @@ public void DynamicStreamingHubClientFactoryProvider_TryGetFactory() DynamicStreamingHubClientFactoryProvider.Instance.TryGetFactory(out var factoryA); Assert.NotNull(factoryA); - var clientA = factoryA(callInvoker, receiverA, "", default, MagicOnionSerializerProvider.Default, NullMagicOnionClientLogger.Instance); + var clientA = factoryA(receiverA, callInvoker, new("", default, MagicOnionSerializerProvider.Default, NullMagicOnionClientLogger.Instance)); DynamicStreamingHubClientFactoryProvider.Instance.TryGetFactory(out var factoryB); Assert.NotNull(factoryB); - var clientB = factoryB(callInvoker, receiverB, "", default, MagicOnionSerializerProvider.Default, NullMagicOnionClientLogger.Instance); + var clientB = factoryB(receiverB, callInvoker, new ("", default, MagicOnionSerializerProvider.Default, NullMagicOnionClientLogger.Instance)); } } } diff --git a/tests/MagicOnion.Client.Tests/StreamingHubClientTestHelper.cs b/tests/MagicOnion.Client.Tests/StreamingHubClientTestHelper.cs index 347421f14..26d231f30 100644 --- a/tests/MagicOnion.Client.Tests/StreamingHubClientTestHelper.cs +++ b/tests/MagicOnion.Client.Tests/StreamingHubClientTestHelper.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Diagnostics; using System.Threading.Channels; +using MagicOnion.Internal; namespace MagicOnion.Client.Tests; @@ -8,8 +9,8 @@ class StreamingHubClientTestHelper where TStreamingHub : IStreamingHub where TReceiver : class { - readonly Channel requestChannel; - readonly Channel responseChannel; + readonly Channel requestChannel; + readonly Channel responseChannel; readonly CallInvoker callInvokerMock; readonly TReceiver receiver; readonly IStreamingHubClientFactoryProvider? factoryProvider; @@ -19,18 +20,18 @@ class StreamingHubClientTestHelper public StreamingHubClientTestHelper(IStreamingHubClientFactoryProvider? factoryProvider = null) { - requestChannel = Channel.CreateUnbounded(); - var requestStream = new ChannelClientStreamWriter(requestChannel); - responseChannel = Channel.CreateUnbounded(); - var responseStream = new ChannelAsyncStreamReader(responseChannel); + requestChannel = Channel.CreateUnbounded(); + var requestStream = new ChannelClientStreamWriter(requestChannel); + responseChannel = Channel.CreateUnbounded(); + var responseStream = new ChannelAsyncStreamReader(responseChannel); this.factoryProvider = factoryProvider; callInvokerMock = Substitute.For(); - callInvokerMock.AsyncDuplexStreamingCall(default(Method)!, default, default) + callInvokerMock.AsyncDuplexStreamingCall(default(Method)!, default, default) .ReturnsForAnyArgs(x => { - return new AsyncDuplexStreamingCall( + return new AsyncDuplexStreamingCall( requestStream, responseStream, _ => Task.FromResult(new Metadata { { "x-magiconion-streaminghub-version", "2" } }), @@ -59,16 +60,38 @@ public async Task ConnectAsync(CancellationToken cancellationToke ); } - public async Task<(int MessageId, int MethodId, T Requst)> ReadRequestAsync() + public async Task ConnectAsync(StreamingHubClientOptions options, CancellationToken cancellationToken = default) + { + return await StreamingHubClient.ConnectAsync( + callInvokerMock, + receiver, + options, + cancellationToken: cancellationToken, + factoryProvider: factoryProvider + ); + } + + public async Task> ReadRequestRawAsync() + { + var requestPayload = await requestChannel.Reader.ReadAsync(); + return requestPayload.Memory; + } + + public async Task<(int MessageId, int MethodId, T Request)> ReadRequestAsync() { var requestPayload = await requestChannel.Reader.ReadAsync(); - return ReadRequestPayload(requestPayload); + return ReadRequestPayload(requestPayload.Memory); } - public async Task<(int MethodId, T Requst)> ReadFireAndForgetRequestAsync() + public async Task<(int MethodId, T Request)> ReadFireAndForgetRequestAsync() { var requestPayload = await requestChannel.Reader.ReadAsync(); - return ReadFireAndForgetRequestPayload(requestPayload); + return ReadFireAndForgetRequestPayload(requestPayload.Memory); + } + + public void WriteResponseRaw(ReadOnlySpan data) + { + responseChannel.Writer.TryWrite(StreamingHubPayloadPool.Shared.RentOrCreate(data)); } public void WriteResponse(int messageId, int methodId, T response) @@ -76,7 +99,7 @@ public void WriteResponse(int messageId, int methodId, T response) responseChannel.Writer.TryWrite(BuildResponsePayload(messageId, methodId, response)); } - static byte[] BuildResponsePayload(int messageId, int methodId, T response) + static StreamingHubPayload BuildResponsePayload(int messageId, int methodId, T response) { var bufferWriter = new ArrayBufferWriter(); var messagePackWriter = new MessagePackWriter(bufferWriter); @@ -85,7 +108,7 @@ static byte[] BuildResponsePayload(int messageId, int methodId, T response) messagePackWriter.Write(methodId); MessagePackSerializer.Serialize(ref messagePackWriter, response); messagePackWriter.Flush(); - return bufferWriter.WrittenSpan.ToArray(); + return StreamingHubPayloadPool.Shared.RentOrCreate(bufferWriter.WrittenSpan); } static (int MessageId, int MethodId, T Body) ReadRequestPayload(ReadOnlyMemory payload) diff --git a/tests/MagicOnion.Client.Tests/StreamingHubTest.cs b/tests/MagicOnion.Client.Tests/StreamingHubTest.cs index 686141479..38aa490bb 100644 --- a/tests/MagicOnion.Client.Tests/StreamingHubTest.cs +++ b/tests/MagicOnion.Client.Tests/StreamingHubTest.cs @@ -303,7 +303,7 @@ public async Task ValueTask_Parameter_Many() Assert.Equal('X', requestBody.Item7); Assert.Equal(456, result); } - + [Fact] public async Task ValueTask_Forget_NoReturnValue_Parameter_Zero() { @@ -371,6 +371,135 @@ public async Task ValueTask_Forget_Parameter_One() Assert.Equal(123, requestBody); Assert.Equal(default, result); } + + [Fact] + public async Task Void_Parameter_Zero() + { + var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance); + var client = await helper.ConnectAsync(timeout.Token); + + // Use Fire-and-forget client + client = client.FireAndForget(); + + // Invoke Hub Method + client.Void_Parameter_Zero(); + + // Read a hub method request payload + var (methodId, requestBody) = await helper.ReadFireAndForgetRequestAsync(); + + Assert.Equal(Nil.Default, requestBody); + } + + [Fact] + public async Task Void_Parameter_One() + { + var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance); + var client = await helper.ConnectAsync(timeout.Token); + + // Use Fire-and-forget client + client = client.FireAndForget(); + + // Invoke Hub Method + client.Void_Parameter_One(123); + + // Read a hub method request payload + var (methodId, requestBody) = await helper.ReadFireAndForgetRequestAsync(); + + Assert.Equal(123, requestBody); + } + + [Fact] + public async Task Void_Parameter_Many() + { + var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance); + var client = await helper.ConnectAsync(timeout.Token); + + // Invoke Hub Method + client.Void_Parameter_Many(12345, "Hello", 0xfffffff1L, true, 128, 12.345, 'X'); + + // Read a hub method request payload + var (methodId, requestBody) = await helper.ReadFireAndForgetRequestAsync>(); + + Assert.Equal(12345, requestBody.Item1); + Assert.Equal("Hello", requestBody.Item2); + Assert.Equal(0xfffffff1L, requestBody.Item3); + Assert.True(requestBody.Item4); + Assert.Equal(128, requestBody.Item5); + Assert.Equal(12.345, requestBody.Item6); + Assert.Equal('X', requestBody.Item7); + } + + [Fact] + public async Task Heartbeat_Interval() + { + // Arrange + var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance); + var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatInterval(TimeSpan.FromMilliseconds(100)); + var client = await helper.ConnectAsync(options, timeout.Token); + + // Act + var t = client.Parameter_One(1234); + await Task.Delay(300); + + // Assert + var (messageId, methodId, requestBody) = await helper.ReadRequestAsync(); + Assert.Equal(1234, requestBody); + + var request1 = await helper.ReadRequestRawAsync(); + var request2 = await helper.ReadRequestRawAsync(); + var request3 = await helper.ReadRequestRawAsync(); + Assert.Equal([0x94, 0x7f, 0xc0, 0xc0, 0xc0], request1.ToArray()); + Assert.Equal([0x94, 0x7f, 0xc0, 0xc0, 0xc0], request2.ToArray()); + Assert.Equal([0x94, 0x7f, 0xc0, 0xc0, 0xc0], request3.ToArray()); + } + + [Fact] + public async Task Heartbeat_Respond() + { + // Arrange + var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance); + var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatInterval(Timeout.InfiniteTimeSpan); // Disable Heartbeat timer. + var client = await helper.ConnectAsync(options, timeout.Token); + + // Act + var t = client.Parameter_One(1234); + helper.WriteResponseRaw([0x95 /* Array(5) */, 0x7f /* Type:127 */, 0xc0, 0xc0, 0xc0, 0xc0 /* Extra */]); // Simulate heartbeat from the server. + await Task.Delay(100); + + // Assert + var (messageId, methodId, requestBody) = await helper.ReadRequestAsync(); + Assert.Equal(1234, requestBody); + + var request1 = await helper.ReadRequestRawAsync(); + Assert.Equal([0x94, 0x7f, 0xc0, 0xc0, 0xc0], request1.ToArray()); // Respond to the heartbeat from the server. + } + + [Fact] + public async Task Heartbeat_Extra() + { + // Arrange + var received = Array.Empty(); + var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance); + var options = StreamingHubClientOptions.CreateWithDefault() + .WithHeartbeatReceived(x => received = x.ToArray()) + .WithHeartbeatInterval(Timeout.InfiniteTimeSpan); // Disable Heartbeat timer. + var client = await helper.ConnectAsync(options, timeout.Token); + + // Act + helper.WriteResponseRaw([0x95 /* Array(5) */, 0x7f /* Type:127 */, 0xc0, 0xc0, 0xc0, .."Hello World"u8 /* Extra */]); // Simulate heartbeat from the server. + await Task.Delay(100); + + // Assert + Assert.Equal([.. "Hello World"u8], received); // Respond to the heartbeat from the server. + var request1 = await helper.ReadRequestRawAsync(); + Assert.Equal([0x94, 0x7f, 0xc0, 0xc0, 0xc0], request1.ToArray()); // Respond to the heartbeat from the server. + } } public interface IGreeterHubReceiver @@ -387,4 +516,7 @@ public interface IGreeterHub : IStreamingHub ValueTask ValueTask_Parameter_One(int arg0); ValueTask ValueTask_Parameter_Two(int arg0, string arg1); ValueTask ValueTask_Parameter_Many(int arg0, string arg1, long arg2, bool arg3, byte arg4, double arg5, char arg6); + void Void_Parameter_Zero(); + void Void_Parameter_One(int arg0); + void Void_Parameter_Many(int arg0, string arg1, long arg2, bool arg3, byte arg4, double arg5, char arg6); } diff --git a/tests/MagicOnion.Integration.Tests/HandCraftedStreamingHubClientTest.cs b/tests/MagicOnion.Integration.Tests/HandCraftedStreamingHubClientTest.cs index 6faf30602..1a36ab8ac 100644 --- a/tests/MagicOnion.Integration.Tests/HandCraftedStreamingHubClientTest.cs +++ b/tests/MagicOnion.Integration.Tests/HandCraftedStreamingHubClientTest.cs @@ -27,7 +27,7 @@ public async Task MethodParameterless() var client = new __HandCraftedClient__IHandCraftedStreamingHubClientTestHub(receiver, channel.CreateCallInvoker(), string.Empty, new CallOptions(), MagicOnionSerializerProvider.Default, NullMagicOnionClientLogger.Instance); // Act - await client.ConnectAsync(receiver); + await client.ConnectAsync(); var retVal = await client.MethodParameterless(); // Assert @@ -43,7 +43,7 @@ public async Task Callback() var client = new __HandCraftedClient__IHandCraftedStreamingHubClientTestHub(receiver, channel.CreateCallInvoker(), string.Empty, new CallOptions(), MagicOnionSerializerProvider.Default, NullMagicOnionClientLogger.Instance); // Act - await client.ConnectAsync(receiver); + await client.ConnectAsync(); var retVal = await client.Callback(1234, "FooBarBaz"); await Task.Delay(500); // Wait for the broadcast queue to be consumed. @@ -64,16 +64,16 @@ public void OnMessage(int arg0, string arg1) class __HandCraftedClient__IHandCraftedStreamingHubClientTestHub : StreamingHubClientBase, IHandCraftedStreamingHubClientTestHub { public __HandCraftedClient__IHandCraftedStreamingHubClientTestHub(IHandCraftedStreamingHubClientTestHubReceiver receiver, CallInvoker callInvoker, string host, CallOptions option, IMagicOnionSerializerProvider messageSerializer, IMagicOnionClientLogger logger) - : base(nameof(IHandCraftedStreamingHubClientTestHub), callInvoker, host, option, messageSerializer, logger) + : base(nameof(IHandCraftedStreamingHubClientTestHub), receiver, callInvoker, new StreamingHubClientOptions(host, option, messageSerializer, logger)) { } - public Task ConnectAsync(IHandCraftedStreamingHubClientTestHubReceiver receiver) + public Task ConnectAsync() { - return __ConnectAndSubscribeAsync(receiver, CancellationToken.None); + return __ConnectAndSubscribeAsync(CancellationToken.None); } - protected override void OnResponseEvent(int methodId, object taskCompletionSource, ArraySegment data) + protected override void OnResponseEvent(int methodId, object taskCompletionSource, ReadOnlyMemory data) { if (FNV1A32.GetHashCode(nameof(MethodParameterless)) == methodId) { @@ -85,7 +85,7 @@ protected override void OnResponseEvent(int methodId, object taskCompletionSourc } } - protected override void OnBroadcastEvent(int methodId, ArraySegment data) + protected override void OnBroadcastEvent(int methodId, ReadOnlyMemory data) { if (FNV1A32.GetHashCode(nameof(IHandCraftedStreamingHubClientTestHubReceiver.OnMessage)) == methodId) { @@ -94,6 +94,11 @@ protected override void OnBroadcastEvent(int methodId, ArraySegment data) } } + protected override void OnClientResultEvent(int methodId, Guid messageId, ReadOnlyMemory data) + { + throw new NotImplementedException(); + } + public IHandCraftedStreamingHubClientTestHub FireAndForget() { return new FireAndForgetClient(this); @@ -191,7 +196,7 @@ public class HandCraftedStreamingHubClientTestHub : StreamingHubBase Callback(int arg0, string arg1) { var group = await Group.AddAsync(Guid.NewGuid().ToString()); - Broadcast(group).OnMessage(arg0, arg1); + group.All.OnMessage(arg0, arg1); return 123; } } diff --git a/tests/MagicOnion.Integration.Tests/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Integration.Tests/MagicOnionApplicationFactory.cs index c87a06103..3ffa3d313 100644 --- a/tests/MagicOnion.Integration.Tests/MagicOnionApplicationFactory.cs +++ b/tests/MagicOnion.Integration.Tests/MagicOnionApplicationFactory.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using MagicOnion.Server; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; @@ -8,10 +9,14 @@ namespace MagicOnion.Integration.Tests; #pragma warning disable CS1998 public class MagicOnionApplicationFactory : WebApplicationFactory { + public const string ItemsKey = "MagicOnionApplicationFactory.Items"; + public ConcurrentDictionary Items => Services.GetRequiredKeyedService>(ItemsKey); + protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { + services.AddKeyedSingleton>(ItemsKey); services.AddMagicOnion(new[] { typeof(TServiceImplementation) }); }); } diff --git a/tests/MagicOnion.Integration.Tests/SerializerStreamingHubTest.cs b/tests/MagicOnion.Integration.Tests/SerializerStreamingHubTest.cs index 812c67473..197290ec0 100644 --- a/tests/MagicOnion.Integration.Tests/SerializerStreamingHubTest.cs +++ b/tests/MagicOnion.Integration.Tests/SerializerStreamingHubTest.cs @@ -123,7 +123,7 @@ public interface ISerializerTestHub : IStreamingHub, ISerializerTestHub { - IGroup? group; + IGroup? group; protected override async ValueTask OnConnecting() { @@ -137,7 +137,7 @@ protected override async ValueTask OnConnecting() public Task Callback(int arg0, string arg1) { - Broadcast(group!).OnMessage(arg0, arg1); + group!.All.OnMessage(arg0, arg1); return Task.FromResult(123); } } diff --git a/tests/MagicOnion.Integration.Tests/StreamingHubClientResultTest.cs b/tests/MagicOnion.Integration.Tests/StreamingHubClientResultTest.cs new file mode 100644 index 000000000..c715a588d --- /dev/null +++ b/tests/MagicOnion.Integration.Tests/StreamingHubClientResultTest.cs @@ -0,0 +1,590 @@ +using System.Collections.Concurrent; +using Grpc.Net.Client; +using MagicOnion.Client.DynamicClient; +using MagicOnion.Server.Hubs; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace MagicOnion.Integration.Tests; + +public class StreamingHubClientResultTest(MagicOnionApplicationFactory factory) : IClassFixture> +{ + readonly MagicOnionApplicationFactory factory = factory; + + public static IEnumerable EnumerateStreamingHubClientFactory() + { + yield return new[] { new TestStreamingHubClientFactory("Dynamic", DynamicStreamingHubClientFactoryProvider.Instance) }; + yield return new[] { new TestStreamingHubClientFactory("Static", MagicOnionGeneratedClientInitializer.StreamingHubClientFactoryProvider) }; + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Parameter_Zero_NoResultValue(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Parameter_Zero_NoResultValue(); + + // Assert + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Parameter_Zero_NoResultValue), (FakeStreamingHubClientResultTestHubReceiver.ArgumentEmpty)), receiver.Received[0]); + Assert.Equal((StreamingHubClientResultTestHub.Empty), serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Parameter_Zero_NoResultValue))); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Parameter_Zero(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Parameter_Zero(); + + // Assert + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Parameter_Zero), (FakeStreamingHubClientResultTestHubReceiver.ArgumentEmpty)), receiver.Received[0]); + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Parameter_Zero)), serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Parameter_Zero))); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Parameter_Many(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Parameter_Many(); + + // Assert + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Parameter_Many), ("Hello", 12345, true)), receiver.Received[0]); + Assert.Equal($"{nameof(IStreamingHubClientResultTestHubReceiver.Parameter_Many)}:Hello,12345,True", serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Parameter_Many))); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Parameter_Zero_With_Cancellation(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Parameter_Zero_With_Cancellation(); + + // Assert + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Parameter_Zero_With_Cancellation), (CancellationToken.None) /* Always None */), receiver.Received[0]); + Assert.Equal($"System.Threading.Tasks.TaskCanceledException", serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Parameter_Zero_With_Cancellation))); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Parameter_Zero_With_Cancellation_Optional(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Parameter_Zero_With_Cancellation_Optional(); + + // Assert + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Parameter_Zero_With_Cancellation_Optional), (CancellationToken.None) /* Always None */), receiver.Received[0]); + Assert.Equal($"System.Threading.Tasks.TaskCanceledException", serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Parameter_Zero_With_Cancellation_Optional))); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Parameter_One_With_Cancellation(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Parameter_One_With_Cancellation(); + + // Assert + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Parameter_One_With_Cancellation), ("Hello", CancellationToken.None) /* Always None */), receiver.Received[0]); + Assert.Equal($"System.Threading.Tasks.TaskCanceledException", serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Parameter_One_With_Cancellation))); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Parameter_One_With_Cancellation_Optional(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Parameter_One_With_Cancellation_Optional(); + + // Assert + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Parameter_One_With_Cancellation_Optional), ("Hello", CancellationToken.None) /* Always None */), receiver.Received[0]); + Assert.Equal($"System.Threading.Tasks.TaskCanceledException", serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Parameter_One_With_Cancellation_Optional))); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Parameter_Many_With_Cancellation(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Parameter_Many_With_Cancellation(); + + // Assert + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Parameter_Many_With_Cancellation), ("Hello", 12345, true, CancellationToken.None) /* Always None */), receiver.Received[0]); + Assert.Equal($"System.Threading.Tasks.TaskCanceledException", serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Parameter_Many_With_Cancellation))); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Parameter_Many_With_Cancellation_Optional(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Parameter_Many_With_Cancellation_Optional(); + + // Assert + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Parameter_Many_With_Cancellation_Optional), ("Hello", 12345, true, CancellationToken.None) /* Always None */), receiver.Received[0]); + Assert.Equal($"System.Threading.Tasks.TaskCanceledException", serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Parameter_Many_With_Cancellation_Optional))); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Throw(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Throw(); + + // Assert + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Throw), (FakeStreamingHubClientResultTestHubReceiver.ArgumentEmpty)), receiver.Received[0]); + Assert.Equal("Grpc.Core.RpcException", (((string,string))serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Throw))!).Item1); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Throw_With_StatusCode(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Throw_With_StatusCode(); + + // Assert + Assert.Equal((nameof(IStreamingHubClientResultTestHubReceiver.Throw_With_StatusCode), (FakeStreamingHubClientResultTestHubReceiver.ArgumentEmpty)), receiver.Received[0]); + Assert.Equal("Grpc.Core.RpcException", (((string, string))serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Throw_With_StatusCode))!).Item1); + Assert.Equal(StatusCode.Unauthenticated, ((StatusCode)serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Throw_With_StatusCode) + "/StatusCode")!)); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Invoke_After_Disconnected(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + var signalFromClient = new SemaphoreSlim(0); + var signalToClient = new SemaphoreSlim(0); + serverItems[nameof(Invoke_After_Disconnected) + "/Signal/FromClient"] = signalFromClient; + serverItems[nameof(Invoke_After_Disconnected) + "/Signal/ToClient"] = signalToClient; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + _ = client.Invoke_After_Disconnected(); + await Task.Delay(200); + await client.DisposeAsync(); + channel.Dispose(); + signalFromClient.Release(); + + // Wait for complete processing the request on the server. + await signalToClient.WaitAsync(); + + // Assert + //testOutputHelper.WriteLine(serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_After_Disconnected) + "/Exception") + ""); + Assert.Empty(receiver.Received); + Assert.Equal("System.Threading.Tasks.TaskCanceledException", (((string, string))serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_After_Disconnected))!).Item1); + } + + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task NotSingleTarget(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var serverItems = factory.Items; + + var receiver = new FakeStreamingHubClientResultTestHubReceiver(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.Invoke_Not_SingleTarget(); + + // Assert + Assert.Empty(receiver.Received); + Assert.Equal("System.NotSupportedException", ((string)serverItems.GetValueOrDefault(nameof(IStreamingHubClientResultTestHub.Invoke_Not_SingleTarget))!)); + } +} + +file class FakeStreamingHubClientResultTestHubReceiver : IStreamingHubClientResultTestHubReceiver +{ + public List<(string Method, object Arguments)> Received { get; } = new(); + + public static readonly object ArgumentEmpty = new(); + + public async Task Parameter_Zero_NoResultValue() + { + Received.Add((nameof(Parameter_Zero_NoResultValue), (ArgumentEmpty))); + await Task.Delay(10); + } + + public async Task Parameter_Zero() + { + Received.Add((nameof(Parameter_Zero), (ArgumentEmpty))); + await Task.Delay(10); + return $"{nameof(Parameter_Zero)}"; + } + + public async Task Parameter_One(string arg1) + { + Received.Add((nameof(Parameter_One), (arg1))); + await Task.Delay(10); + return $"{nameof(Parameter_One)}:{arg1}"; + } + + public async Task Parameter_Many(string arg1, int arg2, bool arg3) + { + Received.Add((nameof(Parameter_Many), (arg1, arg2, arg3))); + await Task.Delay(10); + return $"{nameof(Parameter_Many)}:{arg1},{arg2},{arg3}"; + } + + public Task Throw() + { + Received.Add((nameof(Throw), (ArgumentEmpty))); + throw new InvalidOperationException("Something went wrong."); + } + + public Task Throw_With_StatusCode() + { + Received.Add((nameof(Throw_With_StatusCode), (ArgumentEmpty))); + throw new RpcException(new Status(StatusCode.Unauthenticated, "Something went wrong.")); + } + + public async Task Parameter_Zero_With_Cancellation(CancellationToken cancellationToken) + { + Received.Add((nameof(Parameter_Zero_With_Cancellation), (cancellationToken))); + await Task.Delay(1000); + return $"{nameof(Parameter_Zero_With_Cancellation)}:{cancellationToken.CanBeCanceled}"; + } + + public async Task Parameter_Zero_With_Cancellation_Optional(CancellationToken cancellationToken = default) + { + Received.Add((nameof(Parameter_Zero_With_Cancellation_Optional), (cancellationToken))); + await Task.Delay(1000); + return $"{nameof(Parameter_Zero_With_Cancellation_Optional)}:{cancellationToken.CanBeCanceled}"; + } + + public async Task Parameter_One_With_Cancellation(string arg1, CancellationToken cancellationToken) + { + Received.Add((nameof(Parameter_One_With_Cancellation), (arg1, cancellationToken))); + await Task.Delay(1000); + return $"{nameof(Parameter_One_With_Cancellation)}:{arg1},{cancellationToken.CanBeCanceled}"; + } + + public async Task Parameter_One_With_Cancellation_Optional(string arg1, CancellationToken cancellationToken = default) + { + Received.Add((nameof(Parameter_One_With_Cancellation_Optional), (arg1, cancellationToken))); + await Task.Delay(1000); + return $"{nameof(Parameter_One_With_Cancellation_Optional)}:{arg1},{cancellationToken.CanBeCanceled}"; + } + + public async Task Parameter_Many_With_Cancellation(string arg1, int arg2, bool arg3, CancellationToken cancellationToken) + { + Received.Add((nameof(Parameter_Many_With_Cancellation), (arg1, arg2, arg3, cancellationToken))); + await Task.Delay(1000); + return $"{nameof(Parameter_Many_With_Cancellation)}:{arg1},{arg2},{arg3},{cancellationToken.CanBeCanceled}"; + } + + public async Task Parameter_Many_With_Cancellation_Optional(string arg1, int arg2, bool arg3, CancellationToken cancellationToken = default) + { + Received.Add((nameof(Parameter_Many_With_Cancellation_Optional), (arg1, arg2, arg3, cancellationToken))); + await Task.Delay(1000); + return $"{nameof(Parameter_Many_With_Cancellation_Optional)}:{arg1},{arg2},{arg3},{cancellationToken.CanBeCanceled}"; + } +} + + +public interface IStreamingHubClientResultTestHub : IStreamingHub +{ + Task Invoke_Parameter_Zero_NoResultValue(); + Task Invoke_Parameter_Zero(); + Task Invoke_Parameter_One(); + Task Invoke_Parameter_Many(); + Task Invoke_Throw(); + Task Invoke_Throw_With_StatusCode(); + Task Invoke_Parameter_Zero_With_Cancellation(); + Task Invoke_Parameter_Zero_With_Cancellation_Optional(); + Task Invoke_Parameter_One_With_Cancellation(); + Task Invoke_Parameter_One_With_Cancellation_Optional(); + Task Invoke_Parameter_Many_With_Cancellation(); + Task Invoke_Parameter_Many_With_Cancellation_Optional(); + Task Invoke_After_Disconnected(); + Task Invoke_Not_SingleTarget(); +} + +public interface IStreamingHubClientResultTestHubReceiver +{ + Task Parameter_Zero_NoResultValue(); + Task Parameter_Zero(); + Task Parameter_One(string arg1); + Task Parameter_Many(string arg1, int arg2, bool arg3); + Task Throw(); + Task Throw_With_StatusCode(); + Task Parameter_Zero_With_Cancellation(CancellationToken cancellationToken); + Task Parameter_Zero_With_Cancellation_Optional(CancellationToken cancellationToken = default); + Task Parameter_One_With_Cancellation(string arg1, CancellationToken cancellationToken); + Task Parameter_One_With_Cancellation_Optional(string arg1, CancellationToken cancellationToken = default); + Task Parameter_Many_With_Cancellation(string arg1, int arg2, bool arg3, CancellationToken cancellationToken); + Task Parameter_Many_With_Cancellation_Optional(string arg1, int arg2, bool arg3, CancellationToken cancellationToken = default); +} + +public class StreamingHubClientResultTestHub([FromKeyedServices(MagicOnionApplicationFactory.ItemsKey)]ConcurrentDictionary Items) + : StreamingHubBase, IStreamingHubClientResultTestHub +{ + public static readonly object Empty = new(); + + public async Task Invoke_Parameter_Zero_NoResultValue() + { + await Client.Parameter_Zero_NoResultValue(); + Items.TryAdd(nameof(Invoke_Parameter_Zero_NoResultValue), Empty); + } + + public async Task Invoke_Parameter_Zero() + { + var result = await Client.Parameter_Zero(); + Items.TryAdd(nameof(Invoke_Parameter_Zero), result); + } + + public async Task Invoke_Parameter_One() + { + var result = await Client.Parameter_One("Hello"); + Items.TryAdd(nameof(Invoke_Parameter_One), result); + } + + public async Task Invoke_Parameter_Many() + { + var result = await Client.Parameter_Many("Hello", 12345, true); + Items.TryAdd(nameof(Invoke_Parameter_Many), result); + } + + public async Task Invoke_Throw() + { + try + { + var result = await Client.Throw(); + } + catch (Exception e) + { + Items.TryAdd(nameof(Invoke_Throw), (e.GetType().FullName, e.Message)); + } + } + + public async Task Invoke_Throw_With_StatusCode() + { + try + { + var result = await Client.Throw_With_StatusCode(); + } + catch (RpcException e) + { + Items.TryAdd(nameof(Invoke_Throw_With_StatusCode), (e.GetType().FullName, e.Message)); + Items.TryAdd(nameof(Invoke_Throw_With_StatusCode) + "/StatusCode", e.StatusCode); + } + } + + public async Task Invoke_After_Disconnected() + { + try + { + await ((SemaphoreSlim)Items[nameof(Invoke_After_Disconnected) + "/Signal/FromClient"]).WaitAsync(); + //Context.CallContext.GetHttpContext().Abort(); + var result = await Client.Parameter_Zero(); + } + catch (Exception e) + { + Items.TryAdd(nameof(Invoke_After_Disconnected), (e.GetType().FullName, e.Message)); + Items.TryAdd(nameof(Invoke_After_Disconnected) + "/Exception", (e.ToString())); + } + finally + { + ((SemaphoreSlim)Items[nameof(Invoke_After_Disconnected) + "/Signal/ToClient"]).Release(); + } + } + + public async Task Invoke_Parameter_Zero_With_Cancellation() + { + var cts = new CancellationTokenSource(250); + try + { + var result = await Client.Parameter_Zero_With_Cancellation(cts.Token); + Items.TryAdd(nameof(Invoke_Parameter_Zero_With_Cancellation), (result)); + } + catch (Exception e) + { + Items.TryAdd(nameof(Invoke_Parameter_Zero_With_Cancellation), (e.GetType().FullName!)); + } + } + + public async Task Invoke_Parameter_Zero_With_Cancellation_Optional() + { + var cts = new CancellationTokenSource(250); + try + { + var result = await Client.Parameter_Zero_With_Cancellation_Optional(cts.Token); + Items.TryAdd(nameof(Invoke_Parameter_Zero_With_Cancellation), (result)); + } + catch (Exception e) + { + Items.TryAdd(nameof(Invoke_Parameter_Zero_With_Cancellation_Optional), (e.GetType().FullName!)); + } + } + + public async Task Invoke_Parameter_One_With_Cancellation() + { + var cts = new CancellationTokenSource(250); + try + { + var result = await Client.Parameter_One_With_Cancellation("Hello", cts.Token); + Items.TryAdd(nameof(Invoke_Parameter_One_With_Cancellation), (result)); + } + catch (Exception e) + { + Items.TryAdd(nameof(Invoke_Parameter_One_With_Cancellation), (e.GetType().FullName!)); + } + } + + public async Task Invoke_Parameter_One_With_Cancellation_Optional() + { + var cts = new CancellationTokenSource(250); + try + { + var result = await Client.Parameter_One_With_Cancellation_Optional("Hello", cts.Token); + Items.TryAdd(nameof(Invoke_Parameter_One_With_Cancellation_Optional), (result)); + } + catch (Exception e) + { + Items.TryAdd(nameof(Invoke_Parameter_One_With_Cancellation_Optional), (e.GetType().FullName!)); + } + } + + public async Task Invoke_Parameter_Many_With_Cancellation() + { + var cts = new CancellationTokenSource(250); + try + { + var result = await Client.Parameter_Many_With_Cancellation("Hello", 12345, true, cts.Token); + Items.TryAdd(nameof(Invoke_Parameter_Many_With_Cancellation), (result)); + } + catch (Exception e) + { + Items.TryAdd(nameof(Invoke_Parameter_Many_With_Cancellation), (e.GetType().FullName!)); + } + } + + public async Task Invoke_Parameter_Many_With_Cancellation_Optional() + { + var cts = new CancellationTokenSource(250); + try + { + var result = await Client.Parameter_Many_With_Cancellation_Optional("Hello", 12345, true, cts.Token); + Items.TryAdd(nameof(Invoke_Parameter_Many_With_Cancellation_Optional), (result)); + } + catch (Exception e) + { + Items.TryAdd(nameof(Invoke_Parameter_Many_With_Cancellation_Optional), (e.GetType().FullName!)); + } + } + + public async Task Invoke_Not_SingleTarget() + { + try + { + var group = await Group.AddAsync(nameof(Invoke_Not_SingleTarget) + Guid.NewGuid()); + + var result = await group.All.Parameter_Zero(); + Items.TryAdd(nameof(Invoke_Not_SingleTarget), (result)); + } + catch (Exception e) + { + Items.TryAdd(nameof(Invoke_Not_SingleTarget), (e.GetType().FullName!)); + } + } +} diff --git a/tests/MagicOnion.Integration.Tests/StreamingHubInterfaceInheritanceTest.cs b/tests/MagicOnion.Integration.Tests/StreamingHubInterfaceInheritanceTest.cs index 931667e13..740a75b58 100644 --- a/tests/MagicOnion.Integration.Tests/StreamingHubInterfaceInheritanceTest.cs +++ b/tests/MagicOnion.Integration.Tests/StreamingHubInterfaceInheritanceTest.cs @@ -45,7 +45,7 @@ public async Task InterfaceInheritance(TestStreamingHubClientFactory clientFacto public class StreamingHubInheritanceTestHub : StreamingHubBase, IStreamingHubInheritanceTestHub { - IGroup group = default!; + IGroup group = default!; protected override async ValueTask OnConnecting() { @@ -54,19 +54,19 @@ protected override async ValueTask OnConnecting() public Task MethodA() { - Broadcast(group).Receiver_MethodA(); + group.All.Receiver_MethodA(); return Task.CompletedTask; } public Task MethodB() { - Broadcast(group).Receiver_MethodB(); + group.All.Receiver_MethodB(); return Task.CompletedTask; } public Task MethodC() { - Broadcast(group).Receiver_MethodC(); + group.All.Receiver_MethodC(); return Task.CompletedTask; } } diff --git a/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs b/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs index 52deaf218..83b85d7e0 100644 --- a/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs +++ b/tests/MagicOnion.Integration.Tests/StreamingHubTest.cs @@ -2,9 +2,7 @@ using Grpc.Net.Client; using MagicOnion.Client; using MagicOnion.Client.DynamicClient; -using MagicOnion.Server; using MagicOnion.Server.Hubs; -using Microsoft.Extensions.DependencyInjection; namespace MagicOnion.Integration.Tests; @@ -485,29 +483,187 @@ public async Task Throw() ex.Status.Detail.Should().StartWith("An error occurred while processing handler"); } - [Fact] - public async Task Throw_WithServerStackTrace() + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Concurrency(TestStreamingHubClientFactory clientFactory) { // Arrange - var factory = this.factory.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.Configure(options => options.IsReturnExceptionStackTraceInErrorDetail = true))); - var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = factory.CreateDefaultClient() }); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var cts = new CancellationTokenSource(); + var tasks = Enumerable.Range(0, 10).Select(async x => + { + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var receiver = Substitute.For(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + await tcs.Task; + + var semaphore = new SemaphoreSlim(0); + var results = new List<(int Index, (int Arg0, string Arg1, bool Arg2) Request, (int Arg0, string Arg1, bool Arg2) Response)>(); + var receiverResults = new List<(int Arg0, string Arg1, bool Arg2)>(); + receiver.When(x => x.Receiver_Concurrent(Arg.Any(), Arg.Any(), Arg.Any())) + .Do(x => + { + receiverResults.Add((x.ArgAt(0), x.ArgAt(1), x.ArgAt(2))); + semaphore.Release(); + }); + + var count = 0; + while (!cts.IsCancellationRequested) + { + var response = await client.Concurrent((x * 100) + count, $"Task{x}-{count}", x % 2 == 0); + results.Add((Index: count, Request: ((x * 100) + count, $"Task{x}-{count}", x % 2 == 0), Response: response)); + await semaphore.WaitAsync(); + + count++; + } + + await Task.Delay(1000); + + return (Sequence: x, Results: results, ReceiverResults: receiverResults); + }); + + // Act + tcs.TrySetResult(); + cts.CancelAfter(TimeSpan.FromSeconds(5)); + var allResults = await Task.WhenAll(tasks); + + // Assert + Assert.All(allResults, x => + { + Assert.Equal(x.Results.Count, x.ReceiverResults.Count); + Assert.All(x.Results, y => Assert.Equal((y.Request.Arg0 * 100, y.Request.Arg1 + "-Result", !y.Request.Arg2), y.Response)); + for (var i = 0; i < x.ReceiverResults.Count; i++) + { + var request = x.Results[i]; + Assert.Equal((request.Request.Arg0 * 10, request.Request.Arg1 + "-Receiver", !request.Request.Arg2), x.ReceiverResults[i]); + } + }); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Void_Parameter_Zero(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + var receiver = Substitute.For(); - var client = await StreamingHubClient.ConnectAsync(channel, receiver); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); // Act - var ex = (RpcException?)await Record.ExceptionAsync(async () => await client.Throw()); + client.Void_Parameter_Zero(); + await Task.Delay(500); // Wait for broadcast queue to be consumed. // Assert - ex.Should().NotBeNull(); - ex!.StatusCode.Should().Be(StatusCode.Internal); - ex.Message.Should().Contain("Something went wrong."); - ex.Status.Detail.Should().StartWith("An error occurred while processing handler"); + receiver.Received().Receiver_Test_Void_Parameter_Zero(); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Void_Parameter_One(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = Substitute.For(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + client.Void_Parameter_One(12345); + await Task.Delay(500); // Wait for broadcast queue to be consumed. + + // Assert + receiver.Received().Receiver_Test_Void_Parameter_One(12345); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Void_Parameter_Many(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = Substitute.For(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + client.Void_Parameter_Many(12345, "Hello✨", true); + await Task.Delay(500); // Wait for broadcast queue to be consumed. + + // Assert + receiver.Received().Receiver_Test_Void_Parameter_Many(12345, "Hello✨", true); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task Process_Requests_Sequentially(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = Substitute.For(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + var task1 = client.Delay(1, TimeSpan.FromSeconds(1.5)); + var task2 = client.Delay(2, TimeSpan.FromSeconds(1)); + var task3 = client.Delay(3, TimeSpan.FromSeconds(0.5)); + + // Assert + Assert.Equal(1, await task1); + Assert.False(task2.IsCompleted); + Assert.False(task3.IsCompleted); + + Assert.Equal(2, await task2); + Assert.False(task3.IsCompleted); + + Assert.Equal(3, await task3); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task CustomMethodId(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = Substitute.For(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act & Assert + await client.CustomMethodId(); + } + + [Theory] + [MemberData(nameof(EnumerateStreamingHubClientFactory))] + public async Task CustomMethodId_Receiver(TestStreamingHubClientFactory clientFactory) + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient }); + + var receiver = Substitute.For(); + var client = await clientFactory.CreateAndConnectAsync(channel, receiver); + + // Act + await client.CallReceiver_CustomMethodId(); + await Task.Delay(500); // Wait for broadcast queue to be consumed. + + // Assert + receiver.Received().Receiver_CustomMethodId(); } } public class StreamingHubTestHub : StreamingHubBase, IStreamingHubTestHub { - IGroup group = default!; + IGroup group = default!; protected override async ValueTask OnConnecting() { @@ -554,19 +710,19 @@ public Task Parameter_Many(int arg0, string arg1, bool arg2) public Task CallReceiver_Parameter_Zero() { - Broadcast(group).Receiver_Parameter_Zero(); + group.All.Receiver_Parameter_Zero(); return Task.CompletedTask; } public Task CallReceiver_Parameter_One(int arg0) { - Broadcast(group).Receiver_Parameter_One(12345); + group.All.Receiver_Parameter_One(12345); return Task.CompletedTask; } public Task CallReceiver_Parameter_Many(int arg0, string arg1, bool arg2) { - Broadcast(group).Receiver_Parameter_Many(12345, "Hello✨", true); + group.All.Receiver_Parameter_Many(12345, "Hello✨", true); return Task.CompletedTask; } @@ -628,6 +784,25 @@ public ValueTask ValueTask_Never_With_Return() return new ValueTask(new TaskCompletionSource().Task.WaitAsync(TimeSpan.FromMilliseconds(100))); } + public void Void_Parameter_Zero() + { + group.All.Receiver_Test_Void_Parameter_Zero(); + } + + public void Void_Parameter_One(int arg0) + { + Debug.Assert(arg0 == 12345); + group.All.Receiver_Test_Void_Parameter_One(arg0); + } + + public void Void_Parameter_Many(int arg0, string arg1, bool arg2) + { + Debug.Assert(arg0 == 12345); + Debug.Assert(arg1 == "Hello✨"); + Debug.Assert(arg2 == true); + group.All.Receiver_Test_Void_Parameter_Many(arg0, arg1, arg2); + } + public Task RefType(MyStreamingRequest request) { return Task.FromResult(new MyStreamingResponse(request.Argument0 + request.Argument1)); @@ -641,13 +816,13 @@ public Task RefType(MyStreamingRequest request) public Task CallReceiver_RefType(MyStreamingRequest request) { - Broadcast(group).Receiver_RefType(new MyStreamingResponse(request.Argument0 + request.Argument1)); + group.All.Receiver_RefType(new MyStreamingResponse(request.Argument0 + request.Argument1)); return Task.CompletedTask; } public Task CallReceiver_RefType_Null() { - Broadcast(group).Receiver_RefType_Null(default); + group.All.Receiver_RefType_Null(default); return Task.CompletedTask; } @@ -656,12 +831,18 @@ public Task CallReceiver_Delay(int milliseconds) _ = Task.Run(async () => { await Task.Delay(milliseconds); - Broadcast(group).Receiver_Delay(); + group.All.Receiver_Delay(); }); return Task.CompletedTask; } + public Task CallReceiver_CustomMethodId() + { + group.All.Receiver_CustomMethodId(); + return Task.CompletedTask; + } + public Task ThrowReturnStatusException() { throw new ReturnStatusException(StatusCode.Unknown, "Detail-String"); @@ -671,6 +852,24 @@ public Task Throw() { throw new InvalidOperationException("Something went wrong."); } + + public async Task<(int, string, bool)> Concurrent(int arg0, string arg1, bool arg2) + { + group.All.Receiver_Concurrent(arg0 * 10, arg1 + "-Receiver", !arg2); + await Task.Yield(); + return (arg0 * 100, arg1 + "-Result", !arg2); + } + + public async Task Delay(int id, TimeSpan delay) + { + await Task.Delay(delay); + return id; + } + + public Task CustomMethodId() + { + return Task.CompletedTask; + } } public interface IStreamingHubTestHubReceiver @@ -681,6 +880,14 @@ public interface IStreamingHubTestHubReceiver void Receiver_RefType(MyStreamingResponse request); void Receiver_RefType_Null(MyStreamingResponse? request); void Receiver_Delay(); + void Receiver_Concurrent(int arg0, string arg1, bool arg2); + + void Receiver_Test_Void_Parameter_Zero(); + void Receiver_Test_Void_Parameter_One(int arg0); + void Receiver_Test_Void_Parameter_Many(int arg0, string arg1, bool arg2); + + [MethodId(54321)] + void Receiver_CustomMethodId(); } public interface IStreamingHubTestHub : IStreamingHub @@ -711,6 +918,10 @@ public interface IStreamingHubTestHub : IStreamingHub ValueTask_Never_With_Return(); + void Void_Parameter_Zero(); + void Void_Parameter_One(int arg0); + void Void_Parameter_Many(int arg0, string arg1, bool arg2); + Task RefType(MyStreamingRequest request); Task RefType_Null(MyStreamingRequest? request); Task CallReceiver_RefType(MyStreamingRequest request); @@ -718,6 +929,15 @@ public interface IStreamingHubTestHub : IStreamingHub Concurrent(int arg0, string arg1, bool arg2); + + Task Delay(int id, TimeSpan delay); + + [MethodId(12345)] + Task CustomMethodId(); } diff --git a/tests/MagicOnion.Serialization.MemoryPack.Tests/MemoryPackSerializerStreamingHubTest.cs b/tests/MagicOnion.Serialization.MemoryPack.Tests/MemoryPackSerializerStreamingHubTest.cs index fba874835..b1c5a5287 100644 --- a/tests/MagicOnion.Serialization.MemoryPack.Tests/MemoryPackSerializerStreamingHubTest.cs +++ b/tests/MagicOnion.Serialization.MemoryPack.Tests/MemoryPackSerializerStreamingHubTest.cs @@ -233,7 +233,7 @@ public interface IMemoryPackSerializerTestHub : IStreamingHub, IMemoryPackSerializerTestHub { - IGroup? group; + IGroup? group; protected override async ValueTask OnConnecting() { @@ -249,13 +249,13 @@ protected override async ValueTask OnConnecting() public Task Callback(int arg0, string arg1) { - Broadcast(group!).OnMessage(arg0, arg1); + group!.All.OnMessage(arg0, arg1); return Task.FromResult(123); } public Task CallbackCustomObject(MyRequestResponse arg0) { - Broadcast(group!).OnMessageCustomObject(new MyRequestResponse() { Item1 = arg0.Item1, Item2 = arg0.Item2 }); + group!.All.OnMessageCustomObject(new MyRequestResponse() { Item1 = arg0.Item1, Item2 = arg0.Item2 }); return Task.FromResult(123); } diff --git a/tests/MagicOnion.Server.Redis.Tests/RedisGroupFunctionalTest.cs b/tests/MagicOnion.Server.Redis.Tests/RedisGroupFunctionalTest.cs index 84e0e4d3d..f734a5ca7 100644 --- a/tests/MagicOnion.Server.Redis.Tests/RedisGroupFunctionalTest.cs +++ b/tests/MagicOnion.Server.Redis.Tests/RedisGroupFunctionalTest.cs @@ -1,3 +1,5 @@ +using Cysharp.Runtime.Multicast; +using Cysharp.Runtime.Multicast.Distributed.Redis; using MagicOnion.Server.Hubs; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -21,8 +23,8 @@ public RedisGroupFunctionalTest(MagicOnionApplicationFactory { - services.RemoveAll(); - services.TryAddSingleton(); + services.RemoveAll(); + services.TryAddSingleton(); services.Configure(options => { options.ConnectionMultiplexer = StackExchange.Redis.ConnectionMultiplexer.Connect(redisServer.GetConnectionString()); @@ -33,8 +35,8 @@ public RedisGroupFunctionalTest(MagicOnionApplicationFactory { - services.RemoveAll(); - services.TryAddSingleton(); + services.RemoveAll(); + services.TryAddSingleton(); services.Configure(options => { options.ConnectionMultiplexer = StackExchange.Redis.ConnectionMultiplexer.Connect(redisServer.GetConnectionString()); @@ -70,34 +72,6 @@ public async Task Broadcast() receiver2.Received().OnMessage(12345); } - [Fact] - public async Task RemoveMemberFromInMemoryGroup_CounterKeyIsNotDeleted() - { - // Arrange - var groupName = nameof(RemoveMemberFromInMemoryGroup_CounterKeyIsNotDeleted); - // Client-1 on Server-1 - var httpClient1 = factory.CreateDefaultClient(); - var channel1 = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient1 }); - var receiver1 = Substitute.For(); - var client1 = await StreamingHubClient.ConnectAsync(channel1, receiver1); - await client1.JoinAsync(groupName); - // Client-2 on Server-2 - var httpClient2 = factory2.CreateDefaultClient(); - var channel2 = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions() { HttpClient = httpClient2 }); - var receiver2 = Substitute.For(); - var client2 = await StreamingHubClient.ConnectAsync(channel2, receiver2); - await client2.JoinAsync(groupName); - - // Act - var beforeCount = (await client1.GetMemberCountAsync()); - await client2.LeaveAsync(); // Leave Client-2 from the group. - await Task.Delay(500); // Wait for broadcast queue to be consumed. - - // Assert - beforeCount.Should().Be(2); - (await client1.GetMemberCountAsync()).Should().Be(1); - } - [Fact] public async Task RemoveMemberFromInMemoryGroup_KeepSubscription() { @@ -142,13 +116,11 @@ public interface IRedisGroupFunctionalTestHub : IStreamingHub GetMemberCountAsync(); } public class RedisGroupFunctionalTestHub : StreamingHubBase, IRedisGroupFunctionalTestHub { - IGroup? group; + IGroup? group; public async Task JoinAsync(string groupName) { @@ -163,13 +135,7 @@ public async Task LeaveAsync() public Task CallAsync(int arg0) { - Broadcast(group!).OnMessage(arg0); + group!.All.OnMessage(arg0); return Task.CompletedTask; } - - public async Task GetMemberCountAsync() - { - if (group is null) return 0; - return await group.GetMemberCountAsync(); - } } diff --git a/tests/MagicOnion.Server.Tests/FakeStreamingServiceContext.cs b/tests/MagicOnion.Server.Tests/FakeStreamingServiceContext.cs index 654f5ec66..3695710ae 100644 --- a/tests/MagicOnion.Server.Tests/FakeStreamingServiceContext.cs +++ b/tests/MagicOnion.Server.Tests/FakeStreamingServiceContext.cs @@ -2,6 +2,7 @@ using System.Reflection; using Grpc.Core; using MagicOnion.Serialization; +using NSubstitute; namespace MagicOnion.Server.Tests; @@ -10,13 +11,13 @@ class FakeStreamingServiceContext : IStreamingServiceContex public bool IsStreamingHubCompleted { get; private set; } public List Responses { get; } = new List(); - public Guid ContextId => Guid.Empty; + public Guid ContextId { get; } = Guid.NewGuid(); public DateTime Timestamp => DateTime.UnixEpoch; public Type ServiceType { get; } public MethodInfo MethodInfo { get; } public ILookup AttributeLookup { get; } public MethodType MethodType => MethodType.DuplexStreaming; - public ServerCallContext CallContext => throw new NotImplementedException(); + public ServerCallContext CallContext { get; } public IMagicOnionSerializer MessageSerializer { get; } public IServiceProvider ServiceProvider { get; } public ConcurrentDictionary Items { get; } = new ConcurrentDictionary(); @@ -30,6 +31,10 @@ public FakeStreamingServiceContext(Type serviceType, MethodInfo methodInfo, IMag ServiceProvider = serviceProvider; AttributeLookup = attributeLookup ?? (new (Type, Attribute)[0]).ToLookup(k => k.Item1, v => v.Item2); + + var callContext = Substitute.For(); + callContext.Method.Returns(methodInfo.Name); + CallContext = callContext; } diff --git a/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj b/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj index 60d5a2cc5..5bdca88a7 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj +++ b/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -18,6 +18,7 @@ + diff --git a/tests/MagicOnion.Server.Tests/MagicOnionEngineTest.cs b/tests/MagicOnion.Server.Tests/MagicOnionEngineTest.cs index beaf1ca73..8118e8058 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnionEngineTest.cs +++ b/tests/MagicOnion.Server.Tests/MagicOnionEngineTest.cs @@ -3,6 +3,7 @@ using MagicOnion.Server.Hubs; using MagicOnionEngineTest; using MessagePack; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace MagicOnion.Server.Tests; @@ -14,7 +15,8 @@ public void CollectFromTypes_Empty() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var types = new Type[]{}; var options = new MagicOnionOptions(); @@ -32,7 +34,8 @@ public void CollectFromTypes_NonService() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var types = new Type[]{ typeof(NonService) }; var options = new MagicOnionOptions(); @@ -50,7 +53,8 @@ public void CollectFromTypes_Service() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var types = new Type[]{ typeof(MyService) }; var options = new MagicOnionOptions(); @@ -68,8 +72,8 @@ public void CollectFromTypes_StreamingHub() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var types = new Type[]{ typeof(MyHub) }; var options = new MagicOnionOptions(); @@ -87,8 +91,8 @@ public void CollectFromAssembly() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var assemblies = new Assembly[]{ typeof(IMyService).Assembly }; var options = new MagicOnionOptions(); @@ -106,8 +110,8 @@ public void CollectFromAssembly_Ignore() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var assemblies = new Assembly[]{ typeof(IMyIgnoredService).Assembly }; var options = new MagicOnionOptions(); @@ -125,8 +129,8 @@ public void CollectFromAssembly_NonPublic() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var assemblies = new Assembly[]{ typeof(IMyNonPublicService).Assembly }; var options = new MagicOnionOptions(); @@ -144,8 +148,8 @@ public void CollectFromAssembly_Abstract() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var assemblies = new Assembly[]{ typeof(IMyAbstractService).Assembly }; var options = new MagicOnionOptions(); @@ -163,8 +167,8 @@ public void CollectFromAssembly_Generic_Constructed() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var assemblies = new Assembly[]{ typeof(IMyGenericsService).Assembly }; var options = new MagicOnionOptions(); @@ -182,8 +186,8 @@ public void CollectFromAssembly_Generic_Definitions() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var assemblies = new Assembly[]{ typeof(IMyGenericsService).Assembly }; var options = new MagicOnionOptions(); @@ -203,8 +207,8 @@ public void CollectHandlers_Duplicated_Service() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var types = new Type[]{ typeof(MyService), typeof(MyService) }; var options = new MagicOnionOptions(); @@ -222,8 +226,8 @@ public void CollectHandlers_Duplicated_Hub() { // Arrange var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(); + services.AddSingleton(new ConfigurationManager()); + services.AddMagicOnionCore(); var serviceProvider = services.BuildServiceProvider(); var types = new Type[]{ typeof(MyHub), typeof(MyHub) }; var options = new MagicOnionOptions(); diff --git a/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/BroadcastGroupTestHub.cs b/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/BroadcastGroupTestHub.cs index c759401ec..fc048266d 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/BroadcastGroupTestHub.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/BroadcastGroupTestHub.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1998 +using Cysharp.Runtime.Multicast; using MagicOnion.Server.Hubs; namespace MagicOnion.Server.Tests.StreamingHubBroadcastTest; @@ -33,7 +34,7 @@ public interface IStreamingHubBroadcastTestHub : IStreamingHub, IStreamingHubBroadcastTestHub { - IGroup group; + IGroup group; public async Task RegisterConnectionToGroup() { @@ -43,31 +44,31 @@ public async Task RegisterConnectionToGroup() public async Task CallBroadcastToSelfAsync() { - BroadcastToSelf(group).Call(); + group.Single(ConnectionId).Call(); } public async Task CallBroadcastExceptSelfAsync() { - BroadcastExceptSelf(group).Call(); + group.Except([ConnectionId]).Call(); } public async Task CallBroadcastExceptAsync(Guid connectionId) { - BroadcastExcept(group, connectionId).Call(); + group.Except([connectionId]).Call(); } public async Task CallBroadcastExceptManyAsync(Guid[] connectionIds) { - BroadcastExcept(group, connectionIds).Call(); + group.Except([..connectionIds]).Call(); } public async Task CallBroadcastToAsync(Guid connectionId) { - BroadcastTo(group, connectionId).Call(); + group.Only([connectionId]).Call(); } public async Task CallBroadcastToManyAsync(Guid[] connectionIds) { - BroadcastTo(group, connectionIds).Call(); + group.Only([..connectionIds]).Call(); } } diff --git a/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/ConcurrentDictionaryGroupTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/ConcurrentDictionaryGroupTest.cs deleted file mode 100644 index 4539d0307..000000000 --- a/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/ConcurrentDictionaryGroupTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MagicOnion.Server.Tests.StreamingHubBroadcastTest; - -[CollectionDefinition(nameof(StreamingHubBroadcastConcurrentDictionaryGroupTestGrpcServerFixture))] -public class StreamingHubBroadcastConcurrentDictionaryGroupTestGrpcServerFixture : ICollectionFixture -{ - public class CustomServerFixture : ServerFixture - { - } -} - -[Collection(nameof(StreamingHubBroadcastConcurrentDictionaryGroupTestGrpcServerFixture))] -public class ConcurrentDictionaryGroupTest : GroupTestBase -{ - public ConcurrentDictionaryGroupTest(StreamingHubBroadcastConcurrentDictionaryGroupTestGrpcServerFixture.CustomServerFixture server) - : base(server) - { - } -} diff --git a/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/GroupTestBase.cs b/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/GroupTest.cs similarity index 98% rename from tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/GroupTestBase.cs rename to tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/GroupTest.cs index 1cb02a4ac..16fa09711 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/GroupTestBase.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/GroupTest.cs @@ -1,14 +1,18 @@ namespace MagicOnion.Server.Tests.StreamingHubBroadcastTest; -public abstract class GroupTestBase +public class GroupTest : IClassFixture { readonly ServerFixture server; - public GroupTestBase(ServerFixture server) + public GroupTest(CustomServerFixture server) { this.server = server; } + public class CustomServerFixture : ServerFixture + { + } + [Fact] public async Task BroadcastToSelf() { diff --git a/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/ImmutableArrayGroupTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/ImmutableArrayGroupTest.cs deleted file mode 100644 index b52891a9e..000000000 --- a/tests/MagicOnion.Server.Tests/StreamingHubBroadcastTest/ImmutableArrayGroupTest.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MagicOnion.Server.Hubs; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace MagicOnion.Server.Tests.StreamingHubBroadcastTest; - -[CollectionDefinition(nameof(StreamingHubBroadcastImmutableArrayGroupTestGrpcServerFixture))] -public class StreamingHubBroadcastImmutableArrayGroupTestGrpcServerFixture : ICollectionFixture -{ - public class CustomServerFixture : ServerFixture - { - protected override void ConfigureServices(IServiceCollection services) - { - services.RemoveAll(); - services.TryAddSingleton(); - } - protected override void ConfigureMagicOnion(MagicOnionOptions options) - { - } - } -} - -[Collection(nameof(StreamingHubBroadcastImmutableArrayGroupTestGrpcServerFixture))] -public class ImmutableArrayGroupTest : GroupTestBase -{ - public ImmutableArrayGroupTest(StreamingHubBroadcastImmutableArrayGroupTestGrpcServerFixture.CustomServerFixture server) - : base(server) - { - } -} diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs index 9d4ef72b4..0c6dd2918 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubHandlerTest.cs @@ -1,10 +1,12 @@ using System.Buffers; using Grpc.Core; +using MagicOnion.Internal; using MagicOnion.Serialization; using MagicOnion.Serialization.MessagePack; using MagicOnion.Server.Hubs; using MessagePack; using Microsoft.Extensions.DependencyInjection; +using NSubstitute; namespace MagicOnion.Server.Tests; @@ -19,16 +21,12 @@ public async Task Parameterless_Returns_Task() var hubType = typeof(StreamingHubHandlerTestHub); var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_Task))!; var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.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), - }; + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(Nil.Default), string.Empty, DateTime.Now, 0, 0); await handler.MethodBody.Invoke(ctx); // Assert @@ -46,7 +44,7 @@ byte[] BuildMessage() return buffer.WrittenMemory.ToArray(); } - fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage()); + fakeStreamingHubContext.Responses[0].Memory.ToArray().Should().Equal(BuildMessage()); } [Fact] @@ -58,16 +56,12 @@ public async Task Parameterless_Returns_TaskOfInt32() var hubType = typeof(StreamingHubHandlerTestHub); var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_TaskOfInt32))!; var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.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), - }; + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(Nil.Default), string.Empty, DateTime.Now, 0, 0); await handler.MethodBody.Invoke(ctx); // Assert @@ -85,9 +79,9 @@ byte[] BuildMessage() return buffer.WrittenMemory.ToArray(); } - fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage()); + fakeStreamingHubContext.Responses[0].Memory.ToArray().Should().Equal(BuildMessage()); } - [Fact] + [Fact] public async Task Parameterless_Returns_ValueTask() { // Arrange @@ -96,16 +90,12 @@ public async Task Parameterless_Returns_ValueTask() var hubType = typeof(StreamingHubHandlerTestHub); var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTask))!; var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.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), - }; + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(Nil.Default), string.Empty, DateTime.Now, 0, 0); await handler.MethodBody.Invoke(ctx); // Assert @@ -123,7 +113,7 @@ byte[] BuildMessage() return buffer.WrittenMemory.ToArray(); } - fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage()); + fakeStreamingHubContext.Responses[0].Memory.ToArray().Should().Equal(BuildMessage()); } [Fact] @@ -135,16 +125,12 @@ public async Task Parameterless_Returns_ValueTaskOfInt32() var hubType = typeof(StreamingHubHandlerTestHub); var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Returns_ValueTaskOfInt32))!; var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.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), - }; + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(Nil.Default), string.Empty, DateTime.Now, 0, 0); await handler.MethodBody.Invoke(ctx); // Assert @@ -162,7 +148,7 @@ byte[] BuildMessage() return buffer.WrittenMemory.ToArray(); } - fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage()); + fakeStreamingHubContext.Responses[0].Memory.ToArray().Should().Equal(BuildMessage()); } [Fact] @@ -174,16 +160,12 @@ public async Task Parameter_Single_Returns_Task() var hubType = typeof(StreamingHubHandlerTestHub); var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Single_Returns_Task))!; var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.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(12345), - }; + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(12345), string.Empty, DateTime.Now, 0, 0); await handler.MethodBody.Invoke(ctx); // Assert @@ -201,7 +183,7 @@ byte[] BuildMessage() return buffer.WrittenMemory.ToArray(); } - fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage()); + fakeStreamingHubContext.Responses[0].Memory.ToArray().Should().Equal(BuildMessage()); } [Fact] @@ -213,16 +195,12 @@ public async Task Parameter_Multiple_Returns_Task() var hubType = typeof(StreamingHubHandlerTestHub); var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_Task))!; var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.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(new DynamicArgumentTuple(12345, "テスト", true)), - }; + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(new DynamicArgumentTuple(12345, "テスト", true)), string.Empty, DateTime.Now, 0, 0); await handler.MethodBody.Invoke(ctx); // Assert @@ -239,7 +217,7 @@ byte[] BuildMessage() writer.Flush(); return buffer.WrittenMemory.ToArray(); } - fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage()); + fakeStreamingHubContext.Responses[0].Memory.ToArray().Should().Equal(BuildMessage()); } [Fact] @@ -251,16 +229,12 @@ public async Task Parameter_Multiple_Returns_TaskOfInt32() var hubType = typeof(StreamingHubHandlerTestHub); var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32))!; var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.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(new DynamicArgumentTuple(12345, "テスト", true)), - }; + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(new DynamicArgumentTuple(12345, "テスト", true)), string.Empty, DateTime.Now, 0, 0); await handler.MethodBody.Invoke(ctx); // Assert @@ -277,7 +251,7 @@ byte[] BuildMessage() writer.Flush(); return buffer.WrittenMemory.ToArray(); } - fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage()); + fakeStreamingHubContext.Responses[0].Memory.ToArray().Should().Equal(BuildMessage()); } [Fact] @@ -289,19 +263,14 @@ public async Task CallRepeated_Parameter_Multiple_Returns_TaskOfInt32() var hubType = typeof(StreamingHubHandlerTestHub); var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32))!; var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); // Act var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); for (var i = 0; i < 3; i++) { - var ctx = new StreamingHubContext() - { - MessageId = i * 1000, - HubInstance = hubInstance, - ServiceContext = fakeStreamingHubContext, - Request = MessagePackSerializer.Serialize(new DynamicArgumentTuple(i, $"テスト{i}", i % 2 == 0)), - }; + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(new DynamicArgumentTuple(i, $"テスト{i}", i % 2 == 0)), string.Empty, DateTime.Now, i * 1000, 0); await handler.MethodBody.Invoke(ctx); } @@ -322,9 +291,136 @@ byte[] BuildMessage(int messageId, int retVal) writer.Flush(); return buffer.WrittenMemory.ToArray(); } - fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage(0, 0)); - fakeStreamingHubContext.Responses[1].Should().Equal(BuildMessage(1000, 1)); - fakeStreamingHubContext.Responses[2].Should().Equal(BuildMessage(2000, 2)); + fakeStreamingHubContext.Responses[0].Memory.ToArray().Should().Equal(BuildMessage(0, 0)); + fakeStreamingHubContext.Responses[1].Memory.ToArray().Should().Equal(BuildMessage(1000, 1)); + fakeStreamingHubContext.Responses[2].Memory.ToArray().Should().Equal(BuildMessage(2000, 2)); + } + + [Fact] + public async Task Parameterless_Void() + { + // Arrange + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + var hubType = typeof(StreamingHubHandlerTestHub); + var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Void))!; + var hubInstance = new StreamingHubHandlerTestHub(); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + + // Act + var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(Nil.Default), string.Empty, DateTime.Now, 0, 0); + await handler.MethodBody.Invoke(ctx); + + // Assert + hubInstance.Results.Should().Contain(nameof(StreamingHubHandlerTestHub.Method_Parameterless_Void) + " 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].Memory.ToArray().Should().Equal(BuildMessage()); + } + + [Fact] + public async Task Parameter_Single_Void() + { + // Arrange + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + var hubType = typeof(StreamingHubHandlerTestHub); + var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Single_Void))!; + var hubInstance = new StreamingHubHandlerTestHub(); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + + // Act + var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(12345), string.Empty, DateTime.Now, 0, 0); + await handler.MethodBody.Invoke(ctx); + + // Assert + hubInstance.Results.Should().Contain(nameof(StreamingHubHandlerTestHub.Method_Parameter_Single_Void) + "(12345) 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].Memory.ToArray().Should().Equal(BuildMessage()); + } + + [Fact] + public async Task Parameter_Multiple_Void() + { + // Arrange + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + var hubType = typeof(StreamingHubHandlerTestHub); + var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Void))!; + var hubInstance = new StreamingHubHandlerTestHub(); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + + // Act + var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(new DynamicArgumentTuple(12345, "テスト", true)), string.Empty, DateTime.Now, 0, 0); + await handler.MethodBody.Invoke(ctx); + + // Assert + hubInstance.Results.Should().Contain(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Void) + "(12345,テスト,True) 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].Memory.ToArray().Should().Equal(BuildMessage()); + } + + [Fact] + public async Task Parameter_Multiple_Void_Without_MessageId() + { + // Arrange + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + var hubType = typeof(StreamingHubHandlerTestHub); + var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Void))!; + var hubInstance = new StreamingHubHandlerTestHub(); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + + // Act + var handler = new StreamingHubHandler(hubType, hubMethod, new StreamingHubHandlerOptions(new MagicOnionOptions()), serviceProvider); + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, MessagePackSerializer.Serialize(new DynamicArgumentTuple(12345, "テスト", true)), string.Empty, DateTime.Now, -1 /* The client requires no response */, 0); + await handler.MethodBody.Invoke(ctx); + + // Assert + hubInstance.Results.Should().Contain(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Void) + "(12345,テスト,True) called."); + fakeStreamingHubContext.Responses.Should().BeEmpty(); } [Fact] @@ -336,7 +432,7 @@ public async Task UseCustomMessageSerializer() var hubType = typeof(StreamingHubHandlerTestHub); var hubMethod = hubType.GetMethod(nameof(StreamingHubHandlerTestHub.Method_Parameter_Multiple_Returns_TaskOfInt32))!; var hubInstance = new StreamingHubHandlerTestHub(); - var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, XorMessagePackMagicOnionSerializerProvider.Instance.Create(MethodType.DuplexStreaming, null), serviceProvider); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, XorMessagePackMagicOnionSerializerProvider.Instance.Create(MethodType.DuplexStreaming, null), serviceProvider); var bufferWriter = new ArrayBufferWriter(); var serializer = XorMessagePackMagicOnionSerializerProvider.Instance.Create(MethodType.DuplexStreaming, null); serializer.Serialize(bufferWriter, new DynamicArgumentTuple(12345, "テスト", true)); @@ -346,12 +442,8 @@ public async Task UseCustomMessageSerializer() { MessageSerializer = XorMessagePackMagicOnionSerializerProvider.Instance, }), serviceProvider); - var ctx = new StreamingHubContext() - { - HubInstance = hubInstance, - ServiceContext = fakeStreamingHubContext, - Request = bufferWriter.WrittenMemory.ToArray(), - }; + var ctx = new StreamingHubContext(); + ctx.Initialize(fakeStreamingHubContext, hubInstance, bufferWriter.WrittenMemory.ToArray(), string.Empty, DateTime.Now, 0, 0); await handler.MethodBody.Invoke(ctx); // Assert @@ -368,10 +460,9 @@ byte[] BuildMessage() serializer.Serialize(buffer, 12345); return buffer.WrittenMemory.ToArray(); } - fakeStreamingHubContext.Responses[0].Should().Equal(BuildMessage()); + fakeStreamingHubContext.Responses[0].Memory.ToArray().Should().Equal(BuildMessage()); } - interface IStreamingHubHandlerTestHubReceiver { } @@ -388,7 +479,11 @@ interface IStreamingHubHandlerTestHub : IStreamingHub Method_Parameterless_Returns_ValueTaskOfInt32(); + void Method_Parameterless_Void(); + void Method_Parameter_Single_Void(int arg0); + void Method_Parameter_Multiple_Void(int arg0, string arg1, bool arg2); } + class StreamingHubHandlerTestHub : IStreamingHubHandlerTestHub { public List Results { get; } = new List(); @@ -439,5 +534,19 @@ public ValueTask Method_Parameterless_Returns_ValueTaskOfInt32() return ValueTask.FromResult(12345); } + public void Method_Parameterless_Void() + { + Results.Add(nameof(Method_Parameterless_Void) + " called."); + } + + public void Method_Parameter_Single_Void(int arg0) + { + Results.Add(nameof(Method_Parameter_Single_Void) + $"({arg0}) called."); + } + + public void Method_Parameter_Multiple_Void(int arg0, string arg1, bool arg2) + { + Results.Add(nameof(Method_Parameter_Multiple_Void) + $"({arg0},{arg1},{arg2}) called."); + } } } diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs new file mode 100644 index 000000000..100abd5bd --- /dev/null +++ b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs @@ -0,0 +1,225 @@ +using System.Buffers; +using Grpc.Core; +using MagicOnion.Internal; +using MagicOnion.Serialization.MessagePack; +using MagicOnion.Server.Hubs; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Testing; + +namespace MagicOnion.Server.Tests.StreamingHubHeartbeat; + +public class StreamingHubHeartbeatManagerTest +{ + static readonly byte[] HeartbeatMessageHeader = [0x95, 0x7f, 0xc0, 0xc0, 0xc0]; // [127, Nil, Nil, Nil, + static readonly byte[] HeartbeatMessageNoExtra = [0x95, 0x7f, 0xc0, 0xc0, 0xc0, 0xc0]; // [127, Nil, Nil, Nil, Nil] + + [Fact] + public void Register() + { + // Arrange + var logger = new FakeLogger(); + var manager = new StreamingHubHeartbeatManager(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, null, logger); + var context = CreateFakeStreamingServiceContext(); + + // Act + var handle = manager.Register(context); + + // Assert + Assert.NotNull(handle); + Assert.Equal(context, handle.ServiceContext); + Assert.False(handle.TimeoutToken.IsCancellationRequested); + } + + [Fact] + public void Handle_Dispose() + { + // Arrange + var logger = new FakeLogger(); + var manager = new StreamingHubHeartbeatManager(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, null, logger); + var context = CreateFakeStreamingServiceContext(); + var handle = manager.Register(context); + + // Act + handle.Dispose(); + } + + [Fact] + public async Task Interval_Disable_Timeout() + { + // Arrange + var logger = new FakeLogger(); + var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, null, logger); + var context1 = CreateFakeStreamingServiceContext(); + var context2 = CreateFakeStreamingServiceContext(); + var context3 = CreateFakeStreamingServiceContext(); + + // Act + using var handle1 = manager.Register(context1); + using var handle2 = manager.Register(context2); + using var handle3 = manager.Register(context3); + await Task.Delay(310); + + // Assert + Assert.Equal(3, context1.Responses.Count); + Assert.Equal(HeartbeatMessageNoExtra, context1.Responses[0].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context1.Responses[1].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context1.Responses[2].Memory.ToArray()); + Assert.Equal(3, context2.Responses.Count); + Assert.Equal(HeartbeatMessageNoExtra, context2.Responses[0].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context2.Responses[1].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context2.Responses[2].Memory.ToArray()); + Assert.Equal(3, context3.Responses.Count); + Assert.Equal(HeartbeatMessageNoExtra, context3.Responses[0].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context3.Responses[1].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context3.Responses[2].Memory.ToArray()); + } + + [Fact] + public async Task Interval_Keep() + { + // Arrange + var collector = FakeLogCollector.Create(new FakeLogCollectorOptions()); + var logger = new FakeLogger(collector); + var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(200), null, logger); + var context1 = CreateFakeStreamingServiceContext(); + var context2 = CreateFakeStreamingServiceContext(); + var context3 = CreateFakeStreamingServiceContext(); + + // Act + using var handle1 = manager.Register(context1); + using var handle2 = manager.Register(context2); + using var handle3 = manager.Register(context3); + await Task.Delay(350); + var isCanceled1 = handle1.TimeoutToken.IsCancellationRequested; + var isCanceled2 = handle2.TimeoutToken.IsCancellationRequested; + var isCanceled3 = handle3.TimeoutToken.IsCancellationRequested; + // Simulate to send heartbeat responses from clients. + handle1.Ack(); + handle2.Ack(); + handle3.Ack(); + await Task.Delay(250); + + // Assert + Assert.False(isCanceled1); + Assert.False(isCanceled2); + Assert.False(isCanceled3); + Assert.False(handle1.TimeoutToken.IsCancellationRequested); + Assert.False(handle2.TimeoutToken.IsCancellationRequested); + Assert.False(handle3.TimeoutToken.IsCancellationRequested); + } + + [Fact] + public async Task Interval_With_Timeout() + { + // Arrange + var collector = FakeLogCollector.Create(new FakeLogCollectorOptions()); + var logger = new FakeLogger(collector); + var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(200), null, logger); + var context1 = CreateFakeStreamingServiceContext(); + var context2 = CreateFakeStreamingServiceContext(); + var context3 = CreateFakeStreamingServiceContext(); + + // Act + using var handle1 = manager.Register(context1); + using var handle2 = manager.Register(context2); + using var handle3 = manager.Register(context3); + await Task.Delay(350); + var isCanceled1 = handle1.TimeoutToken.IsCancellationRequested; + var isCanceled2 = handle2.TimeoutToken.IsCancellationRequested; + var isCanceled3 = handle3.TimeoutToken.IsCancellationRequested; + await Task.Delay(250); // No responses from clients and timeouts are reached. + + // Assert + Assert.False(isCanceled1); + Assert.False(isCanceled2); + Assert.False(isCanceled3); + Assert.True(handle1.TimeoutToken.IsCancellationRequested); + Assert.True(handle2.TimeoutToken.IsCancellationRequested); + Assert.True(handle3.TimeoutToken.IsCancellationRequested); + } + + [Fact] + public async Task Interval_Stop_After_HandleDisposed() + { + // Arrange + var logger = new FakeLogger(); + var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, null, logger); + var context1 = CreateFakeStreamingServiceContext(); + var context2 = CreateFakeStreamingServiceContext(); + var context3 = CreateFakeStreamingServiceContext(); + + // Act + var handle1 = manager.Register(context1); + var handle2 = manager.Register(context2); + var handle3 = manager.Register(context3); + await Task.Delay(310); + handle1.Dispose(); + handle2.Dispose(); + handle3.Dispose(); + await Task.Delay(300); + + // Assert + Assert.Equal(3, context1.Responses.Count); + Assert.Equal(HeartbeatMessageNoExtra, context1.Responses[0].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context1.Responses[1].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context1.Responses[2].Memory.ToArray()); + Assert.Equal(3, context2.Responses.Count); + Assert.Equal(HeartbeatMessageNoExtra, context2.Responses[0].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context2.Responses[1].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context2.Responses[2].Memory.ToArray()); + Assert.Equal(3, context3.Responses.Count); + Assert.Equal(HeartbeatMessageNoExtra, context3.Responses[0].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context3.Responses[1].Memory.ToArray()); + Assert.Equal(HeartbeatMessageNoExtra, context3.Responses[2].Memory.ToArray()); + } + + [Fact] + public async Task CustomMetadataProvider() + { + // Arrange + var logger = new FakeLogger(); + var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, new CustomHeartbeatMetadataProvider(), logger); + var context1 = CreateFakeStreamingServiceContext(); + var context2 = CreateFakeStreamingServiceContext(); + var context3 = CreateFakeStreamingServiceContext(); + byte[] expectedHeartbeatMessage = [.. HeartbeatMessageHeader, .. "Hello"u8]; + + // Act + using var handle1 = manager.Register(context1); + using var handle2 = manager.Register(context2); + using var handle3 = manager.Register(context3); + await Task.Delay(310); + + // Assert + Assert.Equal(3, context1.Responses.Count); + Assert.Equal(expectedHeartbeatMessage, context1.Responses[0].Memory.ToArray()); + Assert.Equal(expectedHeartbeatMessage, context1.Responses[1].Memory.ToArray()); + Assert.Equal(expectedHeartbeatMessage, context1.Responses[2].Memory.ToArray()); + Assert.Equal(3, context2.Responses.Count); + Assert.Equal(expectedHeartbeatMessage, context2.Responses[0].Memory.ToArray()); + Assert.Equal(expectedHeartbeatMessage, context2.Responses[1].Memory.ToArray()); + Assert.Equal(expectedHeartbeatMessage, context2.Responses[2].Memory.ToArray()); + Assert.Equal(3, context3.Responses.Count); + Assert.Equal(expectedHeartbeatMessage, context3.Responses[0].Memory.ToArray()); + Assert.Equal(expectedHeartbeatMessage, context3.Responses[1].Memory.ToArray()); + Assert.Equal(expectedHeartbeatMessage, context3.Responses[2].Memory.ToArray()); + } + + class CustomHeartbeatMetadataProvider : IStreamingHubHeartbeatMetadataProvider + { + public bool TryWriteMetadata(IBufferWriter writer) + { + writer.Write("Hello"u8); + return true; + } + } + + static FakeStreamingServiceContext CreateFakeStreamingServiceContext() + { + var hubType = typeof(ITestHub); + var hubMethod = hubType.GetMethod(nameof(ITestHub.OneArgument))!; + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var fakeStreamingHubContext = new FakeStreamingServiceContext(hubType, hubMethod, MessagePackMagicOnionSerializerProvider.Default.Create(MethodType.DuplexStreaming, null), serviceProvider); + return fakeStreamingHubContext; + } +} diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatTest.cs new file mode 100644 index 000000000..4525fe28a --- /dev/null +++ b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatTest.cs @@ -0,0 +1,218 @@ +using System.Collections.Concurrent; +using MagicOnion.Client; +using MagicOnion.Server.Hubs; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; + +namespace MagicOnion.Server.Tests.StreamingHubHeartbeat; + +public abstract class StreamingHubHeartbeatTestBase +{ + protected ServerFixture Fixture { get; } + + public StreamingHubHeartbeatTestBase(ServerFixture fixture) + { + this.Fixture = fixture; + } + + [Fact] + public async Task EnableByAttribute() + { + // Arrange + var receiver = Substitute.For(); + var receivedHeartbeatMetadata = new List(); + var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + await Task.Delay(650); + await client.DisposeAsync(); + + // Assert + Assert.Equal(2, receivedHeartbeatMetadata.Count); // The client must receive a heartbeat every 300ms from the server. + } + + [Fact] + public async Task DisableByAttribute() + { + // Arrange + var receiver = Substitute.For(); + var receivedHeartbeatMetadata = new List(); + var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + await Task.Delay(650); + await client.DisposeAsync(); + + // Assert + Assert.Empty(receivedHeartbeatMetadata); + } + + [Fact] + public async Task Override_Interval() + { + // Arrange + var receiver = Substitute.For(); + var receivedHeartbeatMetadata = new List(); + var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + await Task.Delay(650); + await client.DisposeAsync(); + + // Assert + Assert.Single(receivedHeartbeatMetadata); // The client must receive a heartbeat every 500ms from the server. + } + + [Fact] + public async Task Timeout() + { + // Arrange + var heartbeatReceived = new TaskCompletionSource(); + var receiver = Substitute.For(); + var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => + { + heartbeatReceived.SetResult(); + Thread.Sleep(200); // Block consuming message loop. + }); + + // We need to consume message inline. Avoid post continuations to the synchronization context. + SynchronizationContext.SetSynchronizationContext(null); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + + // Wait for receiving a heartbeat from the server. + // The client must receive a heartbeat every 200ms from the server. + await heartbeatReceived.Task.WaitAsync(TimeSpan.FromSeconds(1)); + + // Timeout at 100 ms after receiving a heartbeat. + await Task.Delay(500); + + // Assert + Assert.True((bool)Fixture.Items.GetValueOrDefault("Disconnected")); + Assert.True(client.WaitForDisconnect().IsCompleted); + } +} + + +public class StreamingHubHeartbeatTest_DisabledByDefault : StreamingHubHeartbeatTestBase, IClassFixture +{ + public StreamingHubHeartbeatTest_DisabledByDefault(StreamingHubHeartbeatTestServerFixture fixture) + : base(fixture) + { + } + + public class StreamingHubHeartbeatTestServerFixture : ServerFixture< + StreamingHubHeartbeatTestHub, + StreamingHubHeartbeatTestHub_EnableByAttribute, + StreamingHubHeartbeatTestHub_DisableByAttribute, + StreamingHubHeartbeatTestHub_CustomIntervalAndTimeout, + StreamingHubHeartbeatTestHub_TimeoutBehavior + > + { + protected override void ConfigureMagicOnion(MagicOnionOptions options) + { + options.StreamingHubHeartbeatInterval = TimeSpan.FromMilliseconds(300); + options.StreamingHubHeartbeatTimeout = TimeSpan.FromMilliseconds(200); + options.EnableStreamingHubHeartbeat = false; // Disabled by default. + } + } + + [Fact] + public async Task Default_Disable() + { + // Arrange + var receiver = Substitute.For(); + var receivedHeartbeatMetadata = new List(); + var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + await Task.Delay(650); + await client.DisposeAsync(); + + // Assert + Assert.Empty(receivedHeartbeatMetadata); + } +} + + +public class StreamingHubHeartbeatTest_EnabledByDefault : StreamingHubHeartbeatTestBase, IClassFixture +{ + public StreamingHubHeartbeatTest_EnabledByDefault(StreamingHubHeartbeatTestServerFixture fixture) + : base(fixture) + { + } + + public class StreamingHubHeartbeatTestServerFixture : ServerFixture< + StreamingHubHeartbeatTestHub, + StreamingHubHeartbeatTestHub_EnableByAttribute, + StreamingHubHeartbeatTestHub_DisableByAttribute, + StreamingHubHeartbeatTestHub_CustomIntervalAndTimeout, + StreamingHubHeartbeatTestHub_TimeoutBehavior + > + { + protected override void ConfigureMagicOnion(MagicOnionOptions options) + { + options.StreamingHubHeartbeatInterval = TimeSpan.FromMilliseconds(300); + options.StreamingHubHeartbeatTimeout = TimeSpan.FromMilliseconds(200); + options.EnableStreamingHubHeartbeat = true; // Enabled by default. + } + } + + [Fact] + public async Task Default_Enable() + { + // Arrange + var receiver = Substitute.For(); + var receivedHeartbeatMetadata = new List(); + var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + await Task.Delay(650); + await client.DisposeAsync(); + + // Assert + Assert.Equal(2, receivedHeartbeatMetadata.Count); + } +} + +public interface IStreamingHubHeartbeatTestHub : IStreamingHub; +public interface IStreamingHubHeartbeatTestHub_EnableByAttribute : IStreamingHub; +public interface IStreamingHubHeartbeatTestHub_DisableByAttribute : IStreamingHub; +public interface IStreamingHubHeartbeatTestHub_CustomIntervalAndTimeout : IStreamingHub; +public interface IStreamingHubHeartbeatTestHub_TimeoutBehavior : IStreamingHub; +public interface IStreamingHubHeartbeatTestHubReceiver; + +// Implementations + +// This streaming hub has no `Heartbeat` attribute. +public class StreamingHubHeartbeatTestHub() + : StreamingHubBase, IStreamingHubHeartbeatTestHub; + +[Heartbeat] +public class StreamingHubHeartbeatTestHub_EnableByAttribute() + : StreamingHubBase, IStreamingHubHeartbeatTestHub_EnableByAttribute; + +[Heartbeat(Enable = false)] +public class StreamingHubHeartbeatTestHub_DisableByAttribute() + : StreamingHubBase, IStreamingHubHeartbeatTestHub_DisableByAttribute; + +[Heartbeat(Interval = 500, Timeout = 100)] +public class StreamingHubHeartbeatTestHub_CustomIntervalAndTimeout() + : StreamingHubBase, IStreamingHubHeartbeatTestHub_CustomIntervalAndTimeout; + +[Heartbeat(Enable = true, Interval = 200, Timeout = 100)] +public class StreamingHubHeartbeatTestHub_TimeoutBehavior([FromKeyedServices(ServerFixture.ItemsServiceKey)] ConcurrentDictionary items) + : StreamingHubBase, IStreamingHubHeartbeatTestHub_TimeoutBehavior +{ + protected override ValueTask OnDisconnected() + { + items["Disconnected"] = true; + return base.OnDisconnected(); + } +} diff --git a/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs index e878c7072..ec1ceb9bb 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubMethodHandlerMetadataFactoryTest.cs @@ -63,20 +63,6 @@ public void Invalid_Returns_Int() ex.Should().NotBeNull(); ex.Should().BeOfType(); } - [Fact] - public void Invalid_Returns_Void() - { - // Arrange - var type = typeof(MyHub); - var methodInfo = type.GetMethod(nameof(MyHub.Invalid_Method_Returns_Void))!; - - // Act - var ex = Record.Exception(() => MethodHandlerMetadataFactory.CreateStreamingHubMethodHandlerMetadata(type, methodInfo)); - - // Assert - ex.Should().NotBeNull(); - ex.Should().BeOfType(); - } [Fact] public void Invalid_Returns_Generic() @@ -348,6 +334,26 @@ public void ValueTaskOfT() metadata.ResponseType.Should().Be(); } + [Fact] + public void Void() + { + // Arrange + var type = typeof(MyHub); + var methodInfo = type.GetMethod(nameof(MyHub.Method_Void))!; + + // 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_Void))); + metadata.ImplementationMethod.Should().BeSameAs(methodInfo); + metadata.Parameters.Should().BeEmpty(); + metadata.RequestType.Should().Be(); + metadata.ResponseType.Should().BeNull(); + } + interface IMyHubReceiver {} @@ -357,12 +363,12 @@ interface IMyHub : IStreamingHub Task Method_TaskOfValue(); ValueTask Method_ValueTask(); ValueTask Method_ValueTaskOfValue(); + void Method_Void(); Task Method_Parameterless(); Task Method_OneParameter(int arg0); Task Method_TwoParameters(int arg0, string arg1); - void Invalid_Method_Returns_Void(int arg0, string arg1); int Invalid_Method_Returns_Int(int arg0, string arg1); T Invalid_Method_Generic(T arg0); } @@ -375,6 +381,7 @@ class MyHub : IMyHub 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(); + public void Method_Void() => throw new NotImplementedException(); public void Invalid_Method_Returns_Void(int arg0, string arg1) => throw new NotImplementedException(); public int Invalid_Method_Returns_Int(int arg0, string arg1) => throw new NotImplementedException(); public T Invalid_Method_Generic(T arg0) => throw new NotImplementedException(); diff --git a/tests/MagicOnion.Server.Tests/StreamingHubTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubTest.cs index 6588a3c4b..fbdd3d9fd 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubTest.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubTest.cs @@ -54,7 +54,7 @@ public class TestObject public class TestHub : StreamingHubBase, ITestHub { - IGroup group; + IGroup group; protected override async ValueTask OnConnecting() { @@ -63,7 +63,7 @@ protected override async ValueTask OnConnecting() protected override async ValueTask OnConnected() { - BroadcastToSelf(group).VoidOnConnected(123, "foo", 12.3f); + group.Single(ConnectionId).VoidOnConnected(123, "foo", 12.3f); } protected override async ValueTask OnDisconnected() @@ -74,25 +74,25 @@ protected override async ValueTask OnDisconnected() public async Task MoreArgument(int x, string y, double z) { - BroadcastToSelf(group).VoidMoreArgument(x, y, z); + group.Single(ConnectionId).VoidMoreArgument(x, y, z); //await Broadcast(group).MoreArgument(x, y, z); } public async Task OneArgument(int x) { - Broadcast(group).VoidOneArgument(x); + group.All.VoidOneArgument(x); // await Broadcast(group).OneArgument(x); } public async Task OneArgument2(TestObject x) { - Broadcast(group).VoidOneArgument2(x); + group.All.VoidOneArgument2(x); //await Broadcast(group).OneArgument2(x); } public async Task OneArgument3(TestObject[] x) { - Broadcast(group).VoidOneArgument3(x); + group.All.VoidOneArgument3(x); //await Broadcast(group).OneArgument3(x); } @@ -123,7 +123,7 @@ public async Task RetrunZeroArgument() public async Task ZeroArgument() { - Broadcast(group).VoidZeroArgument(); + group.All.VoidZeroArgument(); //await Broadcast(group).ZeroArgument(); } } diff --git a/tests/MagicOnion.Server.Tests/_ServerFixture.cs b/tests/MagicOnion.Server.Tests/_ServerFixture.cs index 621a37d80..3b6de8add 100644 --- a/tests/MagicOnion.Server.Tests/_ServerFixture.cs +++ b/tests/MagicOnion.Server.Tests/_ServerFixture.cs @@ -1,3 +1,5 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using MagicOnion.Client; using System.Security.Cryptography; using Grpc.Net.Client; @@ -37,10 +39,15 @@ public static Random ThreadRandom public abstract class ServerFixture : IDisposable { - private Task hostTask; - private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + Task hostTask; + readonly CancellationTokenSource cancellationTokenSource = new(); + readonly TaskCompletionSource serverStartupWaiter = new(); public GrpcChannel DefaultChannel { get; private set; } + public Task ServerStarted => serverStartupWaiter.Task; + + public ConcurrentDictionary Items { get; } = new(); + public const string ItemsServiceKey = "ServerFixture.Items"; public ServerFixture() { @@ -49,7 +56,8 @@ public ServerFixture() protected abstract IEnumerable GetServiceTypes(); - protected virtual void PrepareServer() + [MemberNotNull(nameof(hostTask))] + void PrepareServer() { var port = RandomProvider.ThreadRandom.Next(10000, 30000); @@ -60,6 +68,9 @@ protected virtual void PrepareServer() .ConfigureLogging(logging => { logging.ClearProviders(); +#if DEBUG + logging.AddFilter(x => true); // Disable all rules. +#endif logging.AddDebug(); }) .ConfigureWebHostDefaults(webBuilder => @@ -78,14 +89,37 @@ protected virtual void PrepareServer() .ConfigureServices(services => { services.AddMagicOnion(GetServiceTypes(), ConfigureMagicOnion); + services.AddKeyedSingleton(ItemsServiceKey, Items); + + services.AddKeyedSingleton(HostStartupService.WaiterKey, serverStartupWaiter); + services.AddHostedService(); }) .ConfigureServices(ConfigureServices) .Build() .RunAsync(cancellationTokenSource.Token); + if (hostTask.IsFaulted) hostTask.GetAwaiter().GetResult(); + DefaultChannel = GrpcChannel.ForAddress($"http://localhost:{port}"); } + class HostStartupService([FromKeyedServices(HostStartupService.WaiterKey)] TaskCompletionSource waiter, IHostApplicationLifetime applicationLifetime) : IHostedService + { + public const string WaiterKey = $"{nameof(HostStartupService)}.Waiter"; + + public Task StartAsync(CancellationToken cancellationToken) + { + applicationLifetime.ApplicationStarted.Register(() => waiter.SetResult()); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + waiter.TrySetCanceled(); + return Task.CompletedTask; + } + } + protected virtual void ConfigureMagicOnion(MagicOnionOptions options) { } @@ -117,15 +151,23 @@ public T CreateClient() return MagicOnionClient.Create(DefaultChannel); } - public TStreamingHub CreateStreamingHubClient(TReceiver receiver) + public TStreamingHub CreateStreamingHubClient(TReceiver receiver, StreamingHubClientOptions options = default) + where TStreamingHub : IStreamingHub + { + return CreateStreamingHubClientAsync(receiver, options).GetAwaiter().GetResult(); + } + + public Task CreateStreamingHubClientAsync(TReceiver receiver, StreamingHubClientOptions options = default) where TStreamingHub : IStreamingHub { - return StreamingHubClient.ConnectAsync(DefaultChannel, receiver).GetAwaiter().GetResult(); + options ??= StreamingHubClientOptions.CreateWithDefault(); + return StreamingHubClient.ConnectAsync(DefaultChannel, receiver, options); } public void Dispose() { try { DefaultChannel.ShutdownAsync().Wait(1000); } catch { } + try { DefaultChannel.Dispose(); } catch { } try { cancellationTokenSource.Cancel(); hostTask.Wait(); } catch { } } @@ -134,22 +176,42 @@ public void Dispose() public class ServerFixture : ServerFixture { protected override IEnumerable GetServiceTypes() - => new [] { typeof(TServiceOrHub) }; + => [typeof(TServiceOrHub)]; } public class ServerFixture : ServerFixture { protected override IEnumerable GetServiceTypes() - => new[] { typeof(TServiceOrHub1), typeof(TServiceOrHub2) }; + => [typeof(TServiceOrHub1), typeof(TServiceOrHub2)]; } public class ServerFixture : ServerFixture { protected override IEnumerable GetServiceTypes() - => new[] { typeof(TServiceOrHub1), typeof(TServiceOrHub2), typeof(TServiceOrHub3) }; + => [typeof(TServiceOrHub1), typeof(TServiceOrHub2), typeof(TServiceOrHub3)]; } public class ServerFixture : ServerFixture { protected override IEnumerable GetServiceTypes() - => new[] { typeof(TServiceOrHub1), typeof(TServiceOrHub2), typeof(TServiceOrHub3), typeof(TServiceOrHub4) }; + => [typeof(TServiceOrHub1), typeof(TServiceOrHub2), typeof(TServiceOrHub3), typeof(TServiceOrHub4)]; +} +public class ServerFixture : ServerFixture +{ + protected override IEnumerable GetServiceTypes() + => [typeof(TServiceOrHub1), typeof(TServiceOrHub2), typeof(TServiceOrHub3), typeof(TServiceOrHub4), typeof(TServiceOrHub5)]; +} +public class ServerFixture : ServerFixture +{ + protected override IEnumerable GetServiceTypes() + => [typeof(TServiceOrHub1), typeof(TServiceOrHub2), typeof(TServiceOrHub3), typeof(TServiceOrHub4), typeof(TServiceOrHub5), typeof(TServiceOrHub6)]; +} +public class ServerFixture : ServerFixture +{ + protected override IEnumerable GetServiceTypes() + => [typeof(TServiceOrHub1), typeof(TServiceOrHub2), typeof(TServiceOrHub3), typeof(TServiceOrHub4), typeof(TServiceOrHub5), typeof(TServiceOrHub6), typeof(TServiceOrHub7)]; +} +public class ServerFixture : ServerFixture +{ + protected override IEnumerable GetServiceTypes() + => [typeof(TServiceOrHub1), typeof(TServiceOrHub2), typeof(TServiceOrHub3), typeof(TServiceOrHub4), typeof(TServiceOrHub5), typeof(TServiceOrHub6), typeof(TServiceOrHub7), typeof(TServiceOrHub8)]; } [CollectionDefinition(nameof(AllAssemblyGrpcServerFixture))]