Real-Time Notifications with SignalR
Overview
The BookStore application uses SignalR integrated with Wolverine to provide real-time event notifications from the backend to all connected clients. This enables instant UI updates when data changes, creating a responsive and collaborative experience.
Features
Backend (API Service)
- Wolverine SignalR Integration - Native transport for publishing events
- Event Forwarding - Domain events automatically broadcast to clients
- Transactional Guarantees - Notifications only sent after successful commits
- Cascading Messages - Handlers return notifications that Wolverine publishes
Frontend (Blazor Web)
- SignalR Client - Automatic reconnection and event handling
- Optimistic Updates - Instant UI feedback with eventual consistency
- Event Reconciliation - Confirms optimistic updates with server events
- Visual Feedback - Pulsing animation and "Saving..." badges
How It Works
1. Event Flow
User Action (Create Book)
↓
Handler Processes Command
↓
Event Stored in Marten
↓
Handler Returns Notification (Cascading Message)
↓
Wolverine Publishes to SignalR (after commit)
↓
All Connected Clients Receive Event
↓
UI Updates Automatically
2. Backend Configuration
The API is configured to use SignalR transport in Program.cs:
builder.Services.AddWolverine(opts =>
{
// Enable SignalR transport
opts.UseSignalR();
// Route notifications to SignalR
opts.Publish(x =>
{
x.MessagesImplementing<IDomainEventNotification>();
x.ToSignalR();
});
});
// Map SignalR hub
app.MapHub<WolverineHub>("/hub/bookstore");
3. Handler Implementation
Handlers return notifications as cascading messages:
public static (IResult, BookCreatedNotification) Handle(
CreateBook command,
IDocumentSession session)
{
// Create book and store event
var @event = BookAggregate.Create(...);
session.Events.StartStream<BookAggregate>(command.Id, @event);
// Create notification (published after transaction commits)
var notification = new BookCreatedNotification(
command.Id,
command.Title,
DateTimeOffset.UtcNow);
return (Results.Created(...), notification);
}
4. Frontend Integration
The Blazor frontend connects to the SignalR hub and listens for events:
// Subscribe to events
HubService.OnBookCreated += HandleBookCreated;
// Start connection
await HubService.StartAsync();
// Handle notifications
private void HandleBookCreated(BookNotification notification)
{
// Confirm optimistic update
OptimisticService.ConfirmBook(notification.EntityId);
// Refresh UI
await LoadBooksAsync();
}
Optimistic Updates
The frontend implements optimistic updates to provide instant feedback while maintaining eventual consistency.
How It Works
- Optimistic Addition - Book appears immediately in UI
- Server Processing - Backend processes command asynchronously
- SignalR Event - Notification broadcast when complete
- Reconciliation - Optimistic entry replaced with real data
Visual Feedback
Optimistic books are displayed with:
- Pulsing animation (opacity fades in/out)
- "Saving..." badge with clock icon
- Slightly transparent appearance (85% opacity)
Code Example
// Add optimistic book (instant UI update)
OptimisticService.AddOptimisticBook(
bookId,
"New Book Title",
"Author Name",
"Publisher Name");
// SignalR event arrives → automatically reconciled
// Optimistic entry removed, real data shown
Stale Entry Cleanup
If a SignalR event never arrives (network issue, error):
- Cleanup timer runs every 5 seconds
- Removes optimistic books older than 30 seconds
- Prevents UI clutter from failed operations
Benefits
1. Real-Time Collaboration
- Multiple users see changes instantly
- No manual refresh needed
- Consistent state across all clients
2. Instant Feedback
- Optimistic updates provide 0ms perceived latency
- Users see their actions immediately
- Better user experience
3. Eventual Consistency
- Backend uses async projections
- Optimistic UI bridges the gap
- SignalR events provide reconciliation
4. Reliable Delivery
- Wolverine's outbox pattern ensures delivery
- Transactional guarantees
- Automatic reconnection
Infrastructure Requirements
Development (Single Server)
- No additional infrastructure needed
- SignalR works out of the box
Production (Multi-Server)
For scale-out deployments, you need a backplane:
Option 1: Redis Backplane
builder.Services.AddSignalR()
.AddStackExchangeRedis(connectionString);
Option 2: Azure SignalR Service
builder.Services.AddSignalR()
.AddAzureSignalR(connectionString);
Option 3: Sticky Sessions
Configure load balancer for session affinity (not recommended).
Testing
Manual Testing
Start the application:
aspire runOpen two browser windows to the book catalog
Create a new book via API or admin UI
Observe: Both windows update automatically
Expected Behavior
- ✅ SignalR connection established on page load
- ✅ Book appears optimistically (pulsing)
- ✅ SignalR event received within ~100-500ms
- ✅ Optimistic entry replaced with real data
- ✅ All connected clients see the update
Troubleshooting
SignalR Connection Fails
Check browser console for errors:
// Should see: "SignalR connection started successfully"
Verify hub endpoint is accessible:
curl https://localhost:7001/hub/bookstore
Events Not Received
- Check that handler returns notification
- Verify
UseSignalR()is configured - Check
ToSignalR()routing - Review server logs for errors
Optimistic Updates Not Clearing
- Check SignalR connection is active
- Verify event IDs match
- Check cleanup timer is running
- Review browser console for errors