ASPNET Core Server Sent Events / Response flush

Viewed 28374

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?

2 Answers

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.

Related