Skip to content

Using Server-Sent-Events (SSE) in .NET

TLDR

  • SSE is a technology for one-way data pushing from Server to Client, suitable for real-time update scenarios.
  • Compared to Polling, SSE is more resource-efficient; compared to WebSocket, it is more lightweight (unidirectional transmission only).
  • The Server must set the Content-Type to text/event-stream and Cache-Control to no-cache.
  • Message format must end with data: <content>\n\n, where two newline characters signify the end of a message.
  • When using EventSource, if cross-origin authentication is required, set withCredentials: true and ensure the Server is correctly configured for CORS.

Example Project

Executable example for this article: CloudyWing/SseProgressSample.

The example has been rewritten using .NET 10, including CancellationToken handling and progress reporting for long-running tasks. It is recommended to read it alongside this article.

Comparison of SSE and Other Real-time Communication Methods

When developing real-time communication features, common technical choices include:

  • Polling: The Client sends requests to the Server at regular intervals. The disadvantage is that it generates a large number of invalid requests, consuming server resources.
  • Server-Sent-Events (SSE): Data is pushed unidirectionally from the Server to the Client. The advantage is that it only requires a single TCP connection, effectively reducing server load and bandwidth waste.
  • WebSocket: Establishes a bidirectional connection where both parties can send data at any time. Suitable for scenarios requiring frequent bidirectional interaction.

SSE Implementation in JavaScript

When you need to receive an SSE stream from the frontend, you can use the browser's native EventSource API.

SSE Code Example

javascript
const sse = new EventSource('Your API Url');

// Listen for the open event, triggered when the connection is successful
sse.addEventListener('open', function (e) {
  console.log('SSE connection opened');
});

// Listen for the error event, triggered when a connection error occurs
sse.addEventListener('error', function (e) {
  console.log('SSE connection error');
});

// Listen for the message event, triggered when a message is received
sse.addEventListener('message', function (e) {
  console.log('SSE message received', e);

  const data = JSON.parse(e.data);
  const messageElement = document.createElement('div');
  messageElement.textContent = data.message;
  document.body.appendChild(messageElement);
});

// Listen for the custom end event
sse.addEventListener('end', function (e) {
  console.log('SSE custom end', e);

  sse.close();
});

withCredentials Property

When does this issue occur: When the frontend needs to access an SSE service across origins, and the service requires cookies or authentication information.

When creating an EventSource object, you can set the withCredentials property to send CORS authentication information.

javascript
const sse = new EventSource('Your API Url', { withCredentials: true } );

WARNING

When using SSE with cross-origin requests, the Server-side headers must also be configured for CORS accordingly.

Implementing an SSE Server in .NET

An SSE Server must return the text/event-stream format, and each message must end with an empty line (two newline characters \n\n) as a terminator.

Implementation via ASHX (Generic Handler)

When does this issue occur: When you need to implement real-time pushing in legacy ASP.NET Web Forms projects.

csharp
public class SseHandler : IHttpHandler {
    public void ProcessRequest (HttpContext context) {
        // Set the response Content-Type to text/event-stream
        context.Response.ContentType = "text/event-stream";
        context.Response.CacheControl = "no-cache";

        // Simulate an SSE event stream, sending one message per second
        int count = 0;
        while (count < 10) {
            count++;
            context.Response.Write("data: " + "{\"message\": \"Hello SSE " + count + "\"}\n\n");
            context.Response.Flush();
            System.Threading.Thread.Sleep(1000);
        }

        // Return a custom end event
        context.Response.Write("event: end\ndata: {}\n\n");
        context.Response.Flush();
        context.Response.End();
    }

    public bool IsReusable {
        get {
            return false;
        }
    }
}

Implementation via ASP.NET Core Web API

When does this issue occur: When developing API services in modern .NET Core or .NET 5+ environments.

csharp
[ApiController]
[Route("[controller]")]
public class SseController : ControllerBase {
    [HttpGet]
    public async Task GetAsync() {
        // Set the response Content-Type to text/event-stream
        Response.Headers.Add("Content-Type", "text/event-stream;");
        Response.Headers.Add("Cache-Control", "no-cache");

        // Simulate an SSE event stream, sending one message per second
        int count = 0;
        while (count < 10) {
            count++;
            await Response.WriteAsync($"data: " + "{\"message\": \"Hello SSE " + count + "\"}\n\n");
            await Response.Body.FlushAsync();
            await Task.Delay(TimeSpan.FromSeconds(1));
        }

        // Return a custom end event
        await Response.WriteAsync("event: end\ndata: {}\n\n");
        await Response.Body.FlushAsync();
    }
}

Change Log

  • 2023-03-15 Initial document creation.
  • 2024-02-17 Corrected content related to withCredentials.
  • 2026-05-17 Added link to the GitHub example project.