Server-Sent Events in ASP.NET Core Minimal APIs with .NET 10
In this post, Khalid Abuhakmeh showcases how to implement Server-Sent Events with ASP.NET Core Minimal APIs in .NET 10, highlighting practical differences with SignalR and providing sample C# and JavaScript code for real-time feeds.
Server-Sent Events in ASP.NET Core and .NET 10
Author: Khalid Abuhakmeh
Photo by Allen Rad
Introduction
As .NET 10 and C# 14 approach, new features are coming to ASP.NET Core—one major addition is support for Server-Sent Events (SSE) within Minimal APIs. SSE allows a server to push events to clients over HTTP, making it suitable for applications that require unidirectional, real-time updates such as news feeds or stock tickers.
SSE vs SignalR
A common question concerns the core differences between SSE and SignalR. SSE is lighter than WebSockets, as it uses the HTTP protocol and provides one-way communication from server to client. SignalR, by default, uses WebSockets to provide bidirectional communication, which can be unnecessary overhead for scenarios where clients simply listen for server updates. SSE is best suited for cases where real-time, one-way communication suffices.
SSE Implementation Example with ASP.NET Core Minimal APIs
This section demonstrates a straightforward example of implementing SSE using Minimal APIs in .NET 10. The approach utilizes TypedResults.ServerSentEvents
, a new response type, which can stream items from an IAsyncEnumerable
to any connected client.
using System.ComponentModel;
using System.Runtime.CompilerServices;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<FoodService>();
builder.Services.AddHostedService<FoodServiceWorker>();
var app = builder.Build();
app.UseDefaultFiles().UseStaticFiles();
app.MapGet("/orders", (FoodService foods, CancellationToken token) =>
TypedResults.ServerSentEvents(
foods.GetCurrent(token),
eventType: "order")
);
app.Run();
How it works:
- The
/orders
endpoint streams food emoji updates to clients using the SSE protocol. - The data is streamed by returning an
IAsyncEnumerable<string>
fromFoodService.GetCurrent
. - A cancellation token allows clean client disconnection/stopping of the enumeration.
Building the IAsyncEnumerable Food Service
To keep all clients in sync with the same “current” food emoji, the implementation leverages .NET’s observable and asynchronous patterns. Two core classes are involved:
FoodService
The FoodService
class manages the current food and implements INotifyPropertyChanged
to notify all listeners of updates.
public class FoodService : INotifyPropertyChanged
{
public FoodService()
{
Current = Foods[Random.Shared.Next(Foods.Length)];
}
public event PropertyChangedEventHandler? PropertyChanged;
private static readonly string[] Foods =
["🍔", "🍟", "🥤", "🍤", "🍕", "🌮", "🥙"];
private string Current { get; set { field = value; OnPropertyChanged(); } }
public async IAsyncEnumerable<string> GetCurrent([
EnumeratorCancellation] CancellationToken ct)
{
while (ct is not { IsCancellationRequested: true })
{
yield return Current;
var tcs = new TaskCompletionSource();
PropertyChangedEventHandler handler = (_, _) => tcs.SetResult();
PropertyChanged += handler;
try
{
await tcs.Task.WaitAsync(ct);
}
finally
{
PropertyChanged -= handler;
}
}
}
public void Set()
{
Current = Foods[Random.Shared.Next(Foods.Length)];
}
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
FoodServiceWorker
FoodServiceWorker
updates the food emoji at timed intervals, simulating real-time changes.
public class FoodServiceWorker(FoodService foodService) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
foodService.Set();
await Task.Delay(1000, stoppingToken);
}
}
}
JavaScript Client: Subscribing to the SSE Endpoint
A simple HTML page can take advantage of the newly-created SSE endpoint by connecting an EventSource
instance and responding to incoming events.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Khalid's Fast-Food Fair</title>
<style>
ul { display: flex; flex-direction: row; list-style: none; flex-wrap: wrap; width: 90%; gap: 1rem; padding: 0; }
li { font-size: 2rem; }
</style>
</head>
<body>
<h1>Khalid's Fast-Food Fair</h1>
<ul id="orders"></ul>
<script>
const eventSource = new EventSource('/orders');
const ordersList = document.getElementById('orders');
eventSource.addEventListener('order', event => {
const li = document.createElement('li');
li.textContent = event.data;
ordersList.appendChild(li);
});
eventSource.onerror = error => {
console.error('EventSource failed:', error);
eventSource.close();
};
</script>
</body>
</html>
Whenever the browser loads the page, it automatically subscribes to /orders
, and each time the FoodServiceWorker
updates the food, all subscribers receive the new emoji. Opening multiple pages will show the updated emoji in sync across all clients.
Conclusion
This demonstration shows how .NET 10’s Minimal APIs can leverage built-in support for Server-Sent Events to create simple, real-time server-to-client update channels with minimal complexity. This can be preferable to SignalR when only unidirectional communication is required. The code provided can serve as a starting point for building real-world applications that require live data feeds.
About the Author: Khalid Abuhakmeh is a developer advocate at JetBrains, specializing in .NET technologies and tooling.
Read Next: Generic C# Methods with Enum Constraints for .NET
This post appeared first on “Khalid Abuhakmeh’s Blog”. Read the entire article here