Search code examples
c#.netasp.net-coresignalrsignalr-hub

Injecting tenant information in a SignalR Hub's OnConnected/OnDisconnected


There is a multi-tenant SaaS app where I want to create groups for each tenantIds in the SignalR Hub's OnConnectedAsync/OnDisconnectedAsync hook.

The problem is that ITenancyContext<ApplicationTenant> is registered as a scoped service, which means it is only available within the scope of a request. In the context of a SignalR hub, it is not associated with a request, and thus it is not available.

So how do I make it available? Authorize it and enrich the claims somehow?

public sealed class NotificationHub : Hub
{
    readonly ILogger<NotificationHub> _logger;
    readonly Guid _tenantId;

    public NotificationHub(ILogger<NotificationHub> logger, ITenancyContext<ApplicationTenant> tenancyContext) => (_logger, _tenantId) = (logger, tenancyContext.Tenant.Id);

    public override async Task OnConnectedAsync()
    {
        await JoinGroup(_tenantId.ToString());
        _logger.LogInformation("{ConnectionId} has connected to the hub. TenantId: {TenantId}", Context.ConnectionId, _tenantId);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await LeaveGroup(_tenantId.ToString());
        _logger.LogInformation("{ConnectionId} was disconnected from the hub. TenantId: {TenantId}", Context.ConnectionId, _tenantId);
        await base.OnDisconnectedAsync(exception);
    }

    Task JoinGroup(string groupName) => Groups.AddToGroupAsync(Context.ConnectionId, groupName);

    Task LeaveGroup(string groupName) => Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}

Solution

  • I think we can create a middleware to implement it. Inject ITenancyContext<ApplicationTenant> in this middlerware, let call it TenantMiddleware.

    public class TenantMiddleware
    {
        private readonly ITenancyContext<ApplicationTenant> _tenancyContext;
    
        public TenantMiddleware(ITenancyContext<ApplicationTenant> tenancyContext)
        {
            _tenancyContext = tenancyContext;
        }
    
        public async Task InvokeAsync(HttpContext context, Func<Task> next)
        {
            var user = context.User.Identity as ClaimsIdentity;
            if (user != null)
            {
                var TenantId= _tenancyContext.Tenant.Id;
                user.AddClaim(new Claim("TenantId", TenantId.ToString()));
            }
    
            await next();
        }
    }
    

    Then we can use it in your NotificationHub class.

    public override async Task OnConnectedAsync()
    {
        // Get TenantId like below.
        var user = Context.User.Identity as ClaimsIdentity;
        var tenantIdClaim = user?.FindFirst("TenantId");
        _tenantId = tenantIdClaim != null ? Guid.Parse(tenantIdClaim.Value) : Guid.Empty;
        ...
        await JoinGroup(_tenantId.ToString());
        _logger.LogInformation("{ConnectionId} has connected to the hub. TenantId: {TenantId}", Context.ConnectionId, _tenantId);
        await base.OnConnectedAsync();
    }