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!");
}
}
}
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddSignalR();
builder.Services.AddSingleton<ChatHub>();
builder.Services.AddSingleton<JWTManagerRepository>();
builder.Services.AddSingleton<DbThreadLogic>();
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
}
}
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.
services.AddDbContext<AppDbContext>(options =>
{
// Configure your DbContext options here
}, ServiceLifetime.Singleton);
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: