-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Add support for ServerSentEventsResult and extension methods #60616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
5d844ff
de86920
6f1f6be
664aa6d
ff67f8d
b0c76b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,10 @@ | ||
#nullable enable | ||
Microsoft.AspNetCore.Http.HttpResults.ServerSentEventsResult<T> | ||
Microsoft.AspNetCore.Http.HttpResults.ServerSentEventsResult<T>.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! | ||
Microsoft.AspNetCore.Http.HttpResults.ServerSentEventsResult<T>.StatusCode.get -> int? | ||
static Microsoft.AspNetCore.Http.HttpResults.RedirectHttpResult.IsLocalUrl(string? url) -> bool | ||
static Microsoft.AspNetCore.Http.Results.ServerSentEvents(System.Collections.Generic.IAsyncEnumerable<string!>! value, string? eventType = null) -> Microsoft.AspNetCore.Http.IResult! | ||
static Microsoft.AspNetCore.Http.Results.ServerSentEvents<T>(System.Collections.Generic.IAsyncEnumerable<System.Net.ServerSentEvents.SseItem<T>>! value) -> Microsoft.AspNetCore.Http.IResult! | ||
static Microsoft.AspNetCore.Http.Results.ServerSentEvents<T>(System.Collections.Generic.IAsyncEnumerable<T>! value, string? eventType = null) -> Microsoft.AspNetCore.Http.IResult! | ||
static Microsoft.AspNetCore.Http.TypedResults.ServerSentEvents(System.Collections.Generic.IAsyncEnumerable<string!>! values, string? eventType = null) -> Microsoft.AspNetCore.Http.HttpResults.ServerSentEventsResult<string!>! | ||
static Microsoft.AspNetCore.Http.TypedResults.ServerSentEvents<T>(System.Collections.Generic.IAsyncEnumerable<System.Net.ServerSentEvents.SseItem<T>>! values) -> Microsoft.AspNetCore.Http.HttpResults.ServerSentEventsResult<T>! | ||
static Microsoft.AspNetCore.Http.TypedResults.ServerSentEvents<T>(System.Collections.Generic.IAsyncEnumerable<T>! values, string? eventType = null) -> Microsoft.AspNetCore.Http.HttpResults.ServerSentEventsResult<T>! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// 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 System.Text; | ||
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; | ||
|
||
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"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider following what we do in SignalR There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, and line 47 if that's something we care about. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bleh -- I am torn about adding the Firefox workaround here. That seems like something the Maybe we leave it out for now and see what kind of feedback we get? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, until we see feedback I think it's less interesting for an API. For SignalR it was important because we want to signal that the connection is active and let the user start sending messages. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason not to disable buffering? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
What is that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should do what SignalR is doing, we should not wait for feedback. Disable buffering and do the crazy IIS workaround 😄 (did you test this in IIS?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
SignalR applies a special workaround to send some filler data through the SSE stream before the actual data to force Firefox's EventSource to emit an open event. It's a reaction to this bug in Firefox. The discussion is around whether or not to apply this workaround here in the ServerSentEventResult or have it be part of the default writing beahvior in the SseFormatter. |
||
|
||
await SseFormatter.WriteAsync(_events, httpContext.Response.Body, | ||
(item, writer) => FormatSseItem(item, writer, httpContext), | ||
httpContext.RequestAborted); | ||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
private static void FormatSseItem(SseItem<T> item, IBufferWriter<byte> writer, HttpContext httpContext) | ||
{ | ||
// Emit string and null values as-is | ||
if (item.Data is string stringData) | ||
{ | ||
writer.Write(Encoding.UTF8.GetBytes(stringData)); | ||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return; | ||
} | ||
|
||
if (item.Data is null) | ||
{ | ||
writer.Write([]); | ||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return; | ||
} | ||
|
||
// For non-string types, use JSON serialization with options from DI | ||
var jsonOptions = httpContext.RequestServices.GetService<IOptions<JsonOptions>>()?.Value ?? new JsonOptions(); | ||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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.Serialize(item.Data, typeInfo); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There must be a more performant way to do this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JsonSerializer.SerializeToUtf8Bytes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Slightly better since it avoids the string, but it still allocates a byte[] that we have to copy into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks like SerializeToUtf8Bytes boils down to roughly
but I'm not familiar enough to be sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to utilize ReusableUtf8JsonWriter? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would exposing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yep! I think that would be a nice solution. |
||
writer.Write(Encoding.UTF8.GetBytes(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"])); | ||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.