| File: ServerSentEventsResult.cs | Web Access |
| Project: src\aspnetcore\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"])); builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } }