Friday, August 8, 2025

Real-Time Progress Updates with ASP.NET Core and SignalR

Real-Time Progress Updates with ASP.NET Core and SignalR

In this article, we'll walk through building a simple web application to demonstrate real-time progress updates for a long-running server task using ASP.NET Core and SignalR. This is a common requirement in web applications where you need to provide feedback to the user for operations like file uploads, data processing, or report generation.

Prerequisites

Before you begin, ensure you have the following installed:

Project Setup

First, let's create a new ASP.NET Core Web API project. Open your terminal and run the following commands:

dotnet new webapi -n SignalRProgressDemo
cd SignalRProgressDemo

Next, we need to add the necessary SignalR package.

dotnet add package Microsoft.Azure.SignalR

This will set up a basic project structure for us to build upon.

Project Structure

Here is the folder structure of our SignalRProgressDemo project:

SignalRProgressDemo/
├── Controllers/
│   └── InsertController.cs
├── Properties/
│   └── launchSettings.json
├── wwwroot/
│   └── index.html
├── appsettings.Development.json
├── appsettings.json
├── Program.cs
├── ProgressHub.cs
├── SignalRProgressDemo.csproj
└── SignalRProgressDemo.sln

File-by-File Breakdown

Here’s a detailed look at each of the files we'll be creating or modifying.

1. Program.cs

This is the entry point of our application. We need to configure services for SignalR and controllers, and map the SignalR hub and controller endpoints.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddSignalR();
//builder.Services.AddSignalR().AddAzureSignalR("<connection_string>");
//builder.Services.AddSignalR().
AddAzureSignalR(builder.Configuration["AzureSignalR:ConnectionString"]);
builder.Services.AddControllers(); builder.Services.AddOpenApi(); var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles(); app.MapHub<SignalRProgressDemo.ProgressHub>("/progressHub"); app.MapControllers(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; app.MapGet("/weatherforecast", () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); return forecast; }) .WithName("GetWeatherForecast"); app.Run(); record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); }

2. ProgressHub.cs

This is our SignalR hub. A hub is a class that serves as a high-level pipeline that handles client-server communication. It has methods that can be called by clients, and the server can also invoke methods on the clients.

Create a new file ProgressHub.cs and add the following code:

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRProgressDemo
{
    public class ProgressHub : Hub
    {
        // Method to send progress updates to clients
        public async Task SendProgress(int percent)
        {
            await Clients.All.SendAsync("ProgressUpdate", percent);
        }
    }
}

3. Controllers/InsertController.cs

This API controller will simulate a long-running task. When the InsertTables endpoint is called, it will loop and send progress updates to the clients via the ProgressHub.

Create a Controllers folder if it doesn't exist, and add a new file InsertController.cs with the following code:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRProgressDemo.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class InsertController : ControllerBase
    {
        private readonly IHubContext<ProgressHub> _hubContext;

        public InsertController(IHubContext<ProgressHub> hubContext)
        {
            _hubContext = hubContext;
        }

        [HttpPost]
        public async Task<IActionResult> InsertTables()
        {
            int totalTables = 10;
            for (int i = 1; i <= totalTables; i++)
            {
                // Simulate table insert (replace with actual DB logic)
                await Task.Delay(1000); // Simulate time-consuming insert
                int percent = (i * 100) / totalTables;
                var message = $"Table {i} of {totalTables} inserted.";
                await _hubContext.Clients.All.SendAsync("ProgressUpdate", percent, message);
            }
            await _hubContext.Clients.All.SendAsync("ProgressUpdate", 100, "All tables inserted successfully.");
            return Ok(new { message = "All tables inserted." });
        }
    }
}

4. wwwroot/index.html

This is our client-side application. It's a simple HTML page with JavaScript to connect to the SignalR hub, display a progress bar, and provide a button to start the server-side task.

Create a wwwroot folder in your project root, and inside it, create an index.html file:

<!DOCTYPE html>
<html>
<head>
    <title>SignalR Progress Demo</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/7.0.5/signalr.min.js"></script>
    <style>
        #progressBar {
            width: 100%;
            background-color: #eee;
        }
        #progress {
            width: 0%;
            height: 30px;
            background-color: #4caf50;
            text-align: center;
            color: white;
        }
    </style>
</head>
<body>
    <h2>Insert Progress</h2>
    <div id="progressBar">
        <div id="progress">0%</div>
    </div>
    <div id="message"></div>
    <button onclick="startInsert()">Start Insert</button>
    <script>
        const connection = new signalR.HubConnectionBuilder()
            .withUrl("/progressHub")
            .build();

        connection.on("ProgressUpdate", function (percent, message) {
            const progress = document.getElementById("progress");
            const messageDiv = document.getElementById("message");
            progress.style.width = percent + "%";
            progress.textContent = percent + "%";
            messageDiv.textContent = message;
        });

        async function startInsert() {
            await fetch("/api/insert", { method: "POST" });
        }

        connection.start().catch(function (err) {
            return console.error(err.toString());
        });
    </script>
</body>
</html>

5. SignalRProgressDemo.csproj

This is the project file. It should include the package reference for SignalR.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
    <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
    <PackageReference Include="Microsoft.Azure.SignalR" Version="1.31.0" />
  </ItemGroup>

</Project>

6. appsettings.json

This file contains configuration settings for the application. For this demo, we can also include the Azure SignalR connection string if we were to deploy it.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AzureSignalR": {
    "ConnectionString": "Endpoint=https://rg2signalrr.service.signalr.net;AccessKey=;Version=1.0;"
  }
}

7. Properties/launchSettings.json

This file configures how the application is launched for development.

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": false,
      "applicationUrl": "http://localhost:5186",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

How It Works

  1. Client Connection: When the index.html page is loaded, the JavaScript code establishes a connection to our ProgressHub on the server.
  2. Initiating the Task: The user clicks the "Start Insert" button, which sends a POST request to the /api/insert endpoint.
  3. Server-Side Processing: The InsertController receives the request and starts a simulated long-running task.
  4. Broadcasting Progress: Inside the loop, the controller calculates the progress percentage and uses the IHubContext<ProgressHub> to send a "ProgressUpdate" message to all connected clients.
  5. Client-Side Update: The JavaScript on the client side is listening for the "ProgressUpdate" message. When it receives one, it updates the width and text of the progress bar.

Running the Project

To run the project, open your terminal in the project's root directory and execute:

dotnet run

Then, open your web browser and navigate to http://localhost:5186 (the port may vary, check the terminal output). You should see the "Insert Progress" page. Click the "Start Insert" button to see the progress bar update in real-time.

Conclusion

You've successfully built a web application that provides real-time progress updates using ASP.NET Core and SignalR. This pattern is incredibly useful for improving user experience in applications with long-running background tasks. From here, you could extend this project to handle real database operations, file processing, or any other asynchronous task.

OutPut:







No comments:

Post a Comment

Featured Post

Automating Azure DevOps Task Tracking: A Complete PowerShell Solution

Automating Azure DevOps Task Tracking: A Complete PowerShell Solution param (     [ Parameter ( Mandatory = $false )]     [ Alias ( 'F...

Popular posts