From e5e47ccc9477ca4593010c777cd50925457c4fb6 Mon Sep 17 00:00:00 2001 From: nenoNaninu Date: Wed, 25 Sep 2024 12:24:34 +0900 Subject: [PATCH 1/2] support CancellationToken in the last parameter of a receive method --- src/TypedSignalR.Client/SourceGenerator.cs | 2 +- .../HubConnectionExtensionsBinderTemplate.cs | 6 ++- .../HubConnectionExtensionsTemplate.cs | 47 +++++++++++++++++++ .../Templates/MethodMetadataExtensions.cs | 40 ++++++++++++---- 4 files changed, 82 insertions(+), 13 deletions(-) diff --git a/src/TypedSignalR.Client/SourceGenerator.cs b/src/TypedSignalR.Client/SourceGenerator.cs index 014c4f6..fc90e63 100644 --- a/src/TypedSignalR.Client/SourceGenerator.cs +++ b/src/TypedSignalR.Client/SourceGenerator.cs @@ -208,7 +208,7 @@ private static void GenerateBinderSource(SourceProductionContext context, (Immut { var receiverTypes = ExtractReceiverTypesFromRegisterMethods(context, sourceSymbols, specialSymbols); - var template = new HubConnectionExtensionsBinderTemplate(receiverTypes); + var template = new HubConnectionExtensionsBinderTemplate(receiverTypes, specialSymbols); var source = NormalizeNewLines(template.TransformText()); diff --git a/src/TypedSignalR.Client/Templates/HubConnectionExtensionsBinderTemplate.cs b/src/TypedSignalR.Client/Templates/HubConnectionExtensionsBinderTemplate.cs index c7ba6b4..c037610 100644 --- a/src/TypedSignalR.Client/Templates/HubConnectionExtensionsBinderTemplate.cs +++ b/src/TypedSignalR.Client/Templates/HubConnectionExtensionsBinderTemplate.cs @@ -7,10 +7,12 @@ namespace TypedSignalR.Client.Templates; public sealed class HubConnectionExtensionsBinderTemplate { private readonly IReadOnlyList _receiverTypes; + private readonly SpecialSymbols _specialSymbols; - public HubConnectionExtensionsBinderTemplate(IReadOnlyList receiverTypes) + public HubConnectionExtensionsBinderTemplate(IReadOnlyList receiverTypes, SpecialSymbols specialSymbols) { _receiverTypes = receiverTypes; + _specialSymbols = specialSymbols; } public string TransformText() @@ -104,7 +106,7 @@ private string CreateRegistrationString(TypeMetadata receiverType) private string CreateRegistrationStringCore(MethodMetadata method) { return $$""" - compositeDisposable.Add(global::Microsoft.AspNetCore.SignalR.Client.HubConnectionExtensions.On(connection, nameof(receiver.{{method.MethodName}}), {{method.CreateParameterTypeArrayString()}}, HandlerConverter.Convert{{method.CreateTypeArgumentsStringForHandlerConverter()}}(receiver.{{method.MethodName}}))); + compositeDisposable.Add(global::Microsoft.AspNetCore.SignalR.Client.HubConnectionExtensions.On(connection, nameof(receiver.{{method.MethodName}}), {{method.CreateParameterTypeArrayString(_specialSymbols)}}, HandlerConverter.Convert{{method.CreateTypeArgumentsStringForHandlerConverter(_specialSymbols)}}(receiver.{{method.MethodName}}))); """; } } diff --git a/src/TypedSignalR.Client/Templates/HubConnectionExtensionsTemplate.cs b/src/TypedSignalR.Client/Templates/HubConnectionExtensionsTemplate.cs index 5b6ceaa..e72cf60 100644 --- a/src/TypedSignalR.Client/Templates/HubConnectionExtensionsTemplate.cs +++ b/src/TypedSignalR.Client/Templates/HubConnectionExtensionsTemplate.cs @@ -261,6 +261,25 @@ private static class HandlerConverter """); } + stringBuilder.AppendLine(""" + + public static global::System.Func Convert(global::System.Func handler) + { + return args => handler(default); + } +"""); + + for (int i = 1; i <= 15; i++) + { + stringBuilder.AppendLine($$""" + + public static global::System.Func Convert<{{CreateTypeParametersString(i)}}>(global::System.Func<{{CreateTypeParametersString(i)}}, global::System.Threading.CancellationToken, global::System.Threading.Tasks.Task> handler) + { + return args => handler({{CreateHandlerArgumentsString(i)}}, default); + } +"""); + } + stringBuilder.AppendLine(""" public static global::System.Func> Convert(global::System.Func> handler) @@ -272,6 +291,7 @@ private static class HandlerConverter }; } """); + for (int i = 1; i <= 16; i++) { stringBuilder.AppendLine($$""" @@ -284,6 +304,33 @@ private static class HandlerConverter return result; }; } +"""); + } + + stringBuilder.AppendLine(""" + + public static global::System.Func> Convert(global::System.Func> handler) + { + return async args => + { + var result = await handler(default).ConfigureAwait(false); + return result; + }; + } +"""); + + for (int i = 1; i <= 15; i++) + { + stringBuilder.AppendLine($$""" + + public static global::System.Func> Convert<{{CreateTypeParametersString(i)}}, TResult>(global::System.Func<{{CreateTypeParametersString(i)}}, global::System.Threading.CancellationToken, global::System.Threading.Tasks.Task> handler) + { + return async args => + { + var result = await handler({{CreateHandlerArgumentsString(i)}}, default).ConfigureAwait(false); + return result; + }; + } """); } } diff --git a/src/TypedSignalR.Client/Templates/MethodMetadataExtensions.cs b/src/TypedSignalR.Client/Templates/MethodMetadataExtensions.cs index 634ec05..404fc02 100644 --- a/src/TypedSignalR.Client/Templates/MethodMetadataExtensions.cs +++ b/src/TypedSignalR.Client/Templates/MethodMetadataExtensions.cs @@ -86,9 +86,13 @@ public static string CreateArgumentsStringExceptCancellationToken(this MethodMet return sb.ToString(); } - public static string CreateTypeArgumentsStringForHandlerConverter(this MethodMetadata methodMetadata) + public static string CreateTypeArgumentsStringForHandlerConverter(this MethodMetadata methodMetadata, SpecialSymbols specialSymbols) { - if (methodMetadata.Parameters.Count == 0) + var parameters = methodMetadata.LastParameterEqual(specialSymbols.CancellationTokenSymbol) + ? methodMetadata.Parameters.Take(methodMetadata.Parameters.Count - 1).ToArray() + : methodMetadata.Parameters; + + if (parameters.Count == 0) { if (methodMetadata.IsGenericReturnType) { @@ -98,25 +102,25 @@ public static string CreateTypeArgumentsStringForHandlerConverter(this MethodMet return string.Empty; } - if (methodMetadata.Parameters.Count == 1) + if (parameters.Count == 1) { if (methodMetadata.IsGenericReturnType) { - return $"<{methodMetadata.Parameters[0].TypeName}, {methodMetadata.GenericReturnTypeArgument}>"; + return $"<{parameters[0].TypeName}, {methodMetadata.GenericReturnTypeArgument}>"; } - return $"<{methodMetadata.Parameters[0].TypeName}>"; + return $"<{parameters[0].TypeName}>"; } var sb = new StringBuilder(); sb.Append('<'); - sb.Append(methodMetadata.Parameters[0].TypeName); + sb.Append(parameters[0].TypeName); - for (int i = 1; i < methodMetadata.Parameters.Count; i++) + for (int i = 1; i < parameters.Count; i++) { sb.Append(", "); - sb.Append(methodMetadata.Parameters[i].TypeName); + sb.Append(parameters[i].TypeName); } if (methodMetadata.IsGenericReturnType) @@ -129,9 +133,10 @@ public static string CreateTypeArgumentsStringForHandlerConverter(this MethodMet return sb.ToString(); } - public static string CreateParameterTypeArrayString(this MethodMetadata methodMetadata) + public static string CreateParameterTypeArrayString(this MethodMetadata methodMetadata, SpecialSymbols specialSymbols) { - if (methodMetadata.Parameters.Count == 0) + if (methodMetadata.Parameters.Count == 0 + || (methodMetadata.Parameters.Count == 1 && methodMetadata.LastParameterEqual(specialSymbols.CancellationTokenSymbol))) { return "global::System.Type.EmptyTypes"; } @@ -148,6 +153,13 @@ public static string CreateParameterTypeArrayString(this MethodMetadata methodMe for (int i = 1; i < methodMetadata.Parameters.Count; i++) { + if (IsLast(i, methodMetadata.Parameters.Count) + && methodMetadata.LastParameterEqual(specialSymbols.CancellationTokenSymbol)) + { + // break if the last parameter is CancellationToken. + break; + } + sb.Append($", typeof({methodMetadata.Parameters[i].FullyQualifiedTypeName})"); } @@ -189,6 +201,14 @@ public static string CreateMethodString(this MethodMetadata methodMetadata, Spec }; } + private static bool IsLast(int index, int length) => index == length - 1; + + private static bool LastParameterEqual(this MethodMetadata methodMetadata, ITypeSymbol typeSymbol) + { + return methodMetadata.Parameters.Count > 0 + && SymbolEqualityComparer.Default.Equals(methodMetadata.Parameters[methodMetadata.Parameters.Count - 1].Type, typeSymbol); + } + private static HubMethodType GetHubMethodType(this MethodMetadata methodMetadata, SpecialSymbols specialSymbols) { var returnTypeSymbol = methodMetadata.ReturnTypeSymbol as INamedTypeSymbol; From a59063184b1b5e46c5b0d4f85a2ecbecbf7bc551 Mon Sep 17 00:00:00 2001 From: nenoNaninu Date: Wed, 25 Sep 2024 12:37:18 +0900 Subject: [PATCH 2/2] add tests --- .../ReceiverWithCancellationTokenTestHub.cs | 62 +++++++++ .../Program.cs | 1 + .../IReceiver.cs | 8 ++ .../Hubs/ReceiverWithCancellationTokenTest.cs | 127 ++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 tests/TypedSignalR.Client.Tests.Server/Hubs/ReceiverWithCancellationTokenTestHub.cs create mode 100644 tests/TypedSignalR.Client.Tests/Hubs/ReceiverWithCancellationTokenTest.cs diff --git a/tests/TypedSignalR.Client.Tests.Server/Hubs/ReceiverWithCancellationTokenTestHub.cs b/tests/TypedSignalR.Client.Tests.Server/Hubs/ReceiverWithCancellationTokenTestHub.cs new file mode 100644 index 0000000..7e9eeee --- /dev/null +++ b/tests/TypedSignalR.Client.Tests.Server/Hubs/ReceiverWithCancellationTokenTestHub.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.SignalR; +using TypedSignalR.Client.Tests.Shared; + +namespace TypedSignalR.Client.Tests.Server.Hubs; + +public class ReceiverWithCancellationTokenTestHub : Hub, IReceiverTestHub +{ + private readonly string[] _message = new[] { + "b1f7cd73-13b8-49bd-9557-ffb38859d18b", + "3f5c3585-d01b-4f8f-8139-62a1241850e2", + "92021a22-5823-4501-8cbd-c20d4ca6e54c", + "5b134f73-2dc1-4271-8316-1a4250f42241", + "e73acd30-e034-4569-8f30-88ac34b99052", + "0d7531b5-0a36-4fe7-bbe5-8fee38c38c07", + "32915627-3df6-41dc-8d30-7c655c2f7e61", + "c875a6f9-9ddb-440b-a7e4-6e893f59ab9e", + }; + + private readonly string[] _guids = new[] { + "b2f626e5-b4d4-4713-891d-f6cb107e502e", + "22733524-2087-4701-a586-c6bf0ce36f74", + "b89324bf-daf2-422a-85f2-6843b9c09b6a", + "779769d1-0aee-4dba-82c7-9e1044836d75" + }; + + private readonly string[] _dateTimes = new[] { + "2017-04-17", + "2018-05-25", + "2019-03-31", + "2022-02-06", + }; + + private readonly ILogger _logger; + + public ReceiverWithCancellationTokenTestHub(ILogger logger) + { + _logger = logger; + } + + public async Task Start() + { + _logger.Log(LogLevel.Information, "ReceiverTestHub.Start"); + + for (int i = 0; i < 17; i++) + { + await Task.Delay(TimeSpan.FromMilliseconds(15)); + await this.Clients.Caller.Notify(this.Context.ConnectionAborted); + } + + for (int i = 0; i < _message.Length; i++) + { + await Task.Delay(TimeSpan.FromMilliseconds(15)); + await this.Clients.Caller.ReceiveMessage(_message[i], i, this.Context.ConnectionAborted); + } + + for (int i = 0; i < _guids.Length; i++) + { + await Task.Delay(TimeSpan.FromMilliseconds(15)); + await this.Clients.Caller.ReceiveCustomMessage(new UserDefinedType() { Guid = Guid.Parse(_guids[i]), DateTime = DateTime.Parse(_dateTimes[i]) }, this.Context.ConnectionAborted); + } + } +} diff --git a/tests/TypedSignalR.Client.Tests.Server/Program.cs b/tests/TypedSignalR.Client.Tests.Server/Program.cs index bd977cc..d85b15b 100644 --- a/tests/TypedSignalR.Client.Tests.Server/Program.cs +++ b/tests/TypedSignalR.Client.Tests.Server/Program.cs @@ -25,6 +25,7 @@ app.MapHub("/Hubs/UnaryHub"); app.MapHub("/Hubs/SideEffectHub"); app.MapHub("/Hubs/ReceiverTestHub"); +app.MapHub("/Hubs/ReceiverWithCancellationTokenTestHub"); app.MapHub("/Hubs/StreamingHub"); app.MapHub("/Hubs/ClientResultsTestHub"); app.MapHub("/Hubs/InheritTestHub"); diff --git a/tests/TypedSignalR.Client.Tests.Shared/IReceiver.cs b/tests/TypedSignalR.Client.Tests.Shared/IReceiver.cs index 844c269..8e6958b 100644 --- a/tests/TypedSignalR.Client.Tests.Shared/IReceiver.cs +++ b/tests/TypedSignalR.Client.Tests.Shared/IReceiver.cs @@ -13,6 +13,14 @@ public interface IReceiver Task ReceiveCustomMessage(UserDefinedType userDefined); } + +public interface IReceiverWithCancellationToken +{ + Task ReceiveMessage(string message, int value, CancellationToken cancellationToken); + Task Notify(CancellationToken cancellationToken); + Task ReceiveCustomMessage(UserDefinedType userDefined, CancellationToken cancellationToken); +} + public interface IReceiverTestHub { Task Start(); diff --git a/tests/TypedSignalR.Client.Tests/Hubs/ReceiverWithCancellationTokenTest.cs b/tests/TypedSignalR.Client.Tests/Hubs/ReceiverWithCancellationTokenTest.cs new file mode 100644 index 0000000..83ebda3 --- /dev/null +++ b/tests/TypedSignalR.Client.Tests/Hubs/ReceiverWithCancellationTokenTest.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.SignalR.Client; +using TypedSignalR.Client.Tests.Shared; +using Xunit; +using Xunit.Abstractions; + +namespace TypedSignalR.Client.Tests.Hubs; + +public class ReceiverWithCancellationTokenTest : IntegrationTestBase, IAsyncLifetime, IReceiverWithCancellationToken +{ + private readonly HubConnection _connection; + private readonly IReceiverTestHub _receiverTestHub; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly ITestOutputHelper _output; + + private int _notifyCallCount; + private readonly List<(string, int)> _receiveMessage = new(); + private readonly List _userDefinedList = new(); + + public ReceiverWithCancellationTokenTest(ITestOutputHelper output) + { + _output = output; + + _connection = CreateHubConnection("/Hubs/ReceiverWithCancellationTokenTestHub", HttpTransportType.WebSockets); + + _receiverTestHub = _connection.CreateHubProxy(_cancellationTokenSource.Token); + _connection.Register(this); + } + + public async Task InitializeAsync() + { + await _connection.StartAsync(_cancellationTokenSource.Token); + } + + public async Task DisposeAsync() + { + try + { + await _connection.StopAsync(_cancellationTokenSource.Token); + } + finally + { + _cancellationTokenSource.Cancel(); + } + } + + private readonly string[] _answerMessages = new[] { + "b1f7cd73-13b8-49bd-9557-ffb38859d18b", + "3f5c3585-d01b-4f8f-8139-62a1241850e2", + "92021a22-5823-4501-8cbd-c20d4ca6e54c", + "5b134f73-2dc1-4271-8316-1a4250f42241", + "e73acd30-e034-4569-8f30-88ac34b99052", + "0d7531b5-0a36-4fe7-bbe5-8fee38c38c07", + "32915627-3df6-41dc-8d30-7c655c2f7e61", + "c875a6f9-9ddb-440b-a7e4-6e893f59ab9e", + }; + + private readonly string[] _guids = new[] { + "b2f626e5-b4d4-4713-891d-f6cb107e502e", + "22733524-2087-4701-a586-c6bf0ce36f74", + "b89324bf-daf2-422a-85f2-6843b9c09b6a", + "779769d1-0aee-4dba-82c7-9e1044836d75" + }; + + private readonly string[] _dateTimes = new[] { + "2017-04-17", + "2018-05-25", + "2019-03-31", + "2022-02-06", + }; + + [Fact] + public async Task TestReceiver() + { + await _receiverTestHub.Start(); + + _output.WriteLine($"_notifyCallCount: {_notifyCallCount}"); + + Assert.Equal(17, _notifyCallCount); + + for (int i = 0; i < _receiveMessage.Count; i++) + { + _output.WriteLine($"_receiveMessage[{i}].Item1: {_receiveMessage[i].Item1}"); + _output.WriteLine($"_receiveMessage[{i}].Item2: {_receiveMessage[i].Item2}"); + + Assert.Equal(_receiveMessage[i].Item1, _answerMessages[i]); + Assert.Equal(_receiveMessage[i].Item2, i); + } + + for (int i = 0; i < _userDefinedList.Count; i++) + { + _output.WriteLine($"_userDefinedList[{i}].Guid: {_userDefinedList[i].Guid}"); + _output.WriteLine($"_userDefinedList[{i}].DateTime: {_userDefinedList[i].DateTime}"); + + var guid = Guid.Parse(_guids[i]); + var dateTime = DateTime.Parse(_dateTimes[i]); + + Assert.Equal(_userDefinedList[i].Guid, guid); + Assert.Equal(_userDefinedList[i].DateTime, dateTime); + } + } + + Task IReceiverWithCancellationToken.ReceiveMessage(string message, int value, CancellationToken cancellationToken) + { + _receiveMessage.Add((message, value)); + + return Task.CompletedTask; + } + + Task IReceiverWithCancellationToken.Notify(CancellationToken cancellationToken) + { + _notifyCallCount++; + + return Task.CompletedTask; + } + + Task IReceiverWithCancellationToken.ReceiveCustomMessage(UserDefinedType userDefined, CancellationToken cancellationToken) + { + _userDefinedList.Add(userDefined); + + return Task.CompletedTask; + } +}