Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SignalR] Add Strongly typed serverless hub #25075

Merged
4 commits merged into from
Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions eng/Packages.Data.props
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@
<PackageReference Update="CloudNative.CloudEvents" Version="2.0.0" />
<PackageReference Update="CloudNative.CloudEvents.SystemTextJson" Version="2.0.0" />
<PackageReference Update="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="3.0.0" Condition="'$(TargetFramework)' == 'netcoreapp3.1'" />
<PackageReference Update="Microsoft.Azure.SignalR" Version="1.12.0" />
<PackageReference Update="Microsoft.Azure.SignalR.Management" Version="1.12.0" />
<PackageReference Update="Microsoft.Azure.SignalR.Protocols" Version="1.12.0" />
<PackageReference Update="Microsoft.Azure.SignalR" Version="1.13.0" />
<PackageReference Update="Microsoft.Azure.SignalR.Management" Version="1.13.0" />
<PackageReference Update="Microsoft.Azure.SignalR.Protocols" Version="1.13.0" />
<PackageReference Update="Microsoft.Azure.SignalR.Serverless.Protocols" Version="1.6.0" />
<PackageReference Update="Microsoft.Azure.WebJobs" Version="3.0.30" />
<PackageReference Update="Microsoft.Azure.WebJobs.Sources" Version="3.0.30" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ public SignalRConnectionAttribute(string connectionStringSetting) { }
public string Connection { get { throw null; } set { } }
}
}
public abstract partial class ServerlessHub<T> where T : class
{
protected ServerlessHub(Microsoft.Azure.SignalR.Management.ServiceHubContext<T> serviceHubContext = null) { }
public Microsoft.Azure.SignalR.Management.ClientManager ClientManager { get { throw null; } }
public Microsoft.AspNetCore.SignalR.IHubClients<T> 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<System.Security.Claims.Claim> GetClaims(string jwt) { throw null; }
protected System.Threading.Tasks.Task<Microsoft.Azure.WebJobs.Extensions.SignalRService.SignalRConnectionInfo> NegotiateAsync(Microsoft.Azure.SignalR.Management.NegotiationOptions options) { throw null; }
}
public partial class SignalRAsyncCollector<T> : Microsoft.Azure.WebJobs.IAsyncCollector<T>
{
internal SignalRAsyncCollector() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ public SignalRConnectionAttribute(string connectionStringSetting) { }
public string Connection { get { throw null; } set { } }
}
}
public abstract partial class ServerlessHub<T> where T : class
{
protected ServerlessHub(Microsoft.Azure.SignalR.Management.ServiceHubContext<T> serviceHubContext = null) { }
public Microsoft.Azure.SignalR.Management.ClientManager ClientManager { get { throw null; } }
public Microsoft.AspNetCore.SignalR.IHubClients<T> 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<System.Security.Claims.Claim> GetClaims(string jwt) { throw null; }
protected System.Threading.Tasks.Task<Microsoft.Azure.WebJobs.Extensions.SignalRService.SignalRConnectionInfo> NegotiateAsync(Microsoft.Azure.SignalR.Management.NegotiationOptions options) { throw null; }
}
public partial class SignalRAsyncCollector<T> : Microsoft.Azure.WebJobs.IAsyncCollector<T>
{
internal SignalRAsyncCollector() { }
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IChatClient>
{
[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.
Original file line number Diff line number Diff line change
@@ -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<ServiceHubContext<T>> GetAsync<T>(string hubName) where T : class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,26 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
internal class ServiceHubContextStore : IInternalServiceHubContextStore
{
private readonly ConcurrentDictionary<string, (Lazy<Task<IServiceHubContext>> Lazy, IServiceHubContext Value)> _store = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, Lazy<Task<object>>> _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<ServiceHubContext<T>> GetAsync<T>(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<Task<object>>(async () => await _serviceManager.CreateHubContextAsync<T>(hubName, default).ConfigureAwait(false)));
var hubContext = await lazy.Value.ConfigureAwait(false);
return (ServiceHubContext<T>)hubContext;
}

public ValueTask<IServiceHubContext> GetAsync(string hubName)
Expand Down Expand Up @@ -55,6 +65,7 @@ private async Task<IServiceHubContext> GetFromLazyAsync(string hubName, (Lazy<Ta
}
catch (Exception)
{
// Allow to retry for transient errors.
_store.TryRemove(hubName, out _);
throw;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ private IInternalServiceHubContextStore CreateHubContextStore(string connectionS
}
})
.AddSignalRServiceManager()
.AddSingleton(sp => (ServiceManager)sp.GetService<IServiceManager>())
.AddSingleton(_loggerFactory)
.AddSingleton<IInternalServiceHubContextStore, ServiceHubContextStore>();
if (_router != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Version>1.7.0-beta.1</Version>
<SignAssembly>true</SignAssembly>
<IsExtensionClientLibrary>true</IsExtensionClientLibrary>
<NoWarn>$(NoWarn);CS1591;AZC0001</NoWarn>
<NoWarn>$(NoWarn);CS1591;AZC0001;AZC0107;</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand All @@ -18,7 +18,7 @@
<PackageReference Include="Microsoft.Azure.SignalR.Protocols" />
<PackageReference Include="Microsoft.Azure.SignalR.Serverless.Protocols" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T> where T : class
Y-Sindo marked this conversation as resolved.
Show resolved Hide resolved
{
private static readonly Lazy<JwtSecurityTokenHandler> JwtSecurityTokenHandler = new(() => new JwtSecurityTokenHandler());
private readonly ServiceHubContext<T> _hubContext;

/// <summary>
/// Gets an object that can be used to invoke methods on the clients connected to this hub.
/// </summary>
public IHubClients<T> Clients => _hubContext.Clients;

/// <summary>
/// Get the group manager of this hub.
/// </summary>
public GroupManager Groups => _hubContext.Groups;

/// <summary>
/// Get the user group manager of this hub.
/// </summary>
public UserGroupManager UserGroups => _hubContext.UserGroups;

/// <summary>
/// Get the client manager of this hub.
/// </summary>
public ClientManager ClientManager => _hubContext.ClientManager;

protected ServerlessHub(ServiceHubContext<T> serviceHubContext = null)
{
if (serviceHubContext is null)
{
serviceHubContext = ((IInternalServiceHubContextStore)StaticServiceHubContextStore.Get()).GetAsync<T>(GetType().Name).Result;
}
_hubContext = serviceHubContext;
}

/// <summary>
/// Gets client endpoint access information object for SignalR hub connections to connect to Azure SignalR Service
/// </summary>
Y-Sindo marked this conversation as resolved.
Show resolved Hide resolved
protected async Task<SignalRConnectionInfo> NegotiateAsync(NegotiationOptions options)
{
var negotiateResponse = await _hubContext.NegotiateAsync(options).ConfigureAwait(false);
return new SignalRConnectionInfo
{
Url = negotiateResponse.Url,
AccessToken = negotiateResponse.AccessToken
};
}

/// <summary>
/// Get claim list from a JWT.
/// </summary>
protected static IList<Claim> GetClaims(string jwt)
{
if (jwt.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
jwt = jwt.Substring("Bearer ".Length).Trim();
}
return JwtSecurityTokenHandler.Value.ReadJwtToken(jwt).Claims.ToList();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) ||
Expand Down Expand Up @@ -195,5 +195,27 @@ private static bool HasBindingAttribute(IEnumerable<Attribute> attributes)
{
return attributes.Any(attribute => attribute.GetType().GetCustomAttribute<BindingAttribute>(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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<IChatClient>
{
[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
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<IChatClient>(GetType().Name).Result;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<IChatClient>("hubName", default);
var myHub = new TestStronglyTypedHub(hubContext);
var connectionInfo = await myHub.Negotiate("user");
Assert.NotNull(connectionInfo);
}
}
}
Loading