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

Unable to call AppDbContext in a SignalR Hub


Why I'm unable to use AppDbContext in a SignalR Hub? There is nothing wrong with the query itself. It works outside of the hub context. The email is resolved correctly.

System.NullReferenceException: Object reference not set to an instance of an object. at lambda_method1325(Closure , QueryContext ) at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)

My whole idea is whenever the user logs in, to send all undelivered messages to him. Or maybe I'm not supposed to? Perhaps the frontend is supposed to call an endpoint which is going to initially load these undelivered messages from the outbox table as opposed to OnConnectedAsync?

[Authorize(AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme)]
public sealed class SignalRHub : Hub
{
    readonly ILogger<SignalRHub> _logger;
    readonly IMediator _mediator;

    public SignalRHub(ILogger<SignalRHub> logger, IMediator mediator)
    {
        _logger = logger;
        _mediator = mediator;
    }

    public override async Task OnConnectedAsync()
    {
        var email = Context.User.GetEmailAddress();
        
        await SendUndeliveredNotificationsAsync(email);

        await base.OnConnectedAsync();
    }

    async Task SendUndeliveredNotificationsAsync(string email)
    {
        var undeliveredNotifications = await _mediator.Send(new GetUndeliveredNotificationsForUserQuery(email));
        var tasks = undeliveredNotifications.Select(notificationMessage => _mediator.Send(new SendInAppNotificationCommand(notificationMessage.Id)));
        await Task.WhenAll(tasks);
    }
}

public sealed record GetUndeliveredNotificationsForUserQuery(string Email) : IRequest<IReadOnlyCollection<NotificationMessage>>;

public sealed class GetUndeliveredNotificationsForUserQueryHandler : IRequestHandler<GetUndeliveredNotificationsForUserQuery, IReadOnlyCollection<NotificationMessage>>
{
    readonly AppDbContext _dbContext;

    public GetUndeliveredNotificationsForUserQueryHandler(AppDbContext dbContext) => _dbContext = dbContext;

    public async Task<IReadOnlyCollection<NotificationMessage>> Handle(GetUndeliveredNotificationsForUserQuery request, CancellationToken cancellationToken)
        => await _dbContext.NotificationMessages
            .AsNoTracking()
            .Include(x => x.Notification)
            .ThenInclude(x => x.Employee)
            .Where(x => x.Notification.Employee != null && x.Notification.Employee.Email == request.Email && x.DateDelivered == null)
            .ToListAsync(cancellationToken);
}


Solution

  • Problem solved!

    Although the claims are resolved from the AuthorizeAttribute, the tenant id itself isn't being set.

    This is what I ended up doing. What's important is_dbContext.TenancyContext.Tenant = applicationTenant;

    [Authorize(AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme)]
    public sealed class SignalRHub : Hub
    {
        readonly ILogger<SignalRHub> _logger;
        readonly AppDbContext _dbContext;
    
        public SignalRHub(ILogger<SignalRHub> logger, AppDbContext dbContext)
        {
            _logger = logger;
            _dbContext = dbContext;
        }
    
        ...
    
        public async Task GetUndeliveredMessages()
        {
            var email = Context.User.GetEmailAddress();
            var tenant = Context.User.GetTenant();
            
            var applicationTenant = await _dbContext.Tenants.AsNoTracking().FirstOrDefaultAsync(x => x.NormalizedCanonicalName == tenant);
            
            _dbContext.TenancyContext.Tenant = applicationTenant;
            
            var undeliveredMessages = await _dbContext.NotificationMessages
                .AsNoTracking()
                .Include(x => x.Notification)
                .ThenInclude(x => x.Employee)
                .Where(x => x.Notification.Employee != null && x.Notification.Employee.Email == email && x.ChannelType == ChannelType.InApp && x.DateDelivered == null)
                .Select(x => x.MapToResponse())
                .ToListAsync();
                
            await Clients.Caller.SendAsync("ReceiveUndeliveredMessages", undeliveredMessages);
        }
    }