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

Add a strongly typed ServerlessHub #267

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion build/dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</PropertyGroup>
<PropertyGroup Label="Package Versions">
<MicroBuildCorePackageVersion>0.3.0</MicroBuildCorePackageVersion>
<MicrosoftAzureSignalRManagement>1.11.0-preview1-10835</MicrosoftAzureSignalRManagement>
<MicrosoftAzureSignalRManagement>1.11.0-preview1-10838</MicrosoftAzureSignalRManagement>
<MicrosoftAzureFunctionsExtensionsVersion>1.0.0</MicrosoftAzureFunctionsExtensionsVersion>
<MicrosoftNETTestSdkPackageVersion>15.8.0</MicrosoftNETTestSdkPackageVersion>
<MoqPackageVersion>4.9.0</MoqPackageVersion>
Expand Down
27 changes: 17 additions & 10 deletions samples/bidirectional-chat/csharp/Function.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,53 +14,60 @@

namespace FunctionApp
{
public class SimpleChat : ServerlessHub
public class SimpleChat : ServerlessHub<SimpleChat.IChatClient>
Copy link

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)

{
private const string NewMessageTarget = "newMessage";
private const string NewConnectionTarget = "newConnection";

public interface IChatClient
{
public Task newConnection(NewConnection newConnection);
Copy link

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why change this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NegotiateAsync method returns ValueTask for strongly-typed hub.

{
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))]
Expand Down Expand Up @@ -92,7 +99,7 @@ public void OnDisconnected([SignalRTrigger]InvocationContext invocationContext)
{
}

private class NewConnection
public class NewConnection
{
public string ConnectionId { get; }

Expand All @@ -102,7 +109,7 @@ public NewConnection(string connectionId)
}
}

private class NewMessage
public class NewMessage
{
public string ConnectionId { get; }
public string Sender { get; }
Expand Down
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);
Copy link

Choose a reason for hiding this comment

The 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 IServiceHubContextStore.GetAsync(string hubName). Is this worth documenting?

}
}
38 changes: 34 additions & 4 deletions src/SignalRServiceExtension/Config/ServiceHubContextStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand All @@ -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);
Expand All @@ -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
Copy link

@lfshr lfshr Sep 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer this signature for GetAsync. As it stands though it's currently never used.


///<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
Copy link

Choose a reason for hiding this comment

The 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
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The 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)" />
Expand Down
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);
}
}
}
76 changes: 76 additions & 0 deletions src/SignalRServiceExtension/TriggerBindings/ServerlessHub`T.cs
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
Expand Up @@ -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) ||
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type.IsSubclassOf(typeof(ServerlessHub<>)) doesn't work?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type.IsSubclassOf(typeof(ServerlessHub<>)) doesn't work?

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))
Expand Down