Server Sent Events (SSE) is a HTML standard with good support by all major browsers that allows your server to push events to connected clients as they happen.
In contrast to websockets, the connection is unidirectional (so only the server can push events), making it easier to be implemented, soley relying on default HTTP mechanisms. On the client side, there is a simple, comprehensible API ready to be used.
ASP.NET Core does not offer an official implementation for Server Sent Events, so there are a lot of tutorials which try to add this functionality by directly writing to the response stream in the format the client expects. This approach works, but adds a lot of clutter to your application code and typically lacks some handy features provided by the standard, such as event types or event stream resumption.
There are other web server frameworks that have a first-class support for server sent events built in, but you probably do not want to change your server framework just for implementing an event source.
Gladly, one of those frameworks - namely GenHTTP - provides an adapter that allows us to plug in their implementation into an ASP.NET Core application. The following example will demonstrate this approach with a simple app that pushes randomly generated stock symbols to interested clients.
First, we create a new ASP.NET Core minimal API project from the terminal (or in Visual Studio):
dotnet new web -o StockEvents
In the generated project, we then add the nuget packages for the provider and the adapter.
dotnet add package GenHTTP.Modules.ServerSentEvents
dotnet add package GenHTTP.Adapters.AspNetCore
In our Program.cs
we can then create an Event Source that will randomly generate our stock symbol updates and push them to connected clients. This source can be mapped to any path using the adapter functionality.
using GenHTTP.Adapters.AspNetCore;
using GenHTTP.Modules.ServerSentEvents;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var source = EventSource.Create()
.Generator(GenerateStock)
.Defaults();
app.Map("/stock", source);
app.UseDefaultFiles();
app.UseStaticFiles();
app.Run();
static async ValueTask GenerateStock(IEventConnection connection)
{
var rand = new Random();
var stockSymbols = new List<string> { "AAPL", "GOOGL", "MSFT" };
await connection.CommentAsync("Sending stock data");
while (connection.Connected)
{
var symbol = stockSymbols[rand.Next(0, 3)];
await connection.DataAsync(rand.Next(100, 1000), symbol);
await Task.Delay(1000);
}
}
To test our implementation, we create a wwwroot
sub directory in our project and create an index.html
file there with the following content (adjust the port of the endpoint as needed):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stock Tracker</title>
<style>
body {
font-family: Arial, sans-serif;
}
#stocks {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 20px;
}
.stock {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
</style>
</head>
<body>
<h1>Stock Tracker</h1>
<div id="stocks"></div>
<script>
// Establish a connection to the server using Server-Sent Events
const eventSource = new EventSource('http://localhost:5011/stock/');
// Function to display stock updates
function updateStock(symbol, value) {
let stockElement = document.getElementById(symbol);
if (!stockElement) {
// Create a new element for the stock symbol if it doesn't exist
stockElement = document.createElement('div');
stockElement.id = symbol;
stockElement.className = 'stock';
document.getElementById('stocks').appendChild(stockElement);
}
// Update stock value
stockElement.innerHTML = `<strong>${symbol}:</strong> ${value}`;
}
// Event listener for general updates
eventSource.onmessage = function(event) {
updateStock(event.type, event.data);
};
// Event listeners for specific stock symbols
const symbols = ['AAPL', 'GOOGL', 'MSFT']; // Example stocks
symbols.forEach(symbol => {
eventSource.addEventListener(symbol, event => {
updateStock(symbol, event.data);
});
});
// Error handling
eventSource.onerror = function() {
console.error('Connection to the server lost.');
};
</script>
</body>
</html>
After the server has been started (dotnet run
), you can open the endpoint announced by the launcher (e.g. http://localhost:5011) and will see the stock updates ticking in.
Please note that the endpoint you specified in the HTML file needs to match the URL you open in your browser, or otherwise CORS will prevent us from fetching events (so for example you cannot mix HTTP and HTTPS).
This was a brief tutorial to quickly spawn and consume an event source - you can check the documentation of the GenHTTP module to learn more about event types, event IDs, or error handling.
Cover by: Devon Janse van Rensburg
Top comments (0)