diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props index 863dbb07c53d0..316b9acf56c85 100644 --- a/eng/Packages.Data.props +++ b/eng/Packages.Data.props @@ -115,9 +115,9 @@ - - - + + + diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/api/Microsoft.Azure.WebJobs.Extensions.SignalRService.netcoreapp3.1.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/api/Microsoft.Azure.WebJobs.Extensions.SignalRService.netcoreapp3.1.cs index e94661c14f5b1..b09ecb6dae887 100644 --- a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/api/Microsoft.Azure.WebJobs.Extensions.SignalRService.netcoreapp3.1.cs +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/api/Microsoft.Azure.WebJobs.Extensions.SignalRService.netcoreapp3.1.cs @@ -112,6 +112,16 @@ public SignalRConnectionAttribute(string connectionStringSetting) { } public string Connection { get { throw null; } set { } } } } + public abstract partial class ServerlessHub where T : class + { + protected ServerlessHub(Microsoft.Azure.SignalR.Management.ServiceHubContext serviceHubContext = null) { } + public Microsoft.Azure.SignalR.Management.ClientManager ClientManager { get { throw null; } } + public Microsoft.AspNetCore.SignalR.IHubClients Clients { get { throw null; } } + public Microsoft.Azure.SignalR.Management.GroupManager Groups { get { throw null; } } + public Microsoft.Azure.SignalR.Management.UserGroupManager UserGroups { get { throw null; } } + protected static System.Collections.Generic.IList GetClaims(string jwt) { throw null; } + protected System.Threading.Tasks.Task NegotiateAsync(Microsoft.Azure.SignalR.Management.NegotiationOptions options) { throw null; } + } public partial class SignalRAsyncCollector : Microsoft.Azure.WebJobs.IAsyncCollector { internal SignalRAsyncCollector() { } diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/api/Microsoft.Azure.WebJobs.Extensions.SignalRService.netstandard2.0.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/api/Microsoft.Azure.WebJobs.Extensions.SignalRService.netstandard2.0.cs index e94661c14f5b1..b09ecb6dae887 100644 --- a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/api/Microsoft.Azure.WebJobs.Extensions.SignalRService.netstandard2.0.cs +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/api/Microsoft.Azure.WebJobs.Extensions.SignalRService.netstandard2.0.cs @@ -112,6 +112,16 @@ public SignalRConnectionAttribute(string connectionStringSetting) { } public string Connection { get { throw null; } set { } } } } + public abstract partial class ServerlessHub where T : class + { + protected ServerlessHub(Microsoft.Azure.SignalR.Management.ServiceHubContext serviceHubContext = null) { } + public Microsoft.Azure.SignalR.Management.ClientManager ClientManager { get { throw null; } } + public Microsoft.AspNetCore.SignalR.IHubClients Clients { get { throw null; } } + public Microsoft.Azure.SignalR.Management.GroupManager Groups { get { throw null; } } + public Microsoft.Azure.SignalR.Management.UserGroupManager UserGroups { get { throw null; } } + protected static System.Collections.Generic.IList GetClaims(string jwt) { throw null; } + protected System.Threading.Tasks.Task NegotiateAsync(Microsoft.Azure.SignalR.Management.NegotiationOptions options) { throw null; } + } public partial class SignalRAsyncCollector : Microsoft.Azure.WebJobs.IAsyncCollector { internal SignalRAsyncCollector() { } diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/samples/Sample01_StronglyTypedHub.md b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/samples/Sample01_StronglyTypedHub.md new file mode 100644 index 0000000000000..59386bf7fa6df --- /dev/null +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/samples/Sample01_StronglyTypedHub.md @@ -0,0 +1,35 @@ +# Strongly Typed Serverless Hub + +Strongly typed serverless hub is a programming model which allows you to define your SignalR client methods in an interface, and the RPC implementation will be done by SignalR. + +This sample demonstrates how to create a strongly typed serverless hub and invoke SignalR client methods in it. To see more details on serverless hub, please go [here](https://docs.microsoft.com/azure/azure-signalr/signalr-concept-serverless-development-config#class-based-model). + +## Define a strongly typed serverless hub class + +Let's say you want to invoke a SignalR client method `ReceiveMessage` with a string parameter when a HTTP request comes. + +Firstly you need to define an interface for the client method. + +```C# Snippet:StronglyTypedHub_ClientMethodInterface +public interface IChatClient +{ + Task ReceiveMessage(string message); +} +``` + +Then you creates a strongly typed hub with the interface: + +```C# Snippet:StronglyTypedHub +public class StronglyTypedHub : ServerlessHub +{ + [FunctionName(nameof(Broadcast))] + public async Task Broadcast([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest _, string message) + { + await Clients.All.ReceiveMessage(message); + } +} +``` + +## Call client methods + +The code snippet above defines a method in the hub, which broadcasts the message to all the clients once triggered by HTTP request. diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/IInternalServiceHubContextStore.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/IInternalServiceHubContextStore.cs index 5de4a549f9130..446644bd8469d 100644 --- a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/IInternalServiceHubContextStore.cs +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/IInternalServiceHubContextStore.cs @@ -1,12 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Threading.Tasks; using Microsoft.Azure.SignalR; +using Microsoft.Azure.SignalR.Management; namespace Microsoft.Azure.WebJobs.Extensions.SignalRService { internal interface IInternalServiceHubContextStore : IServiceHubContextStore { AccessKey[] AccessKeys { get; } + + ValueTask> GetAsync(string hubName) where T : class; } } \ No newline at end of file diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/ServiceHubContextStore.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/ServiceHubContextStore.cs index 9d52e6809bdb1..f960a9a9aecb6 100644 --- a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/ServiceHubContextStore.cs +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/ServiceHubContextStore.cs @@ -13,16 +13,26 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService internal class ServiceHubContextStore : IInternalServiceHubContextStore { private readonly ConcurrentDictionary> Lazy, IServiceHubContext Value)> _store = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary>> _stronglyTypedStore = new(StringComparer.OrdinalIgnoreCase); private readonly IServiceEndpointManager _endpointManager; - - public IServiceManager ServiceManager { get; } + private readonly ServiceManager _serviceManager; public AccessKey[] AccessKeys => _endpointManager.Endpoints.Keys.Select(endpoint => endpoint.AccessKey).ToArray(); - public ServiceHubContextStore(IServiceEndpointManager endpointManager, IServiceManager serviceManager) + public IServiceManager ServiceManager => _serviceManager as IServiceManager; + + public ServiceHubContextStore(IServiceEndpointManager endpointManager, ServiceManager serviceManager) { + _serviceManager = serviceManager; _endpointManager = endpointManager; - ServiceManager = serviceManager; + } + + public async ValueTask> GetAsync(string hubName) where T : class + { + // The GetAsync for strongly typed hub is more simple than that for weak typed hub, as it removes codes to handle transient errors. The creation of service hub context should not contain transient errors. + var lazy = _stronglyTypedStore.GetOrAdd(hubName, new Lazy>(async () => await _serviceManager.CreateHubContextAsync(hubName, default).ConfigureAwait(false))); + var hubContext = await lazy.Value.ConfigureAwait(false); + return (ServiceHubContext)hubContext; } public ValueTask GetAsync(string hubName) @@ -55,6 +65,7 @@ private async Task GetFromLazyAsync(string hubName, (Lazy (ServiceManager)sp.GetService()) .AddSingleton(_loggerFactory) .AddSingleton(); if (_router != null) diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Microsoft.Azure.WebJobs.Extensions.SignalRService.csproj b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Microsoft.Azure.WebJobs.Extensions.SignalRService.csproj index 2d8042c5bcd34..dc4430a2c647f 100644 --- a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Microsoft.Azure.WebJobs.Extensions.SignalRService.csproj +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Microsoft.Azure.WebJobs.Extensions.SignalRService.csproj @@ -6,7 +6,7 @@ 1.7.0-beta.1 true true - $(NoWarn);CS1591;AZC0001 + $(NoWarn);CS1591;AZC0001;AZC0107; @@ -18,7 +18,7 @@ - + diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/ServerlessHubOfT.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/ServerlessHubOfT.cs new file mode 100644 index 0000000000000..dfc1f1ecb585e --- /dev/null +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/ServerlessHubOfT.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Azure.Core.Pipeline; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Azure.SignalR.Management; + +namespace Microsoft.Azure.WebJobs.Extensions.SignalRService +{ + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "https://github.com/Azure/azure-sdk-for-net/issues/17164")] + public abstract class ServerlessHub where T : class + { + private static readonly Lazy JwtSecurityTokenHandler = new(() => new JwtSecurityTokenHandler()); + private readonly ServiceHubContext _hubContext; + + /// + /// Gets an object that can be used to invoke methods on the clients connected to this hub. + /// + public IHubClients Clients => _hubContext.Clients; + + /// + /// Get the group manager of this hub. + /// + public GroupManager Groups => _hubContext.Groups; + + /// + /// Get the user group manager of this hub. + /// + public UserGroupManager UserGroups => _hubContext.UserGroups; + + /// + /// Get the client manager of this hub. + /// + public ClientManager ClientManager => _hubContext.ClientManager; + + protected ServerlessHub(ServiceHubContext serviceHubContext = null) + { + if (serviceHubContext is null) + { + serviceHubContext = ((IInternalServiceHubContextStore)StaticServiceHubContextStore.Get()).GetAsync(GetType().Name).Result; + } + _hubContext = serviceHubContext; + } + + /// + /// Gets client endpoint access information object for SignalR hub connections to connect to Azure SignalR Service + /// + protected async Task NegotiateAsync(NegotiationOptions options) + { + var negotiateResponse = await _hubContext.NegotiateAsync(options).ConfigureAwait(false); + return new SignalRConnectionInfo + { + Url = negotiateResponse.Url, + AccessToken = negotiateResponse.AccessToken + }; + } + + /// + /// Get claim list from a JWT. + /// + protected static IList GetClaims(string jwt) + { + if (jwt.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + jwt = jwt.Substring("Bearer ".Length).Trim(); + } + return JwtSecurityTokenHandler.Value.ReadJwtToken(jwt).Claims.ToList(); + } + } +} diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/SignalRTriggerBindingProvider.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/SignalRTriggerBindingProvider.cs index bf3910071d2e9..2843d9f757a6e 100644 --- a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/SignalRTriggerBindingProvider.cs +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/SignalRTriggerBindingProvider.cs @@ -75,7 +75,7 @@ internal SignalRTriggerAttribute GetParameterResolvedAttribute(SignalRTriggerAtt var declaredType = method.DeclaringType; string[] parameterNamesFromAttribute; - if (declaredType != null && declaredType.IsSubclassOf(typeof(ServerlessHub))) + if (declaredType != null && IsServerlessHub(declaredType)) { // Class based model if (!string.IsNullOrEmpty(hubName) || @@ -195,5 +195,27 @@ private static bool HasBindingAttribute(IEnumerable attributes) { return attributes.Any(attribute => attribute.GetType().GetCustomAttribute(false) != null); } + + private static bool IsServerlessHub(Type type) + { + if (type == null) + { + return false; + } + if (type.IsSubclassOf(typeof(ServerlessHub))) + { + return true; + } + var baseType = type.BaseType; + while (baseType != null) + { + if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(ServerlessHub<>)) + { + return true; + } + baseType = baseType.BaseType; + } + return false; + } } } \ No newline at end of file diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Samples/StronglyTypedHub.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Samples/StronglyTypedHub.cs new file mode 100644 index 0000000000000..2f031dc6c340e --- /dev/null +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Samples/StronglyTypedHub.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.WebJobs.Extensions.Http; + +namespace Microsoft.Azure.WebJobs.Extensions.SignalRService.Tests.Samples +{ + #region Snippet:StronglyTypedHub + public class StronglyTypedHub : ServerlessHub + { + [FunctionName(nameof(Broadcast))] + public async Task Broadcast([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest _, string message) + { + await Clients.All.ReceiveMessage(message); + } + } + #endregion + + #region Snippet:StronglyTypedHub_ClientMethodInterface + public interface IChatClient + { + Task ReceiveMessage(string message); + } + #endregion +} \ No newline at end of file diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/IChatClient.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/IChatClient.cs new file mode 100644 index 0000000000000..175186d0d04b5 --- /dev/null +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/IChatClient.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.SignalRService.Tests +{ + public interface IChatClient + { + Task ReceiveMessage(string message); + } +} diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/StronglyTypedHubContextStoreTests.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/StronglyTypedHubContextStoreTests.cs new file mode 100644 index 0000000000000..bb69a5fba4681 --- /dev/null +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/StronglyTypedHubContextStoreTests.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using Microsoft.Azure.SignalR.Management; +using Microsoft.Azure.SignalR.Tests.Common; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Extensions.SignalRService.Tests.Trigger.StronglyTypedHub +{ + public class StronglyTypedHubContextStoreTests + { + [Fact] + public void GetStronglyTypedHubContextFact() + { + var serviceManager = new ServiceManagerBuilder().WithOptions(o => o.ConnectionString = FakeEndpointUtils.GetFakeConnectionString(1).Single()).BuildServiceManager(); + var hubContext = new ServiceHubContextStore(null, serviceManager).GetAsync(GetType().Name).Result; + } + } +} diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/StronglyTypedServerlessHubTests.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/StronglyTypedServerlessHubTests.cs new file mode 100644 index 0000000000000..0f84f05628fe8 --- /dev/null +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/StronglyTypedServerlessHubTests.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.SignalR.Management; +using Microsoft.Azure.SignalR.Tests.Common; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Extensions.SignalRService.Tests +{ + public class StronglyTypedServerlessHubTests + { + [Fact] + public async Task NegotiateAsync() + { + var serviceManager = new ServiceManagerBuilder().WithOptions(o => o.ConnectionString = FakeEndpointUtils.GetFakeConnectionString(1).Single()).BuildServiceManager(); + var hubContext = await serviceManager.CreateHubContextAsync("hubName", default); + var myHub = new TestStronglyTypedHub(hubContext); + var connectionInfo = await myHub.Negotiate("user"); + Assert.NotNull(connectionInfo); + } + } +} diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/TestStronglyTypedHub.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/TestStronglyTypedHub.cs new file mode 100644 index 0000000000000..c1e678c804e66 --- /dev/null +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/TestStronglyTypedHub.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.SignalR.Management; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.WebJobs.Extensions.SignalRService.Tests +{ + public class TestStronglyTypedHub : ServerlessHub + { + public TestStronglyTypedHub(ServiceHubContext serviceHubContext) : base(serviceHubContext) { } + + [FunctionName("negotiate")] + public Task Negotiate(string userId) + { + return NegotiateAsync(new() { UserId = userId }); + } + + [FunctionName(nameof(Broadcast))] + public async Task Broadcast([SignalRTrigger] InvocationContext invocationContext, string message) + { + await Clients.All.ReceiveMessage(message); + } + + internal void TestFunction([SignalRTrigger] InvocationContext context, string arg0, int arg1) + { + } + + internal void TestFunctionWithIgnore([SignalRTrigger] InvocationContext context, string arg0, int arg1, [SignalRIgnore] int arg2) + { + } + + internal void TestFunctionWithSpecificType([SignalRTrigger] InvocationContext context, string arg0, int arg1, ILogger logger, CancellationToken token) + { + } + + internal void OnConnected([SignalRTrigger] InvocationContext context, string arg0, int arg1) + { + } + + internal void OnDisconnected([SignalRTrigger] InvocationContext context, string arg0, int arg1) + { + } + } +} diff --git a/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/TriggerProviderForStronglyTypedHubTests.cs b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/TriggerProviderForStronglyTypedHubTests.cs new file mode 100644 index 0000000000000..f17716073ff2c --- /dev/null +++ b/sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/tests/Trigger/StronglyTypedHub/TriggerProviderForStronglyTypedHubTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Reflection; +using Microsoft.Azure.WebJobs.Host.Triggers; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using SignalRServiceExtension.Tests.Utils; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Extensions.SignalRService.Tests +{ + public class TriggerProviderForStronglyTypedHubTests + { + private const string CustomConnectionStringSetting = "ConnectionStringSetting"; + + [Fact] + public void ResolveAttributeParameterTest() + { + var bindingProvider = CreateBindingProvider(); + var attribute = new SignalRTriggerAttribute(); + var parameter = typeof(TestStronglyTypedHub).GetMethod(nameof(TestStronglyTypedHub.TestFunction), BindingFlags.Instance | BindingFlags.NonPublic).GetParameters()[0]; + var resolvedAttribute = bindingProvider.GetParameterResolvedAttribute(attribute, parameter); + Assert.Equal(nameof(TestStronglyTypedHub), resolvedAttribute.HubName); + Assert.Equal(Category.Messages, resolvedAttribute.Category); + Assert.Equal(nameof(TestStronglyTypedHub.TestFunction), resolvedAttribute.Event); + Assert.Equal(new string[] { "arg0", "arg1" }, resolvedAttribute.ParameterNames); + + // With SignalRIgoreAttribute + parameter = typeof(TestStronglyTypedHub).GetMethod(nameof(TestStronglyTypedHub.TestFunctionWithIgnore), BindingFlags.Instance | BindingFlags.NonPublic).GetParameters()[0]; + resolvedAttribute = bindingProvider.GetParameterResolvedAttribute(attribute, parameter); + Assert.Equal(new string[] { "arg0", "arg1" }, resolvedAttribute.ParameterNames); + + // With ILogger and CancellationToken + parameter = typeof(TestStronglyTypedHub).GetMethod(nameof(TestStronglyTypedHub.TestFunctionWithSpecificType), BindingFlags.Instance | BindingFlags.NonPublic).GetParameters()[0]; + resolvedAttribute = bindingProvider.GetParameterResolvedAttribute(attribute, parameter); + Assert.Equal(new string[] { "arg0", "arg1" }, resolvedAttribute.ParameterNames); + } + + [Fact] + public void ResolveConnectionAttributeParameterTest() + { + var bindingProvider = CreateBindingProvider(); + var attribute = new SignalRTriggerAttribute(); + var parameter = typeof(TestStronglyTypedHub).GetMethod(nameof(TestStronglyTypedHub.OnConnected), BindingFlags.Instance | BindingFlags.NonPublic).GetParameters()[0]; + var resolvedAttribute = bindingProvider.GetParameterResolvedAttribute(attribute, parameter); + Assert.Equal(nameof(TestStronglyTypedHub), resolvedAttribute.HubName); + Assert.Equal(Category.Connections, resolvedAttribute.Category); + Assert.Equal(Event.Connected, resolvedAttribute.Event); + Assert.Equal(new string[] { "arg0", "arg1" }, resolvedAttribute.ParameterNames); + + parameter = typeof(TestStronglyTypedHub).GetMethod(nameof(TestStronglyTypedHub.OnDisconnected), BindingFlags.Instance | BindingFlags.NonPublic).GetParameters()[0]; + resolvedAttribute = bindingProvider.GetParameterResolvedAttribute(attribute, parameter); + Assert.Equal(nameof(TestStronglyTypedHub), resolvedAttribute.HubName); + Assert.Equal(Category.Connections, resolvedAttribute.Category); + Assert.Equal(Event.Disconnected, resolvedAttribute.Event); + Assert.Equal(new string[] { "arg0", "arg1" }, resolvedAttribute.ParameterNames); + } + + [Fact] + public void ResolveAttributeParameterConflictTest() + { + var bindingProvider = CreateBindingProvider(); + var attribute = new SignalRTriggerAttribute(string.Empty, string.Empty, string.Empty, new string[] { "arg0" }); + var parameter = typeof(TestStronglyTypedHub).GetMethod(nameof(TestStronglyTypedHub.TestFunction), BindingFlags.Instance | BindingFlags.NonPublic).GetParameters()[0]; + Assert.ThrowsAny(() => bindingProvider.GetParameterResolvedAttribute(attribute, parameter)); + } + + [Fact] + public void WebhookFailedTest() + { + var bindingProvider = CreateBindingProvider(new Exception()); + var parameter = typeof(TestStronglyTypedHub).GetMethod(nameof(TestStronglyTypedHub.OnConnected), BindingFlags.Instance | BindingFlags.NonPublic).GetParameters()[0]; + var context = new TriggerBindingProviderContext(parameter, default); + Assert.ThrowsAsync(() => bindingProvider.TryCreateAsync(context)); + } + + private SignalRTriggerBindingProvider CreateBindingProvider(Exception exception = null, string connectionStringSetting = Constants.AzureSignalRConnectionStringName) + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); + configuration[connectionStringSetting] = "Endpoint=http://localhost;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;Version=1.0;"; + configuration["Serverless_ExpressionBindings_HubName"] = "test_hub"; + configuration["Serverless_ExpressionBindings_HubCategory"] = "connections"; + configuration["Serverless_ExpressionBindings_HubEvent"] = "connected"; + var dispatcher = new TestTriggerDispatcher(); + return new SignalRTriggerBindingProvider(dispatcher, new DefaultNameResolver(configuration), new ServiceManagerStore(configuration, NullLoggerFactory.Instance, null), exception); + } + } +}