File: ServerSentEventsResult.cs
Web Access
Project: src\src\Http\Http.Results\src\Microsoft.AspNetCore.Http.Results.csproj (Microsoft.AspNetCore.Http.Results)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Net.ServerSentEvents;
using Microsoft.AspNetCore.Http.Metadata;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using System.Text.Json;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http.Features;
 
namespace Microsoft.AspNetCore.Http.HttpResults;
 
/// <summary>
/// Represents a result that writes a stream of server-sent events to the response.
/// </summary>
/// <typeparam name="T">The underlying type of the events emitted.</typeparam>
public sealed class ServerSentEventsResult<T> : IResult, IEndpointMetadataProvider, IStatusCodeHttpResult
{
    private readonly IAsyncEnumerable<SseItem<T>> _events;
 
    /// <inheritdoc/>
    public int? StatusCode => StatusCodes.Status200OK;
 
    internal ServerSentEventsResult(IAsyncEnumerable<T> events, string? eventType)
    {
        _events = WrapEvents(events, eventType);
    }
 
    internal ServerSentEventsResult(IAsyncEnumerable<SseItem<T>> events)
    {
        _events = events;
    }
 
    /// <inheritdoc />
    public async Task ExecuteAsync(HttpContext httpContext)
    {
        ArgumentNullException.ThrowIfNull(httpContext);
 
        httpContext.Response.ContentType = "text/event-stream";
        httpContext.Response.Headers.CacheControl = "no-cache,no-store";
        httpContext.Response.Headers.Pragma = "no-cache";
        httpContext.Response.Headers.ContentEncoding = "identity";
 
        var bufferingFeature = httpContext.Features.GetRequiredFeature<IHttpResponseBodyFeature>();
        bufferingFeature.DisableBuffering();
 
        var jsonOptions = httpContext.RequestServices.GetService<IOptions<JsonOptions>>()?.Value ?? new JsonOptions();
 
        // If the event type is string, we can skip JSON serialization
        // and directly use the SseFormatter's WriteAsync overload for strings.
        if (_events is IAsyncEnumerable<SseItem<string>> stringEvents)
        {
            await SseFormatter.WriteAsync(stringEvents, httpContext.Response.Body, httpContext.RequestAborted);
            return;
        }
 
        await SseFormatter.WriteAsync(_events, httpContext.Response.Body,
            (item, writer) => FormatSseItem(item, writer, jsonOptions),
            httpContext.RequestAborted);
    }
 
    private static void FormatSseItem(SseItem<T> item, IBufferWriter<byte> writer, JsonOptions jsonOptions)
    {
        if (item.Data is null)
        {
            writer.Write([]);
            return;
        }
 
        // Handle byte arrays byt writing them directly as strings.
        if (item.Data is byte[] byteArray)
        {
            writer.Write(byteArray);
            return;
        }
 
        // For non-string types, use JSON serialization with options from DI
        var runtimeType = item.Data.GetType();
        var jsonTypeInfo = jsonOptions.SerializerOptions.GetTypeInfo(typeof(T));
 
        // Use the appropriate JsonTypeInfo based on whether we need polymorphic serialization
        var typeInfo = jsonTypeInfo.ShouldUseWith(runtimeType)
            ? jsonTypeInfo
            : jsonOptions.SerializerOptions.GetTypeInfo(typeof(object));
 
        var json = JsonSerializer.SerializeToUtf8Bytes(item.Data, typeInfo);
        writer.Write(json);
    }
 
    private static async IAsyncEnumerable<SseItem<T>> WrapEvents(IAsyncEnumerable<T> events, string? eventType = null)
    {
        await foreach (var item in events)
        {
            yield return new SseItem<T>(item, eventType);
        }
    }
 
    static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        ArgumentNullException.ThrowIfNull(method);
        ArgumentNullException.ThrowIfNull(builder);
 
        builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(SseItem<T>), contentTypes: ["text/event-stream"]));
    }
}