Implementing Real-Time WebSocket Communication with .NET 8 and JavaScript

In this post, I will walk you through how to build a simple WebSocket communication mechanism using .NET 8 on the backend and raw JavaScript on the frontend. We’ll implement a long-running task that sends real-time updates to the client as the task progresses. Additionally, we’ll add error handling, so the server can notify the client of any issues that arise during task execution.

Why WebSockets?

WebSockets provide a full-duplex communication channel over a single, long-lived connection. Unlike traditional HTTP, which follows a request-response pattern, WebSockets allow for two-way communication without the overhead of repeatedly opening and closing connections. This makes WebSockets perfect for real-time applications like chat applications, live updates, or—like in this case—task progress tracking.

If you’d like to see the complete code, you can check out the GitHub repository.


Project Overview

In this demo, we simulate a task that consists of several steps. As each step is completed, the server sends a progress update to the client. If an error occurs during one of the steps, the server notifies the client, which then gracefully terminates the WebSocket connection.

Key Features:

  1. Real-time progress updates: The client sees progress updates as they happen, without the need to refresh the page or send new requests.
  2. Error handling: If the server encounters an error while processing the task, the client is notified immediately, and the WebSocket connection is closed.
  3. Simplified interaction: The WebSocket allows for continuous communication, which is much more efficient than polling or repeatedly sending HTTP requests.

Setting Up the Project

The project consists of two parts:

  1. A .NET 8 Web API backend that manages the WebSocket connection and task execution.
  2. A JavaScript frontend that initiates the task and listens for updates from the server.

Backend Setup: .NET 8

In the backend, we create a simple WebSocket server that handles a task with five steps.

using System.Net.WebSockets;
using System.Text;
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseWebSockets();

app.Map("/start-task", async context =>
{
    if (context.WebSockets.IsWebSocketRequest)
    {
        var webSocket = await context.WebSockets.AcceptWebSocketAsync();
        await StartLongRunningTask(webSocket);
    }
    else
    {
        context.Response.StatusCode = 400;
    }
});

async Task StartLongRunningTask(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var taskFailed = false; // Variable to simulate task failure

    // Start processing the task with multiple steps
    for (int i = 1; i <= 5 && !taskFailed; i++)
    {
        // Simulating an error in step 3
        if (i == 3)
        {
            var errorMessage = JsonSerializer.Serialize(new { type = "error", message = "An error occurred in step 3" });
            var errorBytes = Encoding.UTF8.GetBytes(errorMessage);
            await webSocket.SendAsync(new ArraySegment<byte>(errorBytes, 0, errorBytes.Length), WebSocketMessageType.Text, true, CancellationToken.None);

            taskFailed = true; // Simulating task failure
            break; // Exit the task loop on error
        }

        // Send task progress message
        var message = Encoding.UTF8.GetBytes($"Step {i} completed");
        await webSocket.SendAsync(new ArraySegment<byte>(message, 0, message.Length), WebSocketMessageType.Text, true, CancellationToken.None);
        await Task.Delay(1000); // Simulate some delay for each step
    }

    if (!taskFailed)
    {
        var finalMessage = Encoding.UTF8.GetBytes("Task completed");
        await webSocket.SendAsync(new ArraySegment<byte>(finalMessage, 0, finalMessage.Length), WebSocketMessageType.Text, true, CancellationToken.None);
    }

    await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Task finished", CancellationToken.None);
}

app.Run();

Frontend: JavaScript

On the frontend, we use raw JavaScript to establish a WebSocket connection and listen for messages from the server. The client updates the UI with each task update and handles any errors gracefully.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Task</title>
</head>
<body>
    <h1>WebSocket Task Runner</h1>
    <button id="startButton">Start Task</button>
    <ul id="taskUpdates"></ul>

    <script>
        document.getElementById('startButton').addEventListener('click', function() {
            const socket = new WebSocket('https://localhost:7048/start-task'); // Adjust the URL as per your setup
            let hasErrorOccurred = false; // Track whether an error has occurred

            socket.onopen = function(event) {
                console.log('WebSocket connection established.');
                document.getElementById('taskUpdates').innerHTML += '<li>Task started...</li>';
            };

            socket.onmessage = function(event) {
                console.log('Message from server:', event.data);

                // Try parsing the message as JSON to check if it's an error message
                try {
                    const data = JSON.parse(event.data);

                    if (data.type === "error") {
                        console.error('Error from server:', data.message);
                        document.getElementById('taskUpdates').innerHTML += `<li style="color:red;">Error: ${data.message}</li>`;
                        hasErrorOccurred = true; // Mark that an error has occurred
                        socket.close(); // Close the connection when an error occurs
                    } else {
                        // If it's not an error, display the normal task progress
                        document.getElementById('taskUpdates').innerHTML += `<li>${event.data}</li>`;
                    }
                } catch (e) {
                    // If the message is not JSON, just treat it as regular task output
                    document.getElementById('taskUpdates').innerHTML += `<li>${event.data}</li>`;
                }
            };

            socket.onclose = function(event) {
                if (hasErrorOccurred) {
                    console.log('WebSocket connection closed due to an error.');
                    document.getElementById('taskUpdates').innerHTML += '<li>Task terminated due to an error.</li>';
                } else {
                    console.log('WebSocket connection closed.');
                    document.getElementById('taskUpdates').innerHTML += '<li>Task finished successfully!</li>';
                }
            };

            socket.onerror = function(error) {
                console.error('WebSocket error observed:', error);
                document.getElementById('taskUpdates').innerHTML += '<li>Connection error occurred!</li>';
                hasErrorOccurred = true; // Handle WebSocket-level errors
                socket.close();
            };
        });
    </script>
</body>
</html>

How It Works

  • The client initiates the connection by clicking the “Start Task” button. This establishes a WebSocket connection to the backend.
  • As the server processes each task step, it sends real-time updates to the client.
  • If an error occurs (simulated in step 3), the server sends an error message, and the client handles it by displaying the error and closing the WebSocket connection.
  • The client gracefully closes the connection either after completing the task or when an error is encountered.

When to Use WebSockets

WebSockets are ideal for scenarios where you need to send frequent updates from the server to the client without reopening connections, such as:

  • Live notifications (e.g., chat applications, real-time stock updates).
  • Progress tracking (e.g., file uploads, long-running tasks).
  • Gaming or collaborative applications where immediate feedback is crucial.

Conclusion

WebSockets offer an efficient way to build real-time, two-way communication between a server and a client. By handling errors and task progress updates as they happen, you can provide users with a responsive experience, which is especially important in modern web applications.

Feel free to adapt the example to your specific needs, and don’t forget to explore the full code on GitHub.

Scroll to Top