Realtime web apps are no longer optional—they’re expected. Whether it’s live stock prices, monitoring dashboards, or social media alerts, users want to see updates instantly without refreshing the page.
For a long time, Server-Sent Events (SSE) has been a simple and efficient way to send updates from the server to the browser. It’s lighter than WebSockets when you only need one-way communication. But in ASP.NET Core, using SSE usually meant extra work—setting headers manually, writing to the response stream, and handling connection cancellation yourself.
.NET 10 introduces a cleaner and easier way to use SSE in Minimal APIs with TypedResults.ServerSentEvents.
What is TypedResults.ServerSentEvents?
TypedResults.ServerSentEvents is a new feature that lets you return an SSE stream almost as easily as returning JSON. You just return an IAsyncEnumerable<SseItem<T>>, and ASP.NET Core takes care of the rest:
Sets the correct Content-Type (text/event-stream)
Formats the data to match the SSE standard
Manages the connection automatically
This means less code, fewer mistakes, and a much simpler way to build realtime features in .NET 10.
The Code
The source code can be downloaded from GitHub - SSE DEMO and GitHub -SSE Client
Let's build a simple "Stock Ticker" simulation.
1. The Backend (ASP.NET Core)
First, we define our data model and a simple generator function that simulates a stream of stock updates.
using System.Runtime.CompilerServices;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseDefaultFiles();
app.UseStaticFiles();
// The SSE Endpoint
app.MapGet("/stock-stream", () =>
return TypedResults.ServerSentEvents(GetStockUpdates());
});
// The Batched SSE Endpoint (Secured)
app.MapGet("/stock-stream-batch", (HttpContext context) =>
{
// Simple API Key Authentication
if (!context.Request.Headers.TryGetValue("X-API-Key", out var apiKey) || apiKey != "secret-key-123")
{
return Results.Unauthorized();
}
return TypedResults.ServerSentEvents(GetStockUpdatesBatch());
});
app.Run();
// Simulating a data stream
async IAsyncEnumerable<SseItem<List<StockUpdate>>> GetStockUpdates(
[EnumeratorCancellation] CancellationToken ct = default)
{
var random = new Random();
var symbol = "MSFT";
var price = 420.00m;
while (!ct.IsCancellationRequested)
{
var batch = new List<StockUpdate>();
// Create a batch of 3 updates
for(int i = 0; i < 3; i++)
{
price += (decimal)(random.NextDouble() * 2 - 1);
batch.Add(new StockUpdate(symbol, Math.Round(price, 2), DateTime.UtcNow));
}
// Yield an SSE Item containing the list
yield return new SseItem<List<StockUpdate>>(batch, "price-update")
{
EventId = Guid.NewGuid().ToString()
};
await Task.Delay(1000, ct); // Update every second
}
}
// Simulating a batched data stream (bursts of events from DB/Service)
async IAsyncEnumerable<SseItem<StockUpdate>> GetStockUpdatesBatch(
[EnumeratorCancellation] CancellationToken ct = default)
{
var random = new Random();
var symbols = new[] { "MSFT", "GOOG", "AAPL", "NVDA" };
var prices = new Dictionary<string, decimal>
{
["MSFT"] = 420.00m, ["GOOG"] = 175.00m, ["AAPL"] = 180.00m, ["NVDA"] = 950.00m
};
while (!ct.IsCancellationRequested)
{
// Simulate fetching a list of updates from a database or external service
// Randomly simulate "no records found" (e.g., 20% chance)
// Randomly simulate "no records found" (e.g., 20% chance)
if (random.NextDouble() > 0.2)
{
// Step 1: Query Database (e.g. var results = await db.GetUpdatesAsync();)
// We fetch ALL updates in a single query here.
// Step 2: Stream the results one by one
foreach (var symbol in symbols)
{
prices[symbol] += (decimal)(random.NextDouble() * 2 - 1);
var update = new StockUpdate(symbol, Math.Round(prices[symbol], 2), DateTime.UtcNow);
yield return new SseItem<StockUpdate>(update, "price-update")
{
EventId = Guid.NewGuid().ToString()
};
}
}
// If no records found, we simply yield nothing this iteration.
// The connection remains open, and the client waits for the next check.
await Task.Delay(1000, ct); // Update every second
}
}
record StockUpdate(string Symbol, decimal Price, DateTime Timestamp);
Join the conversation! Your thoughts help the community grow.