Background Notifications via SignalR
Most real-time updates don't originate from hub methods. They come from background jobs completing, message queue consumers processing events, or scheduled tasks detecting changes. SignalR's IHubContext makes it straightforward to push notifications from anywhere in your application.
The Core Pattern: IHubContext
IHubContext<THub> is the gateway for sending messages from outside a hub. Inject it into any service, controller, or background worker:
public class PaymentProcessor
{
private readonly IHubContext<NotificationHub, INotificationClient> _hub;
public PaymentProcessor(
IHubContext<NotificationHub, INotificationClient> hub)
{
_hub = hub;
}
public async Task ProcessPaymentAsync(Payment payment)
{
// Process the payment...
var result = await _gateway.ChargeAsync(payment);
// Notify the user in real time
await _hub.Clients.User(payment.UserId)
.ReceiveNotification(new Notification
{
Title = "Payment Processed",
Message = $"Your payment of {result.Amount:C} was successful.",
Type = NotificationType.Success,
Timestamp = DateTime.UtcNow
});
}
}
Background Service with Periodic Notifications
A BackgroundService that monitors something and pushes updates:
public class SystemHealthMonitor : BackgroundService
{
private readonly IHubContext<DashboardHub, IDashboardClient> _hub;
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<SystemHealthMonitor> _logger;
public SystemHealthMonitor(
IHubContext<DashboardHub, IDashboardClient> hub,
IServiceScopeFactory scopeFactory,
ILogger<SystemHealthMonitor> logger)
{
_hub = hub;
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var healthService = scope.ServiceProvider
.GetRequiredService<IHealthCheckService>();
var report = await healthService.CheckHealthAsync(stoppingToken);
await _hub.Clients.Group("admins")
.HealthUpdate(new HealthReport
{
Status = report.Status.ToString(),
Checks = report.Entries.Select(e => new HealthCheck
{
Name = e.Key,
Status = e.Value.Status.ToString(),
Duration = e.Value.Duration
}).ToList(),
Timestamp = DateTime.UtcNow
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Health check broadcast failed");
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
Register it:
builder.Services.AddHostedService<SystemHealthMonitor>();
Processing Queue Messages
When consuming from a message queue (RabbitMQ, Azure Service Bus, etc.), push notifications as events are processed:
public class OrderEventConsumer : BackgroundService
{
private readonly IHubContext<OrderHub, IOrderClient> _hub;
private readonly IMessageConsumer _consumer;
public OrderEventConsumer(
IHubContext<OrderHub, IOrderClient> hub,
IMessageConsumer consumer)
{
_hub = hub;
_consumer = consumer;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await _consumer.SubscribeAsync<OrderEvent>(async orderEvent =>
{
switch (orderEvent.Type)
{
case OrderEventType.Shipped:
await _hub.Clients.User(orderEvent.CustomerId)
.OrderUpdated(orderEvent.OrderId, "Shipped");
await _hub.Clients.Group("warehouse")
.OrderUpdated(orderEvent.OrderId, "Shipped");
break;
case OrderEventType.Delivered:
await _hub.Clients.User(orderEvent.CustomerId)
.OrderUpdated(orderEvent.OrderId, "Delivered");
break;
case OrderEventType.Cancelled:
await _hub.Clients.User(orderEvent.CustomerId)
.ReceiveNotification(new Notification
{
Title = "Order Cancelled",
Message = $"Order {orderEvent.OrderId} has been cancelled.",
Type = NotificationType.Warning
});
break;
}
}, stoppingToken);
}
}
Long-Running Operation Progress
A common pattern is reporting progress on long-running operations — file processing, report generation, data imports:
public class ReportGenerator
{
private readonly IHubContext<ReportHub, IReportClient> _hub;
public ReportGenerator(IHubContext<ReportHub, IReportClient> hub)
{
_hub = hub;
}
public async Task GenerateAsync(
string userId, string reportId, ReportParameters parameters)
{
await _hub.Clients.User(userId)
.ReportProgress(reportId, 0, "Starting report generation...");
var data = await FetchDataAsync(parameters);
await _hub.Clients.User(userId)
.ReportProgress(reportId, 25, "Data fetched. Processing...");
var processed = await ProcessDataAsync(data);
await _hub.Clients.User(userId)
.ReportProgress(reportId, 50, "Data processed. Generating charts...");
var charts = await GenerateChartsAsync(processed);
await _hub.Clients.User(userId)
.ReportProgress(reportId, 75, "Charts generated. Rendering PDF...");
var pdfUrl = await RenderPdfAsync(processed, charts);
await _hub.Clients.User(userId)
.ReportComplete(reportId, pdfUrl);
}
}
public interface IReportClient
{
Task ReportProgress(string reportId, int percentage, string status);
Task ReportComplete(string reportId, string downloadUrl);
}
Notification Persistence
Not all clients will be connected when a notification is sent. Store notifications so users can see what they missed:
public class NotificationService
{
private readonly IHubContext<NotificationHub, INotificationClient> _hub;
private readonly INotificationRepository _repository;
public NotificationService(
IHubContext<NotificationHub, INotificationClient> hub,
INotificationRepository repository)
{
_hub = hub;
_repository = repository;
}
public async Task SendAsync(string userId, Notification notification)
{
// Persist first
await _repository.SaveAsync(userId, notification);
// Then push to connected clients
await _hub.Clients.User(userId)
.ReceiveNotification(notification);
}
}
On the client side, fetch unread notifications when connecting:
connection.onreconnected(async () => {
const unread = await connection.invoke("GetUnreadNotifications");
unread.forEach(n => displayNotification(n));
});
Rate Limiting Notifications
Avoid flooding clients with too many notifications in a short period:
public class ThrottledNotificationService
{
private readonly IHubContext<NotificationHub, INotificationClient> _hub;
private readonly ConcurrentDictionary<string, DateTime> _lastSent = new();
private readonly TimeSpan _minInterval = TimeSpan.FromSeconds(1);
public ThrottledNotificationService(
IHubContext<NotificationHub, INotificationClient> hub)
{
_hub = hub;
}
public async Task<bool> TrySendAsync(
string userId, Notification notification)
{
var now = DateTime.UtcNow;
if (_lastSent.TryGetValue(userId, out var lastTime)
&& now - lastTime < _minInterval)
{
return false; // Throttled
}
_lastSent[userId] = now;
await _hub.Clients.User(userId)
.ReceiveNotification(notification);
return true;
}
}
Key Takeaways
- Use
IHubContext<THub, TClient>to send messages from background services, queue consumers, and scheduled jobs. - Create scoped services within
BackgroundServiceusingIServiceScopeFactory. - Persist notifications so offline users can catch up.
- Report progress on long-running operations for better UX.
- Throttle high-frequency notifications to avoid overwhelming clients.