Skip to content

Add support for emitting ServerSentEvents from minimal APIs #56172

Closed
@captainsafia

Description

@captainsafia

Background and Motivation

This proposal adds first-class support for SSE responses through the IResult pattern, making it consistent with other response types in minimal APIs. While ASP.NET Core has supported SSE through manual response writing, there hasn't been a built-in IResult implementation to return SSE streams from minimal API endpoints.

Proposed API

// Assembly: Microsoft.AspNetCore.Http.Results
namespace Microsoft.AspNetCore.Http;

public static class TypedResults 
{
+    public static ServerSentEventResult<string> ServerSentEvents<T>(
+        IAsyncEnumerable<string> value, 
+        string? eventType = null);
+
+    public static ServerSentEventResult<T> ServerSentEvents<T>(
+        IAsyncEnumerable<T> value, 
+        string? eventType = null);
+
+    public static ServerSentEventResult<T> ServerSentEvents<T>(
+        IAsyncEnumerable<SseItem<T>> value);
}

+ public sealed class ServerSentEventResult<T> : IResult, IEndpointMetadataProvider
+ {
+    static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder)
+ }

Usage Examples

Basic usage with simple values:

app.MapGet("/sse", () =>
{
    async IAsyncEnumerable<string> GenerateEvents(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        var counter = 0;
        while (!cancellationToken.IsCancellationRequested)
        {
            yield return $"Event {counter++} at {DateTime.UtcNow}";
            await Task.Delay(1000, cancellationToken);
        }
    }

    return TypedResults.ServerSentEvents(GenerateEvents());
});

Usage with custom event types:

app.MapGet("/notifications", () =>
{
    async IAsyncEnumerable<Notification> GetNotifications(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            yield return new Notification("New message received");
            await Task.Delay(5000, cancellationToken);
        }
    }

    return TypedResults.ServerSentEvents(GetNotifications(), eventType: "notification");
});

Raw string processing with string overload:

app.MapGet("/mixed-events", () =>
{
    async IAsyncEnumerable<string> GetMixedEvents(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        yield return "Starting";
        yield return "Going";
        yield return "Finished";
    }

    return TypedResults.ServerSentEvents(GetMixedEvents());
});

Fine-grained control with the SseItem<T> overload:

app.MapGet("/mixed-events", () =>
{
    async IAsyncEnumerable<SseItem<string>> GetMixedEvents(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        yield return new SseItem<string>("System starting", "init");
        yield return new SseItem<string>("Processing data", "progress") { EventId = "some-guid" };
        yield return new SseItem<string>("Complete", "done");
    }

    return TypedResults.ServerSentEvents(GetMixedEvents());
});

Alternative Designs

  • The TypedResults extension methods do not expose an overload that takes an event ID. Instead, the user must use the SseItem<T> based overload of the extension method if they want to control the event ID at a more granular level.

Risks

N/A

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etc

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions