Blazor .NET 10 Server-Sent Events (SSE) with Minimal APIs

Blazor .NET 10 Server-Sent Events (SSE) with Minimal APIs
.NET 10: .NET 10 is the latest version of the .NET platform, which provides a unified development experience for building applications across various platforms. It includes enhancements in performance, security, and new features that streamline the development process.
Blazor: Blazor is a web framework that allows developers to build interactive web applications using C# instead of JavaScript. It leverages the power of .NET to create rich client-side applications that can run in the browser via WebAssembly or on the server.
Server-Sent Events (SSE): Server-Sent Events (SSE) is a standard allowing a server to push real-time updates to web clients over HTTP. Unlike WebSockets, SSE is a one-way communication channel where the server can send updates to the client without requiring the client to request them.
Minimal APIs: Minimal APIs in .NET 10 provide a simplified way to create HTTP APIs with minimal overhead. They allow developers to define routes and handle requests using a more concise syntax, making it easier to build lightweight services.
EventSource: EventSource, a built-in JavaScript object that allows the client to receive updates from the server.
Event Listeners: Event Listeners, functions that respond to specific events, such as receiving new data or encountering an error.
DOM Manipulation: DOM Manipulation, code dynamically updates the HTML elements to display the latest weather forecasts.
| Project Name | Port | |
|---|---|---|
| WebApplicationNET10MinimalAPIs | :5001 | Blazor Web App |
| BlazorAppNET10SSE | :6001 | ASP.NET Core Web API (Minimal APIs) |
WebApplicationNET10MinimalAPIs/Program.cs
using System.Runtime.CompilerServices;
namespace WebApplicationNET10MinimalAPIs;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var AllowAnyOrigins = "_AllowAnyOrigins"; // TEST
builder.Services.AddCors(options =>
{
options.AddPolicy(name: AllowAnyOrigins, policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
});
// Add services to the container.
builder.Services.AddAuthorization();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", (HttpContext httpContext) =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
})
.ToArray();
return forecast;
}).WithName("GetWeatherForecast");
app.MapGet("/sse-json-weatherforecast", (CancellationToken cancellationToken) =>
{
async IAsyncEnumerable<WeatherForecastDTO> GetWeatherForecasts([EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecastRecord
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
var heartRate = Random.Shared.Next(60, 100);
yield return WeatherForecastDTO.Create(heartRate, forecast);
await Task.Delay(2000, cancellationToken);
}
}
return TypedResults.ServerSentEvents(GetWeatherForecasts(cancellationToken), eventType: "weatherforecasts");
});
app.UseCors(AllowAnyOrigins);
app.Run();
}
}
MapGet method defines a new GET endpoint at /sse-json-weatherforecast. This endpoint will be responsible for streaming weather forecast data. GetWeatherForecasts method is defined as an asynchronous iterator using IAsyncEnumerable. This allows the method to yield results over time rather than returning all data at once. Inside the while loop, the code generates a new weather forecast every two seconds. It creates an array of WeatherForecastRecord objects, each representing a forecast for the next five days. The temperature is randomly generated between -20 and 55 degrees, and a random summary is selected from a predefined list. Method yields a WeatherForecastDTO object that encapsulates the heart rate and the generated forecast data. This object is sent to the client as part of the SSE stream. TypedResults.ServerSentEvents method is called to return the generated data as a stream of Server-Sent Events, with the event type specified as weatherforecasts.
BlazorAppNET10SSE/Weather.razor
@page "/weather"
@implements IAsyncDisposable
@inject IJSRuntime JS
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
<button class="btn btn-success" @onclick="InitEventSourceAsync">Start</button>
<button class="btn btn-danger" @onclick="StopEventSourceAsync">Stop</button>
<div id="time"></div>
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody id="dataTable">
@* <tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr> *@
</tbody>
</table>
@code {
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>("import", "./js/Weather.js");
}
}
private async Task InitEventSourceAsync()
{
if (module is not null)
{
await module.InvokeVoidAsync("InitEventSource");
}
}
private async Task StopEventSourceAsync()
{
if (module is not null)
{
await module.InvokeVoidAsync("StopEventSource");
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (module is not null)
{
try
{
await module.InvokeVoidAsync("StopEventSource");
await module.DisposeAsync();
}
catch (JSDisconnectedException)
{
}
}
}
}
@inject IJSRuntime JS line allows the component to call JavaScript functions. InitEventSourceAsync and StopEventSourceAsync methods manage the connection to the SSE stream.
BlazorAppNET10SSE/Weather.js
let eventSource = null;
export function InitEventSource() {
StopEventSource();
eventSource = new EventSource('https://localhost:6001/sse-json-weatherforecast');
eventSource.addEventListener('weatherforecasts', (event) => {
// console.log(event);
const data = JSON.parse(event.data);
// console.log(data);
const timeElement = document.getElementById('time');
timeElement.innerHTML = `${data.timestamp} ${data.heartRate}`;
const table = document.getElementById('dataTable');
const itemElements = document.getElementsByName("item");
for (let i = itemElements.length - 1; i >= 0; i--) {
itemElements[i].remove();
}
const wfs = data.weatherForecasts;
wfs.forEach(forecast => {
const tr = document.createElement('tr');
tr.setAttribute('name', 'item');
const tddate = document.createElement('td');
tddate.textContent = forecast.date;
tr.appendChild(tddate);
const tdsummary = document.createElement('td');
tdsummary.textContent = forecast.summary;
tr.appendChild(tdsummary);
const tdtemperatureC = document.createElement('td');
tdtemperatureC.textContent = forecast.temperatureC;
tr.appendChild(tdtemperatureC);
const tdtemperatureF = document.createElement('td');
tdtemperatureF.textContent = forecast.temperatureF;
tr.appendChild(tdtemperatureF);
table.appendChild(tr);
});
});
eventSource.onopen = () => {
console.log('Connection opened');
};
eventSource.onmessage = (event) => {
console.log('message:', event);
};
eventSource.onerror = () => {
if (eventSource.readyState === EventSource.CONNECTING) {
console.log('Reconnecting...');
}
};
};
export function StopEventSource() {
if (eventSource != null) {
eventSource.close();
eventSource = null;
}
};
JavaScript code is structured into two main functions: InitEventSource and StopEventSource. The InitEventSource function initializes the connection to the SSE endpoint, while the StopEventSource function closes the connection when it is no longer needed.
addEventListener method listens for the weatherforecasts event. When this event is triggered, the data is parsed from JSON format, and the UI is updated accordingly. The previous forecast entries are removed, and new rows are created for each forecast.
onopen, onmessage, and onerror event handlers manage the connection state, logging messages to the console for debugging purposes.
Source
Full source code is available at this repository in GitHub:
https://github.com/akifmt/DotNetCoding/tree/main/src/BlazorAppNET10SSEwithBlazorandMinimalAPIs
