42

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?

Henk Mollema
  • 44,194
  • 12
  • 93
  • 104
Zygimantas
  • 8,547
  • 7
  • 42
  • 54
  • Are you speaking of SignalR? – Tseng Mar 25 '16 at 21:21
  • No, there is no SignalR for Core yet. – Zygimantas Mar 25 '16 at 23:03
  • As a matter of fact, SignalR 3 is available in rc1-final on both the official nuget feed as well as on the myget stables https://www.myget.org/gallery/aspnetmaster and nightlies. That's why I asked. However, it won't be ready for release, when ASP.NET Core 1.0 is released as mentioned on the roadmap https://github.com/aspnet/Home/wiki/Roadmap – Tseng Mar 25 '16 at 23:36
  • Thank you, I was not aware of that. I will take a look at the source, maybe I will find something useful. At this situation, complete framework is a little bit overkill. I have a single uni-directional channel, implemented using websockets and I am searching for ways how to make it even simpler. – Zygimantas Mar 25 '16 at 23:54
  • pre Core: https://github.com/erizet/ServerSentEvent4Net – yzorg Mar 23 '18 at 17:01

2 Answers2

84

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:

Shaun Luttin
  • 133,272
  • 81
  • 405
  • 467
  • 1
    Thank you Shaun, your answer looks very comprehensive. I will transfer reputation points soon, I am still having problems reproducing this on RC1 on local machine and Azure. It looks like the response is not flushed on each iteration. Maybe it a good time to move to pre-RC2. – Zygimantas Apr 18 '16 at 09:53
  • @Zygimantas It's definitely a good time to move to pre-RC2. I haven't tried the above on RC1. While working with pre-RC2 can be painful, you will learn a massive amount as you work thru the pain. – Shaun Luttin Apr 18 '16 at 15:08
  • 1
    thank you for pushing to the bleeding edge :) Now I can confirm that is works on pre-RC2 as expected, but not on RC1. – Zygimantas Apr 21 '16 at 17:39
  • 1
    @Zygimantas Good stuff. The RC1 stuff is good and is a "go live" product. The RC2 though is the future, so it is a good idea to start learning it. I am glad to hear that this works for you. :) – Shaun Luttin Apr 21 '16 at 17:57
  • 1
    I have also noticed you are using IHttpContextAccessor instead of inheriting ControllerBase and calling this.HttpContext. I haven't seen this before on RC1. Are there any advantages of doing that? – Zygimantas Apr 21 '16 at 18:07
  • 1
    @Zygimantas I am not sure about the advantages of the one over the other. I do know that the new MVC supports plain-old-crl-object (POCO) controllers. In other words, these are controllers that do not inherit from the `Controller` class. I tend away from inheritance for simple projects to avoid the overhead. There are benefits to inheriting the `Controller` class. It comes with a lot of helper methods, so to speak. Here is the class. Checkout its public/protect members to see what it gives you: https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Controller.cs – Shaun Luttin Apr 21 '16 at 18:16
  • 1
    To see the WriteAsyn() on Response you need to include using Microsoft.AspNetCore.Http; – pbz Jul 10 '16 at 15:07
  • @ShaunLuttin Is there a way to send message to only specific users? Can we utilize_httpContextAccessor.HttpContext.User for this? – Radenko Zec Nov 03 '16 at 14:02
  • @RadenkoZec I'm afraid I don't know how to do that off the top of my head. – Shaun Luttin Nov 03 '16 at 15:50
  • How do you know when to close the connection? When I refresh the browser it makes a second request which calls the controller again leaving 2 loops running. – jmathew Jun 29 '17 at 14:35
  • You can close the connection from the client side by calling eventSource.close(). – Dan Aug 28 '17 at 18:39
  • Does anyone know why the response to this request is always text/html and not text/event-stream? I continually get "EventSource's response has a MIME type ("text/html") that is not "text/event-stream". Aborting the connection." in Chrome, though it doesn't seem to abort or reconnect at all. Odd. – Dan Aug 28 '17 at 18:40
  • Nevermind, I found this is an artifact of HotModuleReplacement of the webpack middleware stuff. – Dan Aug 28 '17 at 18:49
  • @Zygimantas did you get this working on Azure? It works on my machine, but on Azure the response never completes. – mcintyre321 Jan 23 '18 at 14:23
  • @mcintyre321yes, but later I have moved to websockets because latency of sending data to the server over separate rest api was too big. – Zygimantas Jan 23 '18 at 17:13
  • @ShaunLuttin is this solution good for sending big files from server to client? – Akmal Salikhov Jan 10 '21 at 10:16
23

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.

delial
  • 331
  • 4
  • 3
  • Hey delial, thanks for the answer! Not sure if you knew, but this question is more than 3 years old - if you'd like to focus your efforts on new questions, you can use the "Newest" tab to achieve that. For example, [this](https://stackoverflow.com/questions/tagged/c%23?tab=Newest) is the link to newest C# questions. – zbee Oct 25 '19 at 21:49
  • 3
    Do you have a GitHub repo or gist with a complete example including a `IMessageRepository` implementation? – Mark G Nov 06 '19 at 04:57
  • 3
    @MarkG I know it's 1 year later, but probably this would be helpful: https://github.com/Nordes/Sample.SSEvent (I don't think I will erase it, but it can help to see where events are coming from and how to broadcast). – Nordes Nov 29 '20 at 05:32
  • Thanks for the answer! using "cancellationToken" and "response.body.FlushAsync" was essential for my project. – wancharle Aug 13 '21 at 20:29