diff --git a/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/HeartRateRecord.cs b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/HeartRateRecord.cs new file mode 100644 index 000000000000..b9950900f083 --- /dev/null +++ b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/HeartRateRecord.cs @@ -0,0 +1,4 @@ +public record HeartRateRecord(DateTime Timestamp, int HeartRate) +{ + public static HeartRateRecord Create(int heartRate) => new(DateTime.UtcNow, heartRate); +} diff --git a/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/MinimalServerSentEvents.http b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/MinimalServerSentEvents.http index 6e19822ba6a5..b61db15b6b30 100644 --- a/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/MinimalServerSentEvents.http +++ b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/MinimalServerSentEvents.http @@ -1,4 +1,4 @@ -@baseUrl = http://localhost:5293 +@baseUrl = http://localhost:58489 ### Connect to SSE stream # This request will open an SSE connection that stays open @@ -12,4 +12,4 @@ Accept: text/event-stream ### GET {{baseUrl}}/sse-item -Accept: text/event-stream \ No newline at end of file +Accept: text/event-stream diff --git a/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs index 42b873277604..d3354b692fc1 100644 --- a/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs +++ b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs @@ -13,30 +13,32 @@ async IAsyncEnumerable GetHeartRate( while (!cancellationToken.IsCancellationRequested) { var heartRate = Random.Shared.Next(60, 100); - yield return $"Hear Rate: {heartRate} bpm"; + yield return $"Heart Rate: {heartRate} bpm"; await Task.Delay(2000, cancellationToken); } } - return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken), eventType: "heartRate"); + return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken), + eventType: "heartRate"); }); // // app.MapGet("/json-item", (CancellationToken cancellationToken) => { - async IAsyncEnumerable GetHeartRate( + async IAsyncEnumerable GetHeartRate( [EnumeratorCancellation] CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { var heartRate = Random.Shared.Next(60, 100); - yield return HearRate.Create(heartRate); + yield return HeartRateRecord.Create(heartRate); await Task.Delay(2000, cancellationToken); } } - return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken), eventType: "heartRate"); + return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken), + eventType: "heartRate"); }); // @@ -64,7 +66,3 @@ async IAsyncEnumerable> GetHeartRate( app.Run(); -public record HearRate(DateTime Timestamp, int HeartRate) -{ - public static HearRate Create(int heartRate) => new(DateTime.UtcNow, heartRate); -} diff --git a/aspnetcore/fundamentals/minimal-apis/responses.md b/aspnetcore/fundamentals/minimal-apis/responses.md index 0f6dca09bb47..71cb31cc663a 100644 --- a/aspnetcore/fundamentals/minimal-apis/responses.md +++ b/aspnetcore/fundamentals/minimal-apis/responses.md @@ -120,7 +120,7 @@ In order to document this endpoint correctly the extension method `Produces` is :::code language="csharp" source="~/fundamentals/minimal-apis/9.0-samples/Snippets/Program.cs" id="snippet_04"::: - + ### Built-in results @@ -172,6 +172,20 @@ The following example streams a video from an Azure Blob: [!code-csharp[](~/fundamentals/minimal-apis/resultsStream/7.0-samples/ResultsStreamSample/Program.cs?name=snippet_video)] +#### Server-Sent Events (SSE) + +The [TypedResults.ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,051e6796e1492f84) API supports returning a [ServerSentEvents](xref:System.Net.ServerSentEvents) result. + +[Server-Sent Events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) is a server push technology that allows a server to send a stream of event messages to a client over a single HTTP connection. In .NET, the event messages are represented as [`SseItem`](/dotnet/api/system.net.serversentevents.sseitem-1) objects, which may contain an event type, an ID, and a data payload of type `T`. + +The [TypedResults](xref:Microsoft.AspNetCore.Http.TypedResults) class has a static method called [ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,ceb980606eb9e295) that can be used to return a `ServerSentEvents` result. The first parameter to this method is an `IAsyncEnumerable>` that represents the stream of event messages to be sent to the client. + +The following example illustrates how to use the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as JSON objects to the client: + +:::code language="csharp" source="~/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs" id="snippet_item" ::: + +For more information, see the [Minimal API sample app](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs) using the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as string, `ServerSentEvents`, and JSON objects to the client. + #### Redirect :::code language="csharp" source="~/fundamentals/minimal-apis/9.0-samples/Snippets/Program.cs" id="snippet_09"::: diff --git a/aspnetcore/release-notes/aspnetcore-10.0.md b/aspnetcore/release-notes/aspnetcore-10.0.md index cefb6b31014e..941232cc039a 100644 --- a/aspnetcore/release-notes/aspnetcore-10.0.md +++ b/aspnetcore/release-notes/aspnetcore-10.0.md @@ -37,6 +37,8 @@ This section describes new features for minimal APIs. [!INCLUDE[](~/release-notes/aspnetcore-10/includes/MinApiEmptyStringInFormPost.md)] +[!INCLUDE[](~/release-notes/aspnetcore-10/includes/sse.md)] + ## OpenAPI This section describes new features for OpenAPI. diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/sse.md b/aspnetcore/release-notes/aspnetcore-10/includes/sse.md new file mode 100644 index 000000000000..dd48c54d8340 --- /dev/null +++ b/aspnetcore/release-notes/aspnetcore-10/includes/sse.md @@ -0,0 +1,17 @@ +### Support for Server-Sent Events (SSE) + +ASP.NET Core now supports returning a [ServerSentEvents](xref:System.Net.ServerSentEvents) result using the [TypedResults.ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,051e6796e1492f84) API. This feature is supported in both Minimal APIs and controller-based apps. + +Server-Sent Events is a server push technology that allows a server to send a stream of event messages to a client over a single HTTP connection. In .NET the event messages are represented as [`SseItem`](/dotnet/api/system.net.serversentevents.sseitem-1) objects, which may contain an event type, an ID, and a data payload of type `T`. + +The [TypedResults](xref:Microsoft.AspNetCore.Http.TypedResults) class has a new static method called [ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,ceb980606eb9e295) that can be used to return a `ServerSentEvents` result. The first parameter to this method is an `IAsyncEnumerable>` that represents the stream of event messages to be sent to the client. + +The following example illustrates how to use the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as JSON objects to the client: + +:::code language="csharp" source="~/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs" id="snippet_json" ::: + +For more information, see: + +- [Server-Sent Events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) on MDN. +- [Minimal API sample app](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs) using the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as string, `ServerSentEvents`, and JSON objects to the client. +- [Controller API sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE) using the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as string, `ServerSentEvents`, and JSON objects to the client. diff --git a/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.csproj b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.csproj new file mode 100644 index 000000000000..b6fe0f1ed00c --- /dev/null +++ b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.http b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.http new file mode 100644 index 000000000000..960c3051d23b --- /dev/null +++ b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.http @@ -0,0 +1,15 @@ +@baseUrl = http://localhost:5201/HeartRate + +### Connect to SSE stream +# This request will open an SSE connection that stays open +GET {{baseUrl}}/string-item +Accept: text/event-stream + +### +GET {{baseUrl}}/json-item +Accept: text/event-stream + +### + +GET {{baseUrl}}/sse-item +Accept: text/event-stream \ No newline at end of file diff --git a/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Controllers/HeartRateController.cs b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Controllers/HeartRateController.cs new file mode 100644 index 000000000000..f42f9a57c468 --- /dev/null +++ b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Controllers/HeartRateController.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Mvc; +using System.Runtime.CompilerServices; +using System.Net.ServerSentEvents; + +[ApiController] +[Route("[controller]")] + +public class HeartRateController : ControllerBase +{ + // /HeartRate/json-item + [HttpGet("json-item")] + public IResult GetHeartRateJson(CancellationToken cancellationToken) + { + async IAsyncEnumerable StreamHeartRates( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + var heartRate = Random.Shared.Next(60, 100); + yield return HearRate.Create(heartRate); + await Task.Delay(2000, cancellationToken); + } + } + + return TypedResults.ServerSentEvents(StreamHeartRates(cancellationToken), eventType: "heartRate"); + } + + // /HeartRate/string-item + [HttpGet("string-item")] + + public IResult GetHeartRateString(CancellationToken cancellationToken) + { + async IAsyncEnumerable GetHeartRate( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + var heartRate = Random.Shared.Next(60, 100); + yield return $"Hear Rate: {heartRate} bpm"; + await Task.Delay(2000, cancellationToken); + } + } + + return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken), eventType: "heartRate"); + } + + // /HeartRate/sse-item + [HttpGet("sse-item")] + + public IResult GetHeartRateSSE(CancellationToken cancellationToken) + { + async IAsyncEnumerable> GetHeartRate( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + var heartRate = Random.Shared.Next(60, 100); + yield return new SseItem(heartRate, eventType: "heartRate") + { + ReconnectionInterval = TimeSpan.FromMinutes(1) + }; + await Task.Delay(2000, cancellationToken); + } + } + + return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken)); + } +} diff --git a/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/HearRate.cs b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/HearRate.cs new file mode 100644 index 000000000000..c784313bc4ad --- /dev/null +++ b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/HearRate.cs @@ -0,0 +1,4 @@ +public record HeartRate(DateTime Timestamp, int HeartRate) +{ + public static HeartRate Create(int heartRate) => new(DateTime.UtcNow, heartRate); +} diff --git a/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Program.cs b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Program.cs new file mode 100644 index 000000000000..08ee54a07da9 --- /dev/null +++ b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Program.cs @@ -0,0 +1,28 @@ + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// 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(); + + +app.MapControllers(); + +app.MapGet("/", () => Results.Redirect("/HeartRate/json-item")); + + +app.Run(); \ No newline at end of file