From 4bb9721d39632641294975370e605920ce1882d9 Mon Sep 17 00:00:00 2001 From: Pawel Kadluczka Date: Thu, 18 May 2017 14:10:20 -0700 Subject: [PATCH] Adding strongly typed Invoke and On overloads See: #311 --- samples/ClientSample/HubSample.cs | 6 +- .../HubConnection.cs | 6 - .../HubConnectionExtensions.cs | 145 ++++++++++++++ .../HubConnectionTests.cs | 9 +- .../HubConnectionExtensionsTests.cs | 186 ++++++++++++++++++ .../HubConnectionProtocolTests.cs | 18 +- .../HubConnectionTests.cs | 6 +- .../TestConnection.cs | 5 +- 8 files changed, 351 insertions(+), 30 deletions(-) create mode 100644 src/Microsoft.AspNetCore.SignalR.Client/HubConnectionExtensions.cs create mode 100644 test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionExtensionsTests.cs diff --git a/samples/ClientSample/HubSample.cs b/samples/ClientSample/HubSample.cs index a277899d00..2b9f885a17 100644 --- a/samples/ClientSample/HubSample.cs +++ b/samples/ClientSample/HubSample.cs @@ -47,11 +47,7 @@ namespace ClientSample }; // Set up handler - connection.On("Send", new[] { typeof(string) }, a => - { - var message = (string)a[0]; - Console.WriteLine(message); - }); + connection.On("Send", Console.WriteLine); while (!cts.Token.IsCancellationRequested) { diff --git a/src/Microsoft.AspNetCore.SignalR.Client/HubConnection.cs b/src/Microsoft.AspNetCore.SignalR.Client/HubConnection.cs index fa7bad1615..490b3b28a1 100644 --- a/src/Microsoft.AspNetCore.SignalR.Client/HubConnection.cs +++ b/src/Microsoft.AspNetCore.SignalR.Client/HubConnection.cs @@ -109,18 +109,12 @@ namespace Microsoft.AspNetCore.SignalR.Client } // TODO: Client return values/tasks? - // TODO: Overloads for void hub methods - // TODO: Overloads that use type parameters (like On, On, etc.) public void On(string methodName, Type[] parameterTypes, Action handler) { var invocationHandler = new InvocationHandler(parameterTypes, handler); _handlers.AddOrUpdate(methodName, invocationHandler, (_, __) => invocationHandler); } - public Task Invoke(string methodName, params object[] args) => Invoke(methodName, CancellationToken.None, args); - public async Task Invoke(string methodName, CancellationToken cancellationToken, params object[] args) => ((T)(await Invoke(methodName, typeof(T), cancellationToken, args))); - - public Task Invoke(string methodName, Type returnType, params object[] args) => Invoke(methodName, returnType, CancellationToken.None, args); public async Task Invoke(string methodName, Type returnType, CancellationToken cancellationToken, params object[] args) { ThrowIfConnectionTerminated(); diff --git a/src/Microsoft.AspNetCore.SignalR.Client/HubConnectionExtensions.cs b/src/Microsoft.AspNetCore.SignalR.Client/HubConnectionExtensions.cs new file mode 100644 index 0000000000..b892fc7488 --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Client/HubConnectionExtensions.cs @@ -0,0 +1,145 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.SignalR.Client +{ + public static class HubConnectionExtensions + { + public static Task Invoke(this HubConnection hubConnection, string methodName, params object[] args) => + Invoke(hubConnection, methodName, CancellationToken.None, args); + + public static Task Invoke(this HubConnection hubConnection, string methodName, CancellationToken cancellationToken, params object[] args) + { + if (hubConnection == null) + { + throw new ArgumentNullException(nameof(hubConnection)); + } + + return hubConnection.Invoke(methodName, typeof(object), cancellationToken, args); + } + + public static Task Invoke(this HubConnection hubConnection, string methodName, params object[] args) => + Invoke(hubConnection, methodName, CancellationToken.None, args); + + public async static Task Invoke(this HubConnection hubConnection, string methodName, CancellationToken cancellationToken, params object[] args) + { + if (hubConnection == null) + { + throw new ArgumentNullException(nameof(hubConnection)); + } + + return (TResult)await hubConnection.Invoke(methodName, typeof(TResult), cancellationToken, args); + } + + public static void On(this HubConnection hubConnection, string methodName, Action handler) + { + if (hubConnection == null) + { + throw new ArgumentNullException(nameof(hubConnection)); + } + + hubConnection.On(methodName, Type.EmptyTypes, args => handler()); + } + + public static void On(this HubConnection hubConnection, string methodName, Action handler) + { + if (hubConnection == null) + { + throw new ArgumentNullException(nameof(hubConnection)); + } + + hubConnection.On(methodName, + new[] { typeof(T1) }, + args => handler((T1)args[0])); + } + + public static void On(this HubConnection hubConnection, string methodName, Action handler) + { + if (hubConnection == null) + { + throw new ArgumentNullException(nameof(hubConnection)); + } + + hubConnection.On(methodName, + new[] { typeof(T1), typeof(T2) }, + args => handler((T1)args[0], (T2)args[1])); + } + + public static void On(this HubConnection hubConnection, string methodName, Action handler) + { + if (hubConnection == null) + { + throw new ArgumentNullException(nameof(hubConnection)); + } + + hubConnection.On(methodName, + new[] { typeof(T1), typeof(T2), typeof(T3) }, + args => handler((T1)args[0], (T2)args[1], (T3)args[2])); + } + + public static void On(this HubConnection hubConnection, string methodName, Action handler) + { + if (hubConnection == null) + { + throw new ArgumentNullException(nameof(hubConnection)); + } + + hubConnection.On(methodName, + new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4) }, + args => handler((T1)args[0], (T2)args[1], (T3)args[2], (T4)args[3])); + } + + public static void On(this HubConnection hubConnection, string methodName, Action handler) + { + if (hubConnection == null) + { + throw new ArgumentNullException(nameof(hubConnection)); + } + + hubConnection.On(methodName, + new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5) }, + args => handler((T1)args[0], (T2)args[1], (T3)args[2], (T4)args[3], (T5)args[4])); + } + + public static void On(this HubConnection hubConnection, string methodName, Action handler) + { + if (hubConnection == null) + { + throw new ArgumentNullException(nameof(hubConnection)); + } + + hubConnection.On(methodName, + new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6) }, + args => handler((T1)args[0], (T2)args[1], (T3)args[2], (T4)args[3], (T5)args[4], (T6)args[5])); + } + + public static void On(this HubConnection hubConnection, string methodName, Action handler) + { + if (hubConnection == null) + { + throw new ArgumentNullException(nameof(hubConnection)); + } + + hubConnection.On(methodName, + new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7) }, + args => handler((T1)args[0], (T2)args[1], (T3)args[2], (T4)args[3], (T5)args[4], (T6)args[5], (T7)args[6])); + } + + + public static void On(this HubConnection hubConnection, string methodName, Action handler) + { + if (hubConnection == null) + { + throw new ArgumentNullException(nameof(hubConnection)); + } + + hubConnection.On(methodName, + new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8) }, + args => handler((T1)args[0], (T2)args[1], (T3)args[2], (T4)args[3], (T5)args[4], (T6)args[5], (T7)args[6], (T8)args[7])); + } + } +} diff --git a/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/HubConnectionTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/HubConnectionTests.cs index 68c7aaa065..161eb75604 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/HubConnectionTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/HubConnectionTests.cs @@ -128,12 +128,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests await connection.StartAsync(TransportType.LongPolling, httpClient); var tcs = new TaskCompletionSource(); - connection.On("Echo", new[] { typeof(string) }, a => - { - tcs.TrySetResult((string)a[0]); - }); + connection.On("Echo", tcs.SetResult); - await connection.Invoke("CallEcho", originalMessage).OrTimeout(); + await connection.Invoke("CallEcho", originalMessage).OrTimeout(); Assert.Equal(originalMessage, await tcs.Task.OrTimeout()); } @@ -157,7 +154,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests await connection.StartAsync(TransportType.LongPolling, httpClient); var ex = await Assert.ThrowsAnyAsync( - async () => await connection.Invoke("!@#$%")); + async () => await connection.Invoke("!@#$%")); Assert.Equal("Unknown hub method '!@#$%'", ex.Message); } diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionExtensionsTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionExtensionsTests.cs new file mode 100644 index 0000000000..703b3b6c80 --- /dev/null +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionExtensionsTests.cs @@ -0,0 +1,186 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Internal.Protocol; +using Microsoft.AspNetCore.SignalR.Tests.Common; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.SignalR.Client.Tests +{ + public class HubConnectionExtensionsTests + { + [Fact] + public async Task On() + { + await InvokeOn( + (hubConnection, tcs) => hubConnection.On("Foo", + () => tcs.SetResult(new object[0])), + new object[0]); + } + + [Fact] + public async Task OnT1() + { + await InvokeOn( + (hubConnection, tcs) => hubConnection.On("Foo", + r => tcs.SetResult(new object[] { r })), + new object[] { 42 }); + } + + [Fact] + public async Task OnT2() + { + await InvokeOn( + (hubConnection, tcs) => hubConnection.On("Foo", + (r1, r2) => tcs.SetResult(new object[] { r1, r2 })), + new object[] { 42, "abc" }); + } + + [Fact] + public async Task OnT3() + { + await InvokeOn( + (hubConnection, tcs) => hubConnection.On("Foo", + (r1, r2, r3) => tcs.SetResult(new object[] { r1, r2, r3 })), + new object[] { 42, "abc", 24.0f }); + } + + [Fact] + public async Task OnT4() + { + await InvokeOn( + (hubConnection, tcs) => hubConnection.On("Foo", + (r1, r2, r3, r4) => tcs.SetResult(new object[] { r1, r2, r3, r4 })), + new object[] { 42, "abc", 24.0f, 10d }); + } + + [Fact] + public async Task OnT5() + { + await InvokeOn( + (hubConnection, tcs) => hubConnection.On("Foo", + (r1, r2, r3, r4, r5) => tcs.SetResult(new object[] { r1, r2, r3, r4, r5 })), + new object[] { 42, "abc", 24.0f, 10d, "123" }); + } + + [Fact] + public async Task OnT6() + { + await InvokeOn( + (hubConnection, tcs) => hubConnection.On("Foo", + (r1, r2, r3, r4, r5, r6) => tcs.SetResult(new object[] { r1, r2, r3, r4, r5, r6 })), + new object[] { 42, "abc", 24.0f, 10d, "123", 24 }); + } + + [Fact] + public async Task OnT7() + { + await InvokeOn( + (hubConnection, tcs) => hubConnection.On("Foo", + (r1, r2, r3, r4, r5, r6, r7) => tcs.SetResult(new object[] { r1, r2, r3, r4, r5, r6, r7 })), + new object[] { 42, "abc", 24.0f, 10d, "123", 24, 'c' }); + } + + [Fact] + public async Task OnT8() + { + await InvokeOn( + (hubConnection, tcs) => hubConnection.On("Foo", + (r1, r2, r3, r4, r5, r6, r7, r8) => tcs.SetResult(new object[] { r1, r2, r3, r4, r5, r6, r7, r8 })), + new object[] { 42, "abc", 24.0f, 10d, "123", 24, 'c', "XYZ" }); + } + + private async Task InvokeOn(Action> onAction, object[] args) + { + var connection = new TestConnection(); + var hubConnection = new HubConnection(connection, new JsonHubProtocol(new JsonSerializer()), new LoggerFactory()); + var handlerTcs = new TaskCompletionSource(); + try + { + onAction(hubConnection, handlerTcs); + await hubConnection.StartAsync(); + + await connection.ReceiveJsonMessage( + new + { + invocationId = "1", + type = 1, + target = "Foo", + arguments = args + }).OrTimeout(); + + var result = await handlerTcs.Task.OrTimeout(); + } + finally + { + await hubConnection.DisposeAsync().OrTimeout(); + await connection.DisposeAsync().OrTimeout(); + } + } + + [Fact] + public async Task ConnectionClosedOnCallbackArgumentCountMismatch() + { + var connection = new TestConnection(); + var hubConnection = new HubConnection(connection, new JsonHubProtocol(new JsonSerializer()), new LoggerFactory()); + var closeTcs = new TaskCompletionSource(); + hubConnection.Closed += e => closeTcs.TrySetException(e); + try + { + hubConnection.On("Foo", r => { }); + await hubConnection.StartAsync(); + + await connection.ReceiveJsonMessage( + new + { + invocationId = "1", + type = 1, + target = "Foo", + arguments = new object[] { 42, "42" } + }).OrTimeout(); + + var ex = await Assert.ThrowsAsync(async () => await closeTcs.Task.OrTimeout()); + Assert.Equal("Invocation provides 2 argument(s) but target expects 1.", ex.Message); + } + finally + { + await hubConnection.DisposeAsync().OrTimeout(); + await connection.DisposeAsync().OrTimeout(); + } + } + + [Fact] + public async Task ConnectionClosedOnCallbackArgumentTypeMismatch() + { + var connection = new TestConnection(); + var hubConnection = new HubConnection(connection, new JsonHubProtocol(new JsonSerializer()), new LoggerFactory()); + var closeTcs = new TaskCompletionSource(); + hubConnection.Closed += e => closeTcs.TrySetException(e); + try + { + hubConnection.On("Foo", r => { }); + await hubConnection.StartAsync(); + + await connection.ReceiveJsonMessage( + new + { + invocationId = "1", + type = 1, + target = "Foo", + arguments = new object[] { "xxx" } + }).OrTimeout(); + + var ex = await Assert.ThrowsAsync(async () => await closeTcs.Task.OrTimeout()); + } + finally + { + await hubConnection.DisposeAsync().OrTimeout(); + await connection.DisposeAsync().OrTimeout(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs index de53017a80..563d6a553e 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs @@ -1,3 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Internal.Protocol; @@ -22,7 +25,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests { await hubConnection.StartAsync(); - var invokeTask = hubConnection.Invoke("Foo", typeof(void)); + var invokeTask = hubConnection.Invoke("Foo"); var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout(); @@ -44,7 +47,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests { await hubConnection.StartAsync(); - var invokeTask = hubConnection.Invoke("Foo", typeof(void)); + var invokeTask = hubConnection.Invoke("Foo"); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout(); @@ -136,15 +139,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests { await hubConnection.StartAsync(); - hubConnection.On("Foo", new[] { typeof(int), typeof(string), typeof(float) }, (a) => handlerCalled.TrySetResult(a)); + hubConnection.On("Foo", (r1, r2, r3) => handlerCalled.TrySetResult(new object[] { r1, r2, r3 })); - await connection.ReceiveJsonMessage(new { invocationId = "1", type = 1, target = "Foo", arguments = new object[] { 1, "Foo", 2.0f } }).OrTimeout(); + var args = new object[] { 1, "Foo", 2.0f }; + await connection.ReceiveJsonMessage(new { invocationId = "1", type = 1, target = "Foo", arguments = args }).OrTimeout(); - var results = await handlerCalled.Task.OrTimeout(); - Assert.Equal(3, results.Length); - Assert.Equal(1, results[0]); - Assert.Equal("Foo", results[1]); - Assert.Equal(2.0f, results[2]); + Assert.Equal(args, await handlerCalled.Task.OrTimeout()); } finally { diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs index 0755c598e4..ba81e34365 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs @@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests await hubConnection.StartAsync(new TestTransportFactory(Mock.Of()), httpClient: null); await hubConnection.DisposeAsync(); var exception = await Assert.ThrowsAsync( - async () => await hubConnection.Invoke("test", typeof(int))); + async () => await hubConnection.Invoke("test")); Assert.Equal("Connection has been terminated.", exception.Message); } @@ -216,7 +216,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var hubConnection = new HubConnection(mockConnection.Object, new LoggerFactory()); await hubConnection.StartAsync(new TestTransportFactory(Mock.Of()), httpClient: null); - var invokeTask = hubConnection.Invoke("testMethod", typeof(int)); + var invokeTask = hubConnection.Invoke("testMethod"); await hubConnection.DisposeAsync(); await Assert.ThrowsAsync(async () => await invokeTask); @@ -235,7 +235,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var hubConnection = new HubConnection(mockConnection.Object, new LoggerFactory()); await hubConnection.StartAsync(new TestTransportFactory(Mock.Of()), httpClient: null); - var invokeTask = hubConnection.Invoke("testMethod", typeof(int)); + var invokeTask = hubConnection.Invoke("testMethod"); await hubConnection.DisposeAsync(); var thrown = await Assert.ThrowsAsync(exception.GetType(), async () => await invokeTask); diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs index 8a5eb49675..5eaacddd65 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; using System.Net.Http; using System.Text; using System.Threading;