.NET: SignalR & Background Services
SignalR — Real-Time Communication
SignalR abstracts WebSockets, Server-Sent Events, and Long Polling behind a single Hub API. Clients can receive server pushes without polling.
// Program.cs
builder.Services.AddSignalR();
app.MapHub<ChatHub>("/hubs/chat");
// Hubs/ChatHub.cs
public class ChatHub : Hub
{
// Called by client
public async Task SendMessage(string room, string message)
{
await Clients.Group(room).SendAsync("ReceiveMessage", new {
User = Context.User?.Identity?.Name,
Message = message,
At = DateTime.UtcNow,
});
}
public async Task JoinRoom(string room)
{
await Groups.AddToGroupAsync(Context.ConnectionId, room);
await Clients.Group(room).SendAsync("UserJoined", Context.ConnectionId);
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
// cleanup
await base.OnDisconnectedAsync(exception);
}
}
// Push from outside a Hub (e.g., from a service)
public class NotificationService(IHubContext<ChatHub> hub)
{
public async Task NotifyAsync(string userId, string message) =>
await hub.Clients.User(userId).SendAsync("Notification", message);
}// TypeScript client
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr'
const connection = new HubConnectionBuilder()
.withUrl('/hubs/chat', { accessTokenFactory: () => getToken() })
.withAutomaticReconnect()
.configureLogging(LogLevel.Information)
.build()
connection.on('ReceiveMessage', (data) => {
console.log(`${data.user}: ${data.message}`)
})
await connection.start()
await connection.invoke('JoinRoom', 'general')
await connection.invoke('SendMessage', 'general', 'Hello!')Background Services
// IHostedService — runs alongside the web server
// BackgroundService — abstract base class (preferred)
public class EmailQueueProcessor : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<EmailQueueProcessor> _logger;
public EmailQueueProcessor(IServiceScopeFactory scopeFactory, ILogger<EmailQueueProcessor> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Email processor started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _scopeFactory.CreateScope(); // Scoped services need a scope
var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();
await emailService.ProcessQueueAsync(stoppingToken);
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
_logger.LogError(ex, "Error processing email queue");
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
// Register
builder.Services.AddHostedService<EmailQueueProcessor>();
// Worker Service project (console app, no HTTP)
// dotnet new worker -n MyWorkerChannels — Producer/Consumer
// System.Threading.Channels — async queue built into .NET
// Use for producer/consumer pipelines within a process
public class WorkQueue
{
private readonly Channel<WorkItem> _channel =
Channel.CreateBounded<WorkItem>(new BoundedChannelOptions(500) {
FullMode = BoundedChannelFullMode.Wait,
});
public async ValueTask EnqueueAsync(WorkItem item, CancellationToken ct = default) =>
await _channel.Writer.WriteAsync(item, ct);
public IAsyncEnumerable<WorkItem> ReadAllAsync(CancellationToken ct = default) =>
_channel.Reader.ReadAllAsync(ct);
}
// Producer (e.g., API endpoint)
await _queue.EnqueueAsync(new WorkItem(payload));
// Consumer (background service)
await foreach (var item in _queue.ReadAllAsync(stoppingToken))
{
await ProcessAsync(item);
}
// Hangfire — persistent background jobs (survives restarts)
// dotnet add package Hangfire.AspNetCore Hangfire.SqlServer
builder.Services.AddHangfire(cfg => cfg.UseSqlServerStorage(connStr));
builder.Services.AddHangfireServer();
app.MapHangfireDashboard("/hangfire");
BackgroundJob.Enqueue(() => Console.WriteLine("Fire and forget"));
BackgroundJob.Schedule(() => SendReminder(userId), TimeSpan.FromDays(1));
RecurringJob.AddOrUpdate("cleanup", () => Cleanup(), Cron.Daily);Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free