In this lesson, you will learn how to bridge the gap between your persistence layer and your transport layer by automatically triggering SignalR broadcasts whenever your Entity Framework DbContext performs a mutation.
The Challenge of Decoupling
In a standard web API, data flow is request-response. However, in a real-time system, a database change made by a background process or an API request needs to reach connected clients instantly. The most robust way to achieve this is by intercepting the SaveChangesAsync method within your DbContext. By overriding this method, you can inspect the change tracker, determine if an entity was modified, and push that state to the hub.
Implementation Pattern
Because DbContext is often registered with a Scoped lifetime, you cannot inject a IHubContext directly into the constructor without causing scope validation errors (as the Hub is usually scoped to the connection). Instead, use IHttpContextAccessor or inject IServiceProvider to resolve the IHubContext<T> within the SaveChangesAsync override.
Consider this implementation:
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
{
var entries = ChangeTracker.Entries<Product>()
.Where(e => e.State == EntityState.Modified);
var result = await base.SaveChangesAsync(ct);
foreach (var entry in entries)
{
// Resolve HubContext from the current service scope
var hubContext = _serviceProvider.GetRequiredService<IHubContext<ProductHub>>();
await hubContext.Clients.All.SendAsync("ProductUpdated", entry.Entity);
}
return result;
}
Architectural Consideration
While this pattern keeps your business logic clean, be mindful of the "Outbox Pattern" if your system demands high reliability. If the database transaction succeeds but the SignalR broadcast fails (e.g., due to a network hiccup or backplane latency), your clients will be out of sync. For mission-critical data, consider writing the update to an "Outbox" table in the same transaction, then use a background service to poll that table and send the SignalR notifications asynchronously. This ensures that the notification is only sent if the database state is guaranteed.
Key Takeaway: By overriding SaveChangesAsync in your DbContext and resolving IHubContext via dependency injection, you can turn database mutations into real-time events without polluting your service-layer controllers.