This document describes the implementation of real-time task updates using SignalR and Fluxor in the FocusFlow application. The architecture enables live updates across multiple browser sessions when tasks are created, updated, or deleted.
📁 FocusFlow.Application/
├── Common/
│ └── Events/
│ └── IEventPublisher.cs ✅ Interface (abstraction)
└── Features/
└── Tasks/
├── CreateTask/
│ └── CreateTaskCommandHandler.cs ✅ Uses IEventPublisher
├── UpdateTask/
│ └── UpdateTaskCommandHandler.cs ✅ Uses IEventPublisher
├── UpdateTaskStatus/
│ └── UpdateTaskStatusCommandHandler.cs ✅ Uses IEventPublisher
└── DeleteTask/
└── DeleteTaskCommandHandler.cs ✅ Uses IEventPublisher
📁 FocusFlow.Infrastructure/
├── SignalR/
│ └── SignalREventPublisher.cs ✅ Implementation of IEventPublisher
└── DependencyInjection.cs ✅ Service registration
📁 FocusFlow.WebApi/
├── Hubs/
│ └── TasksHub.cs ✅ SignalR Hub endpoint
└── Program.cs ✅ Hub registration & CORS
📁 FocusFlow.BlazorApp/
├── Shared/
│ ├── Models/SignalR/
│ │ └── TaskNotifications.cs ✅ Shared DTOs
│ ├── Services/SignalR/
│ │ ├── ISignalRService.cs ✅ Client service interface
│ │ ├── SignalRService.cs ✅ SignalR client implementation
│ │ └── SignalRTasksListener.cs ✅ SignalR → Fluxor bridge
│ └── Components/
│ └── SignalRConnectionManager.razor ✅ Connection lifecycle
├── Features/
│ ├── Tasks/List/Store/
│ │ ├── TasksListActions.cs ✅ SignalR actions
│ │ ├── TasksListEffects.cs ✅ SignalR effects
│ │ └── TasksListReducers.cs ✅ SignalR reducers
│ └── Projects/Detail/Store/
│ ├── ProjectDetailActions.cs ✅ SignalR actions
│ ├── ProjectDetailEffects.cs ✅ SignalR effects
│ └── ProjectDetailReducers.cs ✅ SignalR reducers
├── Services/
│ ├── ITokenProvider.cs ✅ Authentication interface
│ └── TokenProvider.cs ✅ JWT token management
├── Auth/
│ └── BlazorAuthenticationHandler.cs ✅ SignalR authentication
├── Components/
│ └── App.razor ✅ SignalR initialization
└── Program.cs ✅ Service registration
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Browser Tab 1 │ │ Browser Tab 2 │ │ Browser Tab N │
│ │ │ │ │ │
│ ┌─────────────┐│ │ ┌─────────────┐│ │ ┌─────────────┐│
│ │ Fluxor Store││ │ │ Fluxor Store││ │ │ Fluxor Store││
│ └─────────────┘│ │ └─────────────┘│ │ └─────────────┘│
│ ▲ │ │ ▲ │ │ ▲ │
│ │ │ │ │ │ │ │ │
│ ┌─────────────┐│ │ ┌─────────────┐│ │ ┌─────────────┐│
│ │SignalRTasks ││ │ │SignalRTasks ││ │ │SignalRTasks ││
│ │ Listener ││ │ │ Listener ││ │ │ Listener ││
│ └─────────────┘│ │ └─────────────┘│ │ └─────────────┘│
│ ▲ │ │ ▲ │ │ ▲ │
│ │ │ │ │ │ │ │ │
│ ┌─────────────┐│ │ ┌─────────────┐│ │ ┌─────────────┐│
│ │SignalR Client│ │ │SignalR Client│ │ │SignalR Client│
│ └─────────────┘│ │ └─────────────┘│ │ └─────────────┘│
└─────────▲───────┘ └─────────▲───────┘ └─────────▲───────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
┌─────────────┴─────────────┐
│ SignalR Hub │
│ (TasksHub) │
└─────────────▲─────────────┘
│
┌─────────────┴─────────────┐
│ SignalREventPublisher │
│ (IEventPublisher) │
└─────────────▲─────────────┘
│
┌─────────────┴─────────────┐
│ Command Handlers │
│ (Application Layer) │
└───────────────────────────┘
Location: src/FocusFlow.Application/Common/Events/IEventPublisher.cs
public interface IEventPublisher
{
Task PublishTaskCreatedAsync(Guid taskId, Guid projectId);
Task PublishTaskUpdatedAsync(Guid taskId, Guid projectId);
Task PublishTaskStatusChangedAsync(Guid taskId, Guid projectId, ProjectTaskStatus newStatus);
Task PublishTaskDeletedAsync(Guid taskId, Guid projectId);
}Purpose: Abstraction for publishing task-related events. Allows the Application layer to publish events without knowing about SignalR implementation details.
Location: src/FocusFlow.Infrastructure/SignalR/SignalREventPublisher.cs
Purpose: Concrete implementation of IEventPublisher that sends SignalR notifications to connected clients.
Key Features:
- Uses
IHubContext<TasksHub>to send messages - Sends notifications to specific project groups
- Handles connection errors gracefully
Location: src/FocusFlow.WebApi/Hubs/TasksHub.cs
[Authorize]
public class TasksHub : Hub
{
public async Task JoinProjectAsync(string projectId)
public async Task LeaveProjectAsync(string projectId)
}Purpose: SignalR Hub that manages client connections and project-based groups.
Key Features:
- Requires authentication (
[Authorize]) - Manages project-based groups for targeted notifications
- Clients join/leave project groups based on current page
Modified Files:
CreateTaskCommandHandler.csUpdateTaskCommandHandler.csUpdateTaskStatusCommandHandler.csDeleteTaskCommandHandler.cs
Integration Pattern:
public async Task<Result> Handle(CreateTaskCommand request, CancellationToken cancellationToken)
{
// ... business logic
// Publish SignalR event
await _eventPublisher.PublishTaskCreatedAsync(task.Id, task.ProjectId);
return Result.Success();
}Location: src/FocusFlow.BlazorApp/Shared/Services/SignalR/ISignalRService.cs
Purpose: Abstraction for SignalR client operations.
Location: src/FocusFlow.BlazorApp/Shared/Services/SignalR/SignalRService.cs
Key Features:
- Manages SignalR connection lifecycle
- Handles authentication with JWT tokens
- Provides project group management
- Exposes C# events for task notifications
- Automatic reconnection handling
Event Definitions:
public event Func<TaskCreatedNotification, Task>? OnTaskCreated;
public event Func<TaskUpdatedNotification, Task>? OnTaskUpdated;
public event Func<TaskStatusChangedNotification, Task>? OnTaskStatusChanged;
public event Func<TaskDeletedNotification, Task>? OnTaskDeleted;Location: src/FocusFlow.BlazorApp/Shared/Services/SignalR/SignalRTasksListener.cs
Purpose: Bridge between SignalR events and Fluxor actions. This is the key component that maintains unidirectional data flow.
Architecture Pattern:
private Task HandleTaskCreated(TaskCreatedNotification notification)
{
// Convert SignalR notification → Fluxor actions
_dispatcher.Dispatch(new TasksListActions.TaskCreatedFromSignalRAction(
notification.TaskId, notification.ProjectId));
_dispatcher.Dispatch(new TaskCreatedInProjectFromSignalRAction(
notification.TaskId, notification.ProjectId));
return Task.CompletedTask;
}Why This Pattern?:
- Keeps Fluxor as single source of truth
- Maintains unidirectional data flow
- SignalR updates go through same flow as user actions
- Enables time-travel debugging (Fluxor DevTools work!)
Files:
TasksListActions.cs- SignalR-specific actionsTasksListEffects.cs- Effects that fetch updated dataTasksListReducers.cs- State updates
Files:
ProjectDetailActions.cs- SignalR-specific actionsProjectDetailEffects.cs- Effects that fetch updated dataProjectDetailReducers.cs- State updates
Dual State Update Pattern:
The SignalRTasksListener dispatches actions to both states simultaneously, ensuring real-time updates work on both /tasks and /projects/{id} pages.
Location: src/FocusFlow.BlazorApp/Services/TokenProvider.cs
Key Changes:
- Added async methods for
ProtectedLocalStorage - Static cache for token sharing across scoped instances
- Thread-safe initialization with lazy loading
Location: src/FocusFlow.BlazorApp/Auth/BlazorAuthenticationHandler.cs
Purpose: Ensures SignalR connections use proper JWT authentication.
User creates task in Tab 1
↓
CreateTaskCommand → Handler
↓
Task saved to database
↓
SignalREventPublisher.PublishTaskCreatedAsync()
↓
TasksHub sends notification to project group
↓
All connected clients receive notification
↓
SignalRTasksListener converts to Fluxor actions
↓
Effects fetch updated task data from API
↓
Reducers update both TasksList and ProjectDetail states
↓
UI re-renders in all tabs showing new task
SignalR Notification
↓
SignalRTasksListener.HandleTaskCreated()
↓
Dispatch TaskCreatedFromSignalRAction (TasksList)
Dispatch TaskCreatedInProjectFromSignalRAction (ProjectDetail)
↓
TasksListEffects.HandleTaskCreatedFromSignalR()
ProjectDetailEffects.HandleTaskCreatedInProjectFromSignalR()
↓
API calls to fetch fresh task data
↓
Dispatch success actions with task data
↓
Reducers update states
↓
UI components re-render with new data
Current Approach: Events published directly from Command Handlers
// In Handler
await _eventPublisher.PublishTaskCreatedAsync(task.Id, task.ProjectId);Alternative: Domain Events from Entities
// In Entity
public void Complete()
{
Status = TaskStatus.Completed;
AddDomainEvent(new TaskCompletedEvent(Id));
}Decision Rationale:
- Simplicity: Less infrastructure code required
- Direct Control: Explicit control over when events are published
- Faster Implementation: Quicker to implement and test
- Sufficient for Current Needs: Meets requirements without over-engineering
Problem: Users can be on different pages:
/tasks- displays tasks fromTasksListState/projects/{id}- displays tasks fromProjectDetailState.Project.Tasks
Solution: SignalRTasksListener dispatches to both states
// Update both states simultaneously
_dispatcher.Dispatch(new TasksListActions.TaskCreatedFromSignalRAction(...));
_dispatcher.Dispatch(new TaskCreatedInProjectFromSignalRAction(...));Benefits:
- Real-time updates work on all pages
- No duplicate SignalR connections needed
- Consistent user experience
Alternative: Direct SignalR → Component updates
// BAD: Direct updates
signalRService.OnTaskCreated += (notification) => {
// Update component state directly
tasks.Add(newTask);
StateHasChanged();
};Current: SignalR → Fluxor Actions → State Updates
// GOOD: Through Fluxor
signalRService.OnTaskCreated += (notification) => {
_dispatcher.Dispatch(new TaskCreatedFromSignalRAction(...));
};Benefits:
- Maintains unidirectional data flow
- Fluxor DevTools work for debugging
- Consistent state management patterns
- Easier testing and reasoning about state changes
// Add SignalR
builder.Services.AddSignalR();
// Map hub
app.MapHub<TasksHub>("/hubs/tasks");services.AddScoped<IEventPublisher, SignalREventPublisher>();// Register SignalR services
builder.Services.AddScoped<ISignalRService, SignalRService>();
builder.Services.AddScoped<SignalRTasksListener>();<!-- Initialize SignalR connection and listener -->
<SignalRConnectionManager />- Unit Tests: Command handlers with mocked
IEventPublisher - Integration Tests: End-to-end SignalR message flow
- Hub Tests: Connection and group management
- Unit Tests: Fluxor actions, effects, and reducers
- Component Tests: UI updates with mocked SignalR service
- E2E Tests: Multi-tab real-time update scenarios
- Project Groups: Only users viewing a project receive its notifications
- Automatic Cleanup: Connections cleaned up when users navigate away
- Reconnection: Automatic reconnection on connection loss
- Efficient Reducers: Only update necessary parts of state
- Duplicate Prevention: Check for existing tasks before adding
- Lazy Loading: Fetch full task data only when needed
- JWT Tokens: SignalR connections authenticated with JWT
- Hub Authorization:
[Authorize]attribute on TasksHub - Project-based Groups: Users only receive notifications for projects they have access to
- Server-side Validation: All task operations validated on server
- Client-side Sync: Client state synced with server data via API calls
- Structured Logging: Comprehensive logging throughout the flow
- SignalR Events: Log all SignalR events with task and project IDs
- Connection Status: Log connection/disconnection events
- Fluxor DevTools: Time-travel debugging for state changes
- SignalR Logging: Enable detailed SignalR client logging
- Browser DevTools: Monitor WebSocket connections
- Domain Events: Refactor to use proper Domain Events pattern
- Event Sourcing: Store all events for audit trail
- Offline Support: Queue events when connection is lost
- Optimistic Updates: Update UI immediately, sync with server later
- Selective Updates: Only send changed fields, not full objects
- Redis Backplane: For multi-server deployments
- Connection Pooling: Optimize connection management
- Message Batching: Batch multiple updates for efficiency
- Caching Strategy: Cache frequently accessed data
- Check JWT token validity
- Verify hub endpoint configuration
- Check CORS settings
- Verify client is in correct project group
- Check SignalR connection status
- Verify event publisher is called in handlers
- Check Fluxor state updates in DevTools
- Verify component subscribes to correct state
- Check for JavaScript errors in console
- Verify TokenProvider has valid token
- Check BlazorAuthenticationHandler configuration
- Verify JWT token format and claims
This SignalR + Fluxor architecture provides a robust, scalable solution for real-time task updates in the FocusFlow application. The bridge pattern maintains clean separation of concerns while enabling seamless real-time collaboration across multiple browser sessions.
The implementation prioritizes simplicity and maintainability while providing a solid foundation for future enhancements.