ASP.NET Hosting

ASP.NET 10 Realtime Updates: Simpler Server-Sent Events

Real-time web applications are now required rather than optional. Users expect to view information instantaneously without having to reload the page, whether it’s social media alerts, monitoring dashboards, or live stock prices. Server-Sent Events (SSE) have long been a straightforward and effective method of communicating server modifications to the browser. It is less heavy than WebSockets for one-way communication. However, using SSE in ASP.NET Core typically required additional work manually specifying headers, publishing to the response stream, and managing connection cancelation on your own .NET 10 offers a simpler and more straightforward method of using 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.

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:

  1. 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).
  2. Key Rotation: Static keys can be leaked. Ensure you have a way to rotate keys without redeploying the application.
  3. 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!