DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Server-Sent Events Using Spring
  • Reactive Messaging Examples for Quarkus
  • Thoughts on Server-Sent Events, HTTP/2, and Envoy
  • GDPR Compliance With .NET: Securing Data the Right Way

Trending

  • Power BI Embedded Analytics — Part 2: Power BI Embedded Overview
  • *You* Can Shape Trend Reports: Join DZone's Software Supply Chain Security Research
  • Scalable System Design: Core Concepts for Building Reliable Software
  • Accelerating AI Inference With TensorRT
  1. DZone
  2. Software Design and Architecture
  3. Cloud Architecture
  4. Server-Sent Events (SSE) Support for ASP.NET Core

Server-Sent Events (SSE) Support for ASP.NET Core

In this article, we look at how using Server-Sent Events (SSE) in our ASP.NET Core code, can help create better HTTP pipelines than web sockets.

By 
Tomasz Pęczek user avatar
Tomasz Pęczek
·
Apr. 07, 17 · Tutorial
Likes (0)
Comment
Save
Tweet
Share
24.1K Views

Join the DZone community and get the full member experience.

Join For Free

The web socket protocol is currently the most popular method for pushing data to browsers, however, it's not the only one. The Server-Sent Events (SSE) is a very interesting alternative which can provide better performance for specific use cases.

What Is SSE

The Server-Sent Events is a unidirectional (server to browser) protocol for streaming events. The protocol delivers text-based messages over a long-lived HTTP connection. It also has built-in support for events identification, auto-reconnection (with tracking of last received event), and notifications through DOM events. Its biggest advantage is high performance as events can be pushed immediately with minimum overhead (there is an already open HTTP connection waiting, which, thanks to text-based messages, can utilize HTTP compression mechanisms). A considerable limitation is a general lack of support for binary streaming (but JSON or XML will work nicely).

Why Use SSE

In general, web sockets can do everything that Server-Sent Events can and more, as they provide bidirectional communication. There is also broader browser support (93%) for web sockets. So why would one consider the SSE (assuming bidirectional isn't a requirement, or the client to server communication is occasional and can be done in a REST style)? The fact that it runs over a long-lived HTTP connection is a game changer here. In the case of web sockets, we are talking about custom TCP-based protocols which need to be supported by the server and an entire infrastructure (proxies, firewalls, etc.); any legacy element along the way may cause an issue. There are no such issues for SSE, anything that speaks HTTP will speak SSE and the aspect of browser support (87%) can be addressed with polyfills. Taking this into consideration, and notably lower latency, Server-Sent Events are a very compelling choice for scenarios like stock tickers or notifications.

Bringing SSE to ASP.NET Core

One of the key concepts behind ASP.NET Core is the modular HTTP request pipeline which can be extended through middlewares, so I'm going to create one for Server-Sent Events. But first, some prerequisites are needed.

The middleware will require an abstraction for representing a client. As previously stated, SSE runs over a long-lived HTTP connection, which means that channel for communication with a client is HttpResponse instance. The abstraction will simply wrap around it.

public class ServerSentEventsClient
{
    private readonly HttpResponse _response;

    internal ServerSentEventsClient(HttpResponse response)
    {
        _response = response;
    }
}

Also, there is a need for some kind of service which will serve as a bridge between the middleware and the rest of the application. Its primary goal will be managing the collection of connected clients. Below is a simple implementation based on a ConcurrentDictionary.

public class ServerSentEventsService
{
    private readonly ConcurrentDictionary<Guid, ServerSentEventsClient> _clients = new ConcurrentDictionary<Guid, ServerSentEventsClient>();

    internal Guid AddClient(ServerSentEventsClient client)
    {
        Guid clientId = Guid.NewGuid();

        _clients.TryAdd(clientId, client);

        return clientId;
    }

    internal void RemoveClient(Guid clientId)
    {
        ServerSentEventsClient client;

        _clients.TryRemove(clientId, out client);
    }
}

With those elements in place, the middleware can be created. It will have two responsibilities: establishing the connection and cleaning up when the client closes the connection.

In order to establish the connection, the middleware should inspect the Accept header of the incoming request, if its value is text/event-stream it means that client is attempting to open an SSE connection. In such cases, the Content-Type response header should be set to  text/event-stream, headers should be sent, and connection needs to be kept open.

The cleanup part requires detecting that a client has closed the connection. This can be done by waiting on the CancellationToken available through the HttpContext.RequestAborted property. An important thing to note here is that a closed connection can only be detected when sending a new event. This limitation is often being solved by sending a dedicated heartbeat event which the client should simply ignore.

public class ServerSentEventsMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ServerSentEventsService _serverSentEventsService;

    public ServerSentEventsMiddleware(RequestDelegate next, ServerSentEventsService serverSentEventsService)
    {
        _next = next;
        _serverSentEventsService = serverSentEventsService;
    }

    public Task Invoke(HttpContext context)
    {
        if (context.Request.Headers["Accept"] == "text/event-stream")
        {
            context.Response.ContentType = "text/event-stream";
            context.Response.Body.Flush();

            ServerSentEventsClient client = new ServerSentEventsClient(context.Response);
            Guid clientId = _serverSentEventsService.AddClient(client);

            context.RequestAborted.WaitHandle.WaitOne();

            _serverSentEventsService.RemoveClient(clientId);

            return Task.FromResult(true);
        }
        else
        {
            return _next(context);
        }
    }
}

With the connection management part in place, the sending part can be added. The message format in SSE is a very simple one. The basic building blocks of every message are fields which have a general format that looks like this: <FieldName>: <FieldValue>\n. There are three types of fields (well, in fact, four as there is an additional one for controlling the client reconnect interval):

  • id - The identifier of the event.
  • event - The type of the event.
  • data - A single line of data (the entire payload of the message is represented by one or more adjacent data fields).

Only the data field is required and the entire message is being terminated by an additional new line (\n).

public class ServerSentEvent
{
    public string Id { get; set; }

    public string Type { get; set; }

    public IList<string> Data { get; set; }
}

internal static class ServerSentEventsHelper
{
    internal static async Task WriteSseEventAsync(this HttpResponse response, ServerSentEvent serverSentEvent)
    {
        if (!String.IsNullOrWhiteSpace(serverSentEvent.Id))
            await response.WriteSseEventFieldAsync("id", serverSentEvent.Id);

        if (!String.IsNullOrWhiteSpace(serverSentEvent.Type))
            await response.WriteSseEventFieldAsync("event", serverSentEvent.Type);

        if (serverSentEvent.Data != null)
        {
            foreach(string data in serverSentEvent.Data)
                await response.WriteSseEventFieldAsync("data", data);
        }

        await response.WriteSseEventBoundaryAsync();
        response.Body.Flush();
    }

    private static Task WriteSseEventFieldAsync(this HttpResponse response, string field, string data)
    {
        return response.WriteAsync($"{field}: {data}\n");
    }

    private static Task WriteSseEventBoundaryAsync(this HttpResponse response)
    {
        return response.WriteAsync("\n");
    }
}

The above helper can be used in order to expose the send method on the client abstraction.

public class ServerSentEventsClient
{
    ...

    public Task SendEventAsync(ServerSentEvent serverSentEvent)
    {
        return _response.WriteSseEventAsync(serverSentEvent);
    }
}

The last step is exposing the send method at the service level - it should perform a send for all connected clients.

public class ServerSentEventsService
{
    ...

    public Task SendEventAsync(ServerSentEvent serverSentEvent)
    {
        List<Task> clientsTasks = new List<Task>();
        foreach (ServerSentEventsClient client in _clients.Values)
        {
            clientsTasks.Add(client.SendEventAsync(serverSentEvent));
        }

        return Task.WhenAll(clientsTasks);
    }
}

We can say that this gives us what project managers like to call a minimum viable product. After extending the pipeline with the middleware and adding a service to the services collection (as \ singleton) we can send events from any desired place in the application. In the case of a need for exposing more than one endpoint, a derived service can be created, added to the services collection, and passed to the respective middlewares during initialization.

I've made an extended version (support for the reconnect interval, extensibility point for the auto-reconnect, and extensions for service and middleware registrations) available on GitHub and as a NuGet package.

SSE at Work

I've also created a demo application which utilizes the above components, it can be found here. The application exposes two SSE endpoints:

  • /see-heartbeat which can be "listened" by navigating to /sse-heartbeat-receiver.html. It sends an event every 5s and is implemented through an ugly background thread.
  • /sse-notifications which can be "listened" by navigating to /notifications/sse-notifications-receiver. Sending events to this endpoint can be done by navigating to /notifications/sse-notifications-sender.

It might be a good starting point for those who would like to play with what I've shown here.

Server-sent events Event ASP.NET ASP.NET Core Connection (dance) Web Service Middleware

Published at DZone with permission of Tomasz Pęczek. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Server-Sent Events Using Spring
  • Reactive Messaging Examples for Quarkus
  • Thoughts on Server-Sent Events, HTTP/2, and Envoy
  • GDPR Compliance With .NET: Securing Data the Right Way

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!