Exercise - Shared Live Counter
Shared Live Counter: Real-Time Updates with SignalR, DI & Logging in Blazor Server
We'll build a "Shared Live Counter" where multiple browser tabs/users can see the same counter increment in real-time.
Simple Demo: Shared Live Counter (SignalR + DI + Logging)
Goal:
- Demonstrate SignalR: All connected clients see the same counter update instantly.
- Demonstrate Dependency Injection: A central service manages the counter's state and is easily accessed.
- Demonstrate Blazor Logging: Track events on both the server and client (browser console).
Project Type: Blazor Server App (This simplifies SignalR setup as the server is already hosting Blazor).
Step 1: Create the Blazor Server Project
- Open Visual Studio.
- Select "Create a new project".
- Choose "Blazor Server App" and click "Next".
- Name the project LiveCounterDemo.
- Click "Create".
Step 2: Server-Side: Implement the Shared Counter Service (for DI)
This service will hold our shared counter value and notify anyone interested when it changes. We'll use DI to make it available throughout the server.
2.1. Create Services Folder and SharedCounterService.cs:
In your LiveCounterDemo project, create a new folder called Services. Inside this folder, add a new class file named SharedCounterService.cs.
LiveCounterDemo/Services/SharedCounterService.cs:
using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; namespace LiveCounterDemo.Services { // Analogy: This is our 'scorekeeper'. It holds the actual score and announces changes. public class SharedCounterService { private int _currentCount = 0; private readonly ILogger<SharedCounterService> _logger; // Injected Logger // Event that others can subscribe to when the counter changes public event Func<int, Task>? OnCounterUpdate; public SharedCounterService(ILogger<SharedCounterService> logger) { _logger = logger; _logger.LogInformation("SharedCounterService initialized. Initial count: {Count}", _currentCount); } public int GetCurrentCount() { _logger.LogDebug("GetCurrentCount called. Current count: {Count}", _currentCount); return _currentCount; } public async Task IncrementCounter() { _currentCount++; _logger.LogInformation("Counter incremented to: {Count}", _currentCount); // Announce the new score to anyone listening (via the OnCounterUpdate event) if (OnCounterUpdate != null) { // We use Task.Run to avoid blocking the caller if there are many subscribers await Task.Run(() => OnCounterUpdate.Invoke(_currentCount)); _logger.LogDebug("OnCounterUpdate event invoked for count: {Count}", _currentCount); } } } }
2.2. Register the Service in Program.cs:
Now, we tell Blazor's Dependency Injection system about our SharedCounterService. We'll register it as a Singleton so that all users/connections share the exact same instance of the counter.
Open Program.cs in the root of your LiveCounterDemo project.
LiveCounterDemo/Program.cs (additions highlighted):
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; // This might be there by default, keep it if present using LiveCounterDemo.Data; using LiveCounterDemo.Services; // Add this using statement using LiveCounterDemo.Hubs; // Add this using statement for the Hub below using Microsoft.Extensions.Logging; // Add this for logging config var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddSingleton<WeatherForecastService>(); // Existing service // Register our SharedCounterService as a Singleton. // Analogy: We're telling the house's central management system to always provide the SAME scorekeeper. builder.Services.AddSingleton<SharedCounterService>(); // Add SignalR services // Analogy: We're installing the broadcast system into our house. builder.Services.AddSignalR(); // Configure Logging: // Analogy: Setting up our recording cameras. builder.Logging.ClearProviders(); // Clear default console provider if you want to explicitly control builder.Logging.AddConsole(); // Log to the console (where your server runs) builder.Logging.AddDebug(); // Log to the debug output window in Visual Studio // Set minimum log levels for specific categories. builder.Logging.SetMinimumLevel(LogLevel.Debug); // Default to Debug for everything builder.Logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Debug); // Be verbose about SignalR builder.Logging.AddFilter("LiveCounterDemo.Services.SharedCounterService", LogLevel.Information); // Our service builder.Logging.AddFilter("LiveCounterDemo.Components.Pages.Counter", LogLevel.Information); // Our Blazor component var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.MapBlazorHub(); // Needed for Blazor Server's SignalR connection app.MapFallbackToPage("/_Host"); // Map our custom SignalR Hub // Analogy: We're giving our broadcast system a specific channel name: '/counterhub' app.MapHub<CounterHub>("/counterhub"); // Map the hub here after MapBlazorHub and before MapFallbackToPage app.Run();
Step 3: Server-Side: Create the SignalR Hub
This hub will listen for client requests to increment the counter and broadcast updates.
3.1. Create Hubs Folder and CounterHub.cs:
In your LiveCounterDemo project, create a new folder called Hubs. Inside this folder, add a new class file named CounterHub.cs.
LiveCounterDemo/Hubs/CounterHub.cs:
using Microsoft.AspNetCore.SignalR; using LiveCounterDemo.Services; // Add this using statement using System; using System.Threading.Tasks; using Microsoft.Extensions.Logging; // For logging namespace LiveCounterDemo.Hubs { // Analogy: This is the 'broadcaster'. It takes requests to update the score and sends out the new score. public class CounterHub : Hub { private readonly SharedCounterService _counterService; // Injected service private readonly ILogger<CounterHub> _logger; // Injected Logger public CounterHub(SharedCounterService counterService, ILogger<CounterHub> logger) { _counterService = counterService; _logger = logger; // Subscribe to the counter service's update event // Analogy: The broadcaster listens to the scorekeeper. _counterService.OnCounterUpdate -= OnCounterUpdateHandler; // Prevent duplicate subscriptions _counterService.OnCounterUpdate += OnCounterUpdateHandler; _logger.LogInformation("CounterHub initialized and subscribed to SharedCounterService updates."); } // Method clients can call to get the current count public Task GetCurrentCount() { _logger.LogInformation("Client {ConnectionId} requested current count.", Context.ConnectionId); return Clients.Caller.SendAsync("ReceiveCounterUpdate", _counterService.GetCurrentCount()); } // Method clients can call to increment the count public async Task IncrementCounter() { _logger.LogInformation("Client {ConnectionId} requested counter increment.", Context.ConnectionId); await _counterService.IncrementCounter(); // Let the service handle the increment // The service's event will then trigger the broadcast below. } // This method is called when the SharedCounterService notifies of an update private async Task OnCounterUpdateHandler(int newCount) { _logger.LogInformation("Broadcasting new count: {NewCount} to all clients.", newCount); // Analogy: The broadcaster sends the new score to everyone connected. await Clients.All.SendAsync("ReceiveCounterUpdate", newCount); } public override Task OnConnectedAsync() { _logger.LogInformation("Client {ConnectionId} connected to CounterHub.", Context.ConnectionId); // When a new client connects, send them the current count immediately return Clients.Caller.SendAsync("ReceiveCounterUpdate", _counterService.GetCurrentCount()); } public override Task OnDisconnectedAsync(Exception? exception) { _logger.LogInformation("Client {ConnectionId} disconnected from CounterHub. Exception: {ExceptionMessage}", Context.ConnectionId, exception?.Message); return base.OnDisconnectedAsync(exception); } } }
Step 4: Client-Side: Modify the Blazor Counter Component
We'll modify the default Counter.razor page to connect to our SignalR Hub and display the shared count.
4.1. Install SignalR Client Package:
In your LiveCounterDemo project, right-click on the LiveCounterDemo project (the client part, not the solution). Select "Manage NuGet Packages...". Search for Microsoft.AspNetCore.SignalR.Client and install it.
4.2. Modify Components/Pages/Counter.razor:
Open Components/Pages/Counter.razor.
LiveCounterDemo/Components/Pages/Counter.razor:
@page "/counter" @inject NavigationManager NavigationManager // Used to get the base URL for SignalR connection @inject ILogger<Counter> Logger // Injected Logger @using Microsoft.AspNetCore.SignalR.Client // Required for HubConnection @implements IAsyncDisposable // To dispose the HubConnection properly <PageTitle>Counter</PageTitle> <h1>Shared Live Counter</h1> <p>This counter is shared across all connected clients. Increment it and see it update everywhere!</p> <p role="status">Current count: <b>@_currentCount</b></p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int _currentCount = 0; private HubConnection? _hubConnection; // Our SignalR connection object protected override async Task OnInitializedAsync() { Logger.LogInformation("Counter component initialized. Setting up SignalR."); // Analogy: Our score display board (the Blazor component) connects to the broadcast channel. _hubConnection = new HubConnectionBuilder() .WithUrl(NavigationManager.ToAbsoluteUri("/counterhub")) // Connect to the hub we mapped in Program.cs .WithAutomaticReconnect() // Automatically try to reconnect if connection drops .Build(); // Analogy: We tell the score display board, "When the broadcaster sends 'ReceiveCounterUpdate', update my display." _hubConnection.On<int>("ReceiveCounterUpdate", (newCount) => { _currentCount = newCount; Logger.LogInformation("Received counter update from server: {NewCount}", newCount); InvokeAsync(StateHasChanged); // Tell Blazor to re-render the UI with the new count }); // Start the connection try { await _hubConnection.StartAsync(); Logger.LogInformation("SignalR connection started successfully to /counterhub."); // Request the current count immediately when connected (this will also be handled by OnConnectedAsync in hub) await _hubConnection.SendAsync("GetCurrentCount"); Logger.LogDebug("Requested current count from hub."); } catch (Exception ex) { Logger.LogError(ex, "Error starting SignalR connection."); } } private async Task IncrementCount() { Logger.LogInformation("Increment button clicked. Sending increment request to hub."); // Analogy: We tell the score display board to tell the broadcaster, "Increment the score!" if (_hubConnection != null && _hubConnection.State == HubConnectionState.Connected) { await _hubConnection.SendAsync("IncrementCounter"); } else { Logger.LogWarning("SignalR connection not active. Cannot send increment request."); } } // Important: Dispose the HubConnection when the component is removed public async ValueTask DisposeAsync() { if (_hubConnection is not null) { await _hubConnection.DisposeAsync(); Logger.LogInformation("SignalR connection disposed."); } } }
Step 5: Run the Demo and Observe
- Set
LiveCounterDemoas the startup project (it should be by default). - Run the application (
F5orCtrl+F5). - A browser tab will open displaying your Blazor app.
- Navigate to the "Counter" page (or it might be the default home page).
- Open multiple browser tabs or even different browsers and navigate to the same
http://localhost:XXXX/counterURL. - Click the "Click me" button on any of the tabs.
- Observe: All other tabs will instantly update their counter value without refreshing!
Where to see the Logs:
- Server-side Logs: In Visual Studio, go to
View > Output. In the "Show output from:" dropdown, select "Web Server" or "ASP.NET Core Web Server". You'll see logs fromSharedCounterService,CounterHub, and various SignalR messages. - Client-side Logs: In your browser, open the Developer Tools (usually by pressing
F12). Go to the "Console" tab. You'll see logs from yourCountercomponent and browser-side SignalR activities.
Explanation of Concepts in this Demo:
SignalR:
- The Hub (
CounterHub.cs): This is the central communication point on the server. Clients connect to it. It has methods (IncrementCounter,GetCurrentCount) that clients can call, and it usesClients.All.SendAsync("ReceiveCounterUpdate", ...)to broadcast messages to all connected clients. - The Client Connection (
HubConnectioninCounter.razor): The Blazor component establishes a WebSocket connection to the/counterhubendpoint on the server. _hubConnection.On<int>("ReceiveCounterUpdate", ...): This tells the client to listen for a message named "ReceiveCounterUpdate" from the server. When it receives one, it updates the local counter variable and triggers a UI refresh (StateHasChanged)._hubConnection.SendAsync("IncrementCounter"): This is how the client tells the server'sCounterHubto run itsIncrementCountermethod.
Dependency Injection (DI):
SharedCounterService: This is our custom service. It's a plain C# class that encapsulates the logic for managing the counter. It doesn't know anything about Blazor components or SignalR Hubs; it just holds the count and raises an event when it changes.- Registration in
Program.cs(builder.Services.AddSingleton<SharedCounterService>();): We registerSharedCounterServiceas aSingleton. This is crucial: it means only one instance of this service exists for the entire application's lifetime, ensuring all users share the exact same counter value. - Injection (
private readonly SharedCounterService _counterService;inCounterHub.cs, and its constructor): When Blazor (or the SignalR framework) needs to create an instance ofCounterHub, the DI container automatically "injects" the single instance ofSharedCounterServiceinto its constructor. TheCounterHubthen uses this injected service to interact with the counter logic. TheSharedCounterServicealso hasILoggerinjected into its constructor.
Blazor Logging:
ILogger<T>Interface: This is the standard logging interface in .NET.- Injection (
@inject ILogger<Counter> Loggerin.razorfiles, orILogger<SomeClass> loggerin constructors): You simply declare that you need anILoggerfor your component or service, and the DI container provides it. The<T>(e.g.,ILogger<Counter>) specifies the "category" of the logger, making it easier to filter logs. - Log Methods (
Logger.LogInformation,Logger.LogDebug,Logger.LogError,Logger.LogWarning): These methods allow you to emit log messages at different severity levels. - Configuration in
Program.cs(builder.Logging.AddConsole(),builder.Logging.SetMinimumLevel(),builder.Logging.AddFilter()): This is where you configure where logs go (e.g., console, debug output) and what minimum severity level to log for different parts of your application. This allows you to control verbosity. - Observation: Logs appear in the server's console/output and the client's browser developer console, providing valuable insights for debugging and monitoring.
This simple demo effectively ties together these three fundamental concepts, showing how they cooperate to create a dynamic, real-time Blazor application.
Comments
Post a Comment