ASPNET Core Server Sent Events / Response flush

24,791

Solution 1

Client Side - wwwroot/index.html

On page load, create an EventSource for the http://www.somehost.ca/sse url. Then write its events to the console.

<body>
    <script type="text/javascript">

        var source = new EventSource('sse');

        source.onmessage = function (event) {
            console.log('onmessage: ' + event.data);
        };

        source.onopen = function(event) {
            console.log('onopen');
        };

        source.onerror = function(event) {
            console.log('onerror');
        }

    </script>
</body>

Server Side Alternative #1 - Use Middleware

The middleware handles the sse path. It sets the Content-Type header to text/event-stream, which the server socket event requires. It writes to the response stream, without closing the connection. It mimics doing work, by delaying for five seconds between writes.

app.Use(async (context, next) =>
{
    if (context.Request.Path.ToString().Equals("/sse"))
    {
        var response = context.Response;
        response.Headers.Add("Content-Type", "text/event-stream");

        for(var i = 0; true; ++i)
        {
            // WriteAsync requires `using Microsoft.AspNetCore.Http`
            await response
                .WriteAsync($"data: Middleware {i} at {DateTime.Now}\r\r");

            await response.Body.FlushAsync();
            await Task.Delay(5 * 1000);
        }
    }

    await next.Invoke();
});

Server Side Alternative #2 - Use a Controller

The controller does the exact same thing as the middleware does.

[Route("/api/sse")]
public class ServerSentEventController : Controller
{
    [HttpGet]
    public async Task Get()
    {
        var response = Response;
        response.Headers.Add("Content-Type", "text/event-stream");

        for(var i = 0; true; ++i)
        {
            await response
                .WriteAsync($"data: Controller {i} at {DateTime.Now}\r\r");

            response.Body.Flush();
            await Task.Delay(5 * 1000);
        }
    }
}

Client Side Console Output in Firefox

This is the result in the Firefox console window. Every five seconds a new messages arrives.

onopen
onmessage: Message 0 at 4/15/2016 3:39:04 PM
onmessage: Message 1 at 4/15/2016 3:39:09 PM
onmessage: Message 2 at 4/15/2016 3:39:14 PM
onmessage: Message 3 at 4/15/2016 3:39:19 PM
onmessage: Message 4 at 4/15/2016 3:39:24 PM

References:

Solution 2

Server sent events can be implemented entirely in a controller action.

This is based on the answer by Shaun Luttin, but it's more of a real-world example in that it will hold open the connection indefinitely, and it sends messages to the EventSource in response to messages being created.

using Example.Models;
using Example.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Example.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SseMessagesController : ControllerBase
    {
        private readonly IMessageRepository messageRepository;
        private readonly JsonSerializerSettings jsonSettings;

        public SseMessagesController(IMessageRepository messageRepository)
        {
            this.messageRepository = messageRepository;
            this.jsonSettings = new JsonSerializerSettings();
            jsonSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        }

        [HttpGet]
        public async Task GetMessages(CancellationToken cancellationToken)
        {
            Response.StatusCode = 200;
            Response.Headers.Add("Content-Type", "text/event-stream");

            EventHandler<MessageCreatedArgs> onMessageCreated = async (sender, eventArgs) =>
            {
                try
                {
                    var message = eventArgs.Message;
                    var messageJson = JsonConvert.SerializeObject(message, jsonSettings);
                    await Response.WriteAsync($"data:{messageJson}\n\n");
                    await Response.Body.FlushAsync();
                }
                catch (Exception)
                {
                    // TODO: log error
                }
            };
            messageRepository.MessageCreated += onMessageCreated;

            while (!cancellationToken.IsCancellationRequested) {
                await Task.Delay(1000);
            }

            messageRepository.MessageCreated -= onMessageCreated;
        }
    }
}

Whenever the EventSource connects to /api/ssemessages, we add an event delegate to the MessageCreated event on the message repository. Then we check every 1 second to see if the EventSource has been closed, which will cause the request to be cancelled. Once the request is cancelled, we remove the event delegate.

The event delegate gets the Message object from the event arguments, serializes it to JSON (using camel case to be consistent with ASP.NET Core's default behavior when returning an object result), writes the JSON to the body, and flushes the body's stream to push the data to the EventSource.

For more on creating the event delegate, see this article and this update for .NET Core.

Also, if you host this behind Nginx, you'll want to read this SO answer and this ServerFault answer.

Share:
24,791
Dj_Bear
Author by

Dj_Bear

Updated on July 19, 2022

Comments

  • Dj_Bear
    Dj_Bear almost 2 years

    While there is no official documentation, does anyone know how SSE may be implemented using ASP.NET Core?

    I suspect one implementation may use custom middleware, but maybe it is possible to do that in controller action?