-
Notifications
You must be signed in to change notification settings - Fork 48
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
Add a strongly typed ServerlessHub
#267
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,53 +14,60 @@ | |
|
||
namespace FunctionApp | ||
{ | ||
public class SimpleChat : ServerlessHub | ||
public class SimpleChat : ServerlessHub<SimpleChat.IChatClient> | ||
{ | ||
private const string NewMessageTarget = "newMessage"; | ||
private const string NewConnectionTarget = "newConnection"; | ||
|
||
public interface IChatClient | ||
{ | ||
public Task newConnection(NewConnection newConnection); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably some discussion here around casing. The example was pascal cased. Personally I'd want to stick to the language standards for examples. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agree with @lfshr should be pascal |
||
public Task newMessage(NewMessage newMessage); | ||
} | ||
|
||
[FunctionName("negotiate")] | ||
public Task<SignalRConnectionInfo> NegotiateAsync([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req) | ||
public async Task<SignalRConnectionInfo> NegotiateAsync([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req) | ||
{ | ||
var claims = GetClaims(req.Headers["Authorization"]); | ||
return NegotiateAsync(new NegotiationOptions | ||
var result = await NegotiateAsync(new NegotiationOptions | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why change this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
{ | ||
UserId = claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value, | ||
Claims = claims | ||
}); | ||
return result; | ||
} | ||
|
||
[FunctionName(nameof(OnConnected))] | ||
public async Task OnConnected([SignalRTrigger]InvocationContext invocationContext, ILogger logger) | ||
{ | ||
await Clients.All.SendAsync(NewConnectionTarget, new NewConnection(invocationContext.ConnectionId)); | ||
await Clients.All.newConnection(new NewConnection(invocationContext.ConnectionId)); | ||
logger.LogInformation($"{invocationContext.ConnectionId} has connected"); | ||
} | ||
|
||
[FunctionAuthorize] | ||
[FunctionName(nameof(Broadcast))] | ||
public async Task Broadcast([SignalRTrigger]InvocationContext invocationContext, string message, ILogger logger) | ||
{ | ||
await Clients.All.SendAsync(NewMessageTarget, new NewMessage(invocationContext, message)); | ||
await Clients.All.newMessage(new NewMessage(invocationContext, message)); | ||
logger.LogInformation($"{invocationContext.ConnectionId} broadcast {message}"); | ||
} | ||
|
||
[FunctionName(nameof(SendToGroup))] | ||
public async Task SendToGroup([SignalRTrigger]InvocationContext invocationContext, string groupName, string message) | ||
{ | ||
await Clients.Group(groupName).SendAsync(NewMessageTarget, new NewMessage(invocationContext, message)); | ||
await Clients.Group(groupName).newMessage(new NewMessage(invocationContext, message)); | ||
} | ||
|
||
[FunctionName(nameof(SendToUser))] | ||
public async Task SendToUser([SignalRTrigger]InvocationContext invocationContext, string userName, string message) | ||
{ | ||
await Clients.User(userName).SendAsync(NewMessageTarget, new NewMessage(invocationContext, message)); | ||
await Clients.User(userName).newMessage(new NewMessage(invocationContext, message)); | ||
} | ||
|
||
[FunctionName(nameof(SendToConnection))] | ||
public async Task SendToConnection([SignalRTrigger]InvocationContext invocationContext, string connectionId, string message) | ||
{ | ||
await Clients.Client(connectionId).SendAsync(NewMessageTarget, new NewMessage(invocationContext, message)); | ||
await Clients.Client(connectionId).newMessage(new NewMessage(invocationContext, message)); | ||
} | ||
|
||
[FunctionName(nameof(JoinGroup))] | ||
|
@@ -92,7 +99,7 @@ public void OnDisconnected([SignalRTrigger]InvocationContext invocationContext) | |
{ | ||
} | ||
|
||
private class NewConnection | ||
public class NewConnection | ||
{ | ||
public string ConnectionId { get; } | ||
|
||
|
@@ -102,7 +109,7 @@ public NewConnection(string connectionId) | |
} | ||
} | ||
|
||
private class NewMessage | ||
public class NewMessage | ||
{ | ||
public string ConnectionId { get; } | ||
public string Sender { get; } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,17 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System; | ||
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; } | ||
|
||
public dynamic GetAsync(Type THubType, Type TType); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not quite understanding the necessity for this to be separated from |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,13 +7,16 @@ | |
using System.Threading.Tasks; | ||
using Microsoft.Azure.SignalR; | ||
using Microsoft.Azure.SignalR.Management; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService | ||
{ | ||
internal class ServiceHubContextStore : IInternalServiceHubContextStore | ||
{ | ||
private readonly ConcurrentDictionary<string, (Lazy<Task<IServiceHubContext>> lazy, IServiceHubContext value)> store = new ConcurrentDictionary<string, (Lazy<Task<IServiceHubContext>>, IServiceHubContext value)>(StringComparer.OrdinalIgnoreCase); | ||
private readonly ConcurrentDictionary<string, (Lazy<Task<IServiceHubContext>> lazy, IServiceHubContext value)> _weakTypedHubStore = new(StringComparer.OrdinalIgnoreCase); | ||
private readonly IServiceEndpointManager endpointManager; | ||
|
||
private readonly IServiceProvider _strongTypedHubServiceProvider; | ||
|
||
public IServiceManager ServiceManager { get; } | ||
|
||
|
@@ -23,11 +26,15 @@ public ServiceHubContextStore(IServiceEndpointManager endpointManager, IServiceM | |
{ | ||
this.endpointManager = endpointManager; | ||
ServiceManager = serviceManager; | ||
_strongTypedHubServiceProvider = new ServiceCollection() | ||
.AddSingleton(serviceManager as ServiceManager) | ||
.AddSingleton(typeof(ServerlessHubContext<,>)) | ||
.BuildServiceProvider(); | ||
} | ||
|
||
public ValueTask<IServiceHubContext> GetAsync(string hubName) | ||
{ | ||
var pair = store.GetOrAdd(hubName, | ||
var pair = _weakTypedHubStore.GetOrAdd(hubName, | ||
(new Lazy<Task<IServiceHubContext>>( | ||
() => ServiceManager.CreateHubContextAsync(hubName)), default)); | ||
return GetAsyncCore(hubName, pair); | ||
|
@@ -50,14 +57,37 @@ private async Task<IServiceHubContext> GetFromLazyAsync(string hubName, (Lazy<Ta | |
try | ||
{ | ||
var value = await pair.lazy.Value; | ||
store.TryUpdate(hubName, (null, value), pair); | ||
_weakTypedHubStore.TryUpdate(hubName, (null, value), pair); | ||
return value; | ||
} | ||
catch (Exception) | ||
{ | ||
store.TryRemove(hubName, out _); | ||
_weakTypedHubStore.TryRemove(hubName, out _); | ||
throw; | ||
} | ||
} | ||
|
||
private Task<ServiceHubContext<T>> GetAsync<THub, T>() where THub : ServerlessHub<T> where T : class | ||
{ | ||
return _strongTypedHubServiceProvider.GetRequiredService<ServerlessHubContext<THub, T>>().HubContextTask; | ||
} | ||
Comment on lines
+70
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer this signature for |
||
|
||
///<summary> | ||
/// The method actually does the following thing | ||
///<code> | ||
/// private Task<ServiceHubContext<T>> GetAsync<THub, T>() where THub : ServerlessHub<T> where T : class | ||
///{ | ||
/// return _serviceProvider.GetRequiredService<ServerlessHubContext<THub, T>>().HubContext; | ||
///} | ||
/// </code> | ||
/// </summary> | ||
public dynamic GetAsync(Type THubType, Type TType) | ||
{ | ||
var genericType = typeof(ServerlessHubContext<,>); | ||
Type[] typeArgs = { THubType, TType }; | ||
var serverlessHubContextType = genericType.MakeGenericType(typeArgs); | ||
dynamic serverlessHubContext = _strongTypedHubServiceProvider.GetRequiredService(serverlessHubContextType); | ||
return serverlessHubContext.HubContextTask.GetAwaiter().GetResult(); | ||
} | ||
Comment on lines
+84
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be dynamic? We seem to know all the types at compile-time. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,10 @@ | |
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="$(MicrosoftAzureSignalRManagement)" /> | ||
<!--<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="$(MicrosoftAzureSignalRManagement)" />--> | ||
<ProjectReference Include="C:\Users\zityang\source\repos\azure-signalr\src\Microsoft.Azure.SignalR.Management\Microsoft.Azure.SignalR.Management.csproj" /> | ||
<ProjectReference Include="C:\Users\zityang\source\repos\azure-signalr\src\Microsoft.Azure.SignalR.Common\Microsoft.Azure.SignalR.Common.csproj" /> | ||
Comment on lines
+11
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't forget these! |
||
|
||
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="$(MicrosoftAzureFunctionsExtensionsVersion)" /> | ||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="$(SystemIdentityModelTokensJwtVersion)" /> | ||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="$(MicrosoftAspNetCoreSignalRPackageVersion)" /> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System.Threading.Tasks; | ||
using Microsoft.Azure.SignalR.Management; | ||
|
||
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService | ||
{ | ||
//A helper class so that nameof(THub) together with T can be used as a key to retrieve a ServiceHubContext<T> from a ServiceProvider. | ||
internal class ServerlessHubContext<THub, T> where THub : ServerlessHub<T> where T : class | ||
{ | ||
public Task<ServiceHubContext<T>> HubContextTask { get; } | ||
|
||
public ServerlessHubContext(ServiceManager serviceManager) | ||
{ | ||
HubContextTask = serviceManager.CreateHubContextAsync<T>(typeof(THub).Name, default); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IdentityModel.Tokens.Jwt; | ||
using System.Linq; | ||
using System.Security.Claims; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.SignalR; | ||
using Microsoft.Azure.SignalR.Management; | ||
|
||
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService | ||
{ | ||
public abstract class ServerlessHub<T> where T : class | ||
{ | ||
private static readonly Lazy<JwtSecurityTokenHandler> JwtSecurityTokenHandler = new Lazy<JwtSecurityTokenHandler>(() => new JwtSecurityTokenHandler()); | ||
protected ServiceHubContext<T> HubContext { get; } | ||
|
||
public ServerlessHub(ServiceHubContext<T> hubContext) | ||
{ | ||
HubContext = hubContext; | ||
} | ||
|
||
public ServerlessHub() | ||
{ | ||
HubContext = (StaticServiceHubContextStore.Get() as IInternalServiceHubContextStore).GetAsync(GetType(), typeof(T)); | ||
Clients = HubContext.Clients; | ||
Groups = HubContext.Groups; | ||
UserGroups = HubContext.UserGroups; | ||
ClientManager = HubContext?.ClientManager; | ||
} | ||
|
||
/// <summary> | ||
/// Gets client endpoint access information object for SignalR hub connections to connect to Azure SignalR Service | ||
/// </summary> | ||
protected async ValueTask<SignalRConnectionInfo> NegotiateAsync(NegotiationOptions options) | ||
{ | ||
var negotiateResponse = await HubContext.NegotiateAsync(options); | ||
return new SignalRConnectionInfo | ||
{ | ||
Url = negotiateResponse.Url, | ||
AccessToken = negotiateResponse.AccessToken | ||
}; | ||
} | ||
|
||
/// <summary> | ||
/// Get claim list from a JWT. | ||
/// </summary> | ||
protected IList<Claim> GetClaims(string jwt) | ||
{ | ||
if (jwt.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
jwt = jwt.Substring("Bearer ".Length).Trim(); | ||
} | ||
return JwtSecurityTokenHandler.Value.ReadJwtToken(jwt).Claims.ToList(); | ||
} | ||
|
||
/// <summary> | ||
/// Gets an object that can be used to invoke methods on the clients connected to this hub. | ||
/// </summary> | ||
public IHubClients<T> Clients { get; } | ||
|
||
/// <summary> | ||
/// Get the group manager of this hub. | ||
/// </summary> | ||
public IGroupManager Groups { get; } | ||
|
||
/// <summary> | ||
/// Get the user group manager of this hub. | ||
/// </summary> | ||
public IUserGroupManager UserGroups { get; } | ||
|
||
/// <summary> | ||
/// Get the client manager of this hub. | ||
/// </summary> | ||
public ClientManager ClientManager { get; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -75,7 +75,7 @@ internal SignalRTriggerAttribute GetParameterResolvedAttribute(SignalRTriggerAtt | |
var declaredType = method.DeclaringType; | ||
string[] parameterNamesFromAttribute; | ||
|
||
if (declaredType != null && declaredType.IsSubclassOf(typeof(ServerlessHub))) | ||
if (IsServerlessHub(declaredType)) | ||
{ | ||
// Class based model | ||
if (!string.IsNullOrEmpty(hubName) || | ||
|
@@ -116,6 +116,28 @@ internal SignalRTriggerAttribute GetParameterResolvedAttribute(SignalRTriggerAtt | |
return new SignalRTriggerAttribute(hubName, category, @event, parameterNames) { ConnectionStringSetting = connectionStringSetting }; | ||
} | ||
|
||
private bool IsServerlessHub(Type type) | ||
{ | ||
if (type == null) | ||
{ | ||
return false; | ||
} | ||
if(type.IsSubclassOf(typeof(ServerlessHub))) | ||
{ | ||
return true; | ||
} | ||
var baseType = type.BaseType; | ||
while (baseType != null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Doesn't work on an open generic I'm afraid! |
||
{ | ||
if(baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(ServerlessHub<>)) | ||
{ | ||
return true; | ||
} | ||
baseType = baseType.BaseType; | ||
} | ||
return false; | ||
} | ||
|
||
private void ValidateSignalRTriggerAttributeBinding(SignalRTriggerAttribute attribute) | ||
{ | ||
if (string.IsNullOrWhiteSpace(attribute.ConnectionStringSetting)) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it not be better to copy & paste the
bidirectional-chat
example, and highlight the fact that it's a strongly typed example? (eg.strongly-typed-bidirectional-chat
)