ASPNET Core Server Sent Events / Response flush
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:
- The above sample on GitHub
- The HTML Living Standard, section 9.2 Server-sent events
- Http Push Technology on Wikipedia
- Chunked transfer encoding
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.