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);
2. The Frontend (Vanilla JS)
Consuming the stream is standard SSE. We use the browser's native EventSource API.
<!DOCTYPE html>
<html>
<head>
<title>.NET 10 SSE Demo</title>
</head>
<body>
<h1>Stock Ticker </h1>
<div id="ticker">Waiting for updates...</div>
<script>
const tickerDiv = document.getElementById('ticker');
const eventSource = new EventSource('/stock-stream');
eventSource.addEventListener('price-update', (event) => {
const batch = JSON.parse(event.data);
tickerDiv.innerHTML = '';
batch.forEach(data => {
tickerDiv.innerHTML += `
<div>
<strong>${data.symbol}</strong>: $${data.price}
<small>(${new Date(data.timestamp).toLocaleTimeString()})</small>
</div>
`;
});
});
eventSource.onerror = (err) => {
console.error("EventSource failed:", err);
eventSource.close();
};
</script>
</body>
</html>
3. The C# Client (Hosted Service)
For backend-to-backend communication (like a Hosted Service in IIS), .NET 9+ introduces SseParser.
using System.Net.ServerSentEvents;
using System.Text.Json;
// Connect to the stream
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "secret-key-123"); // Add Auth Header
using var stream = await client.GetStreamAsync("http://localhost:5000/stock-stream-batch");
// Parse the stream
var parser = SseParser.Create(stream);
await foreach (var sseItem in parser.EnumerateAsync())
{
if (sseItem.EventType == "price-update")
{
var update = JsonSerializer.Deserialize<StockUpdate>(sseItem.Data, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
Console.WriteLine($"Received: {update?.Symbol} - ${update?.Price}");
}
}
record StockUpdate(string Symbol, decimal Price, DateTime Timestamp);
Security Considerations
Passing an API Key in a header (like X-API-Key) is a common pattern, but it comes with risks:
- HTTPS is Mandatory: Headers are sent in plain text. If you use HTTP, anyone on the network can sniff the key. Always use HTTPS in production to encrypt the traffic (including headers).
- Key Rotation: Static keys can be leaked. Ensure you have a way to rotate keys without redeploying the application.
- Better Alternatives: For high-security scenarios, consider using OAuth 2.0 / OIDC (Bearer tokens) or mTLS (Mutual TLS) for server-to-server authentication.
Conclusion
Server-Sent Events (SSE) offer a lightweight and efficient standard for handling real-time unidirectional data streams. By leveraging standard HTTP connections, SSE avoids the complexity of WebSockets for scenarios where the client only needs to receive updates. Whether you're building live dashboards, notification systems, or news feeds, SSE provides a robust and easy-to-implement solution that keeps your application responsive and up-to-date. Happy Coding!
European Best, cheap and reliable ASP.NET Core 10.0 hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.
