Recently, I implemented a chat application using AspNetCore.SignalR. I need to store chat history in a database and maintain the online/offline status of users. Additionally, I need to access the database in my SignalR hub. In this post, we'll discuss how to access my SQL Server database through my hub using Entity Framework Core. Specifically, we'll discuss how to access a DB context defined using a Configuration.GetConnectionString in Program.cs inside my hub.


Method 1: Requesting a DbContext from a scope in a SignalR Core hub using IServiceScopeFactory

ChatHub class which manages communication between clients in a chat application. 

[Authorize]
    public class ChatHub : Hub
    {
        private readonly IServiceScopeFactory _scopeFactory;

        public ChatHub(IServiceScopeFactory scopeFactory)
        {
            _scopeFactory = scopeFactory;
        }
        public override Task OnDisconnectedAsync(Exception exception)
        {
            using (var scope = _scopeFactory.CreateScope())
            {

                var _dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
                //new Guid("61A4FF60-F6EF-EE11-9CBB-A4BF016CE56D");
                var userId =  new Guid(Context.User.GetLoggedInUserClaim(ClaimType.UserId));
                var user = _dbContext.Users.FirstOrDefault(a => a.Id == userId);
                user.IsOnline = false;
                user.LastDisconnectedAt = DateTime.UtcNow;
                _dbContext.SaveChanges();
                Debug.WriteLine("Client disconnected: " + Context.ConnectionId);
                return base.OnDisconnectedAsync(exception);
            }
           
        }
        public override Task OnConnectedAsync()
        {
            using (var scope = _scopeFactory.CreateScope())
            {
                Debug.WriteLine("Client connected: " + Context.ConnectionId);
                var _dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
                //new Guid("61A4FF60-F6EF-EE11-9CBB-A4BF016CE56D");
                var userId = new Guid(Context.User.GetLoggedInUserClaim(ClaimType.UserId));
                var user = _dbContext.Users.FirstOrDefault(a => a.Id == userId);
                user.ConnectionId = Context.ConnectionId;
                user.IsOnline = true;
                user.LastConnectedAt = DateTime.UtcNow;
                _dbContext.SaveChanges();
                return base.OnConnectedAsync();
            }
                
        }
        //Create Group for each user to chat sepeartely
        public void SetUserChatGroup(string userChatId)
        {
            var id = Context.ConnectionId;
            Debug.WriteLine($"Client {id} added to group " + userChatId);
            Groups.AddToGroupAsync(Context.ConnectionId, userChatId.ToLower());
        }
        //Send message to user Group
        public async Task SendMessageToOtherUser(ReceiveMessageDTO receiveMessageDTO)
        {
            if (Clients != null)
            {
                await Clients.Group(receiveMessageDTO.ReceiverId.ToString().ToLower()).SendAsync("ReceiveMessage", receiveMessageDTO);
            }
        }
        //Send message to user Group
        public async Task SendNotification(string userId)
        {
            if(Clients!=null)
            {
                await Clients.Group(userId.ToLower()).SendAsync("Notification", "New notification recived!");
            }

        }
    }


  • In ChatHub constructor we injects an instance of IServiceScopeFactory which is used to create a scope for resolving services.
  • OnDisconnectedAsync(): this function called when a client disconnects from the hub and inside that function we updates the user's online status to false.
  • OnConnectedAsync(): this method is called when a client connects to the hub and in this function we updates the user's online status to true.
  • SetUserChatGroup(string userChatId): here we are adding the current connection to a specified chat group identified by userChatId. This allows clients to communicate within specific chat groups.
  • SendMessageToOtherUser(ReceiveMessageDTO receiveMessageDTO): this function sends a message to a specific user identified by receiveMessageDTO.ReceiverId. It uses SignalR's group feature to send the message to the appropriate group.
  • using (var scope = _scopeFactory.CreateScope()): here we are creating a new scope using the IServiceScopeFactory, which is used for resolving services within that scope. 
  • var _dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();: Within the scope, this line get an instance of the DatabaseContext service using dependency injection. This context allows interaction with the database. 
  • var userId = new Guid(Context.User.GetLoggedInUserClaim(ClaimType.UserId));: retrieves the logged-in user's ID from the claims associated with the current connection's user context. 

Above code shows you how to handle user connections, disconnections, group management, and message sending in a SignalR hub for a chat application. Additionally, it interacts with a database using Entity Framework Core to update user information such as online status & connection details.

Methood 2: Injecting database context it into the constructor of the  hub class

In .NET , you can access the database context within a SignalR hub by injecting it into the constructor of the hub class. Here's how you can do it: 
First, ensure you have your database context set up. Let's assume we have a DbContext named AppDbContext. DbContext in the Program.cs file:

builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddSignalR();
builder.Services.AddSingleton<ChatHub>();
builder.Services.AddSingleton<JWTManagerRepository>();
builder.Services.AddSingleton<DbThreadLogic>();
Then, in SignalR hub class, we can inject AppDbContext into the constructor like this:

With this setup, you can now use _dbContext within your SignalR hub methods to interact with your database. Make sure you register your DbContext in the dependency injection container in your Program.cs:
using Microsoft.AspNetCore.SignalR;

public class ChatHub  : Hub
{
    private readonly AppDbContext _dbContext;

    public ChatHub (AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    //SignalR hub methods here
    public void SendMessage()
    {
           //call the dbContext here
    }
}
Above ensures that your SignalR hub has access to the database context, allowing you to perform database operations within hub methods.

But when I run the code, I get an exception with the error message. "Cannot consume scoped service 
'DatabaseContext' from singleton 'SignalRHub.ChatHub'" typically occurs because you're trying to inject a scoped service (such as a DbContext) into a singleton service (such as a SignalR hub).

In .NET Core, a DbContext is registered as a scoped service, meaning it's created once per request. However, SignalR hubs are singleton instances by default. If you look at the program.cs file, you can see that we have registered SignalR as a singleton instance.
To resolve this issue, we have a few options:

  • Change the Lifetime of DbContext: One option is to change the lifetime of DbContext registration to match that of the SignalR hub. However, this might not be ideal depending on application's requirements.

services.AddDbContext<AppDbContext>(options =>
{
    // Configure your DbContext options here
}, ServiceLifetime.Singleton);
Keep in mind that changing the lifetime of DbContext to singleton might not be recommended for most scenarios, as DbContext is typically designed to be used as a scoped service.

  • Use a Different Service Lifetime for the Hub: Instead of injecting DbContext directly into the hub, we can use a factory method or another service with a suitable lifetime to create DbContext instances when needed within hub methods.
  • Inject DbContextFactory: Another option is to inject IDbContextFactory<AppDbContext> into the hub constructor instead of AppDbContext. Then, use the factory to create a new instance of DbContext when needed.

using Microsoft.EntityFrameworkCore;

public class ChatHub : Hub
{
    private readonly IDbContextFactory<AppDbContext> _contextFactory;

    public ChatHub(IDbContextFactory<AppDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    // Use _contextFactory.CreateDbContext() to create a new instance of AppDbContext
}
As per best practices , it's usually preferred to keep the DbContext scoped to the lifetime of the request. to ensures that each request gets its own instance of the DbContext, which helps in managing concurrency and ensures data consistency.Therefore, the second or third approach would be more aligned with best practices:

Use a Different Service Lifetime for the Hub : approach involves injecting another service with a suitable lifetime (such as scoped) into the hub, and then using that service to perform database operations, to ensures that the DbContext is not directly injected into the hub but is still scoped appropriately.

Inject DbContextFactory:  approach involves injecting IDbContextFactory<AppDbContext> into the hub constructor and using it to create a new instance of DbContext when needed. This allows us to control the lifetime of DbContext instances within hub methods explicitly.