|
| 1 | +using System.Diagnostics.CodeAnalysis; |
1 | 2 | using System.Runtime.CompilerServices; |
2 | 3 | using System.Text.Json; |
3 | 4 | #if NET10_0_OR_GREATER |
@@ -82,57 +83,73 @@ public static IServiceCollection AddSseStream<T>(this IServiceCollection service |
82 | 83 | /// <item><description>Automatically unsubscribes and cleans up resources when the client disconnects.</description></item> |
83 | 84 | /// </list> |
84 | 85 | /// </remarks> |
| 86 | + [SuppressMessage("Design", "MA0051:Method is too long", |
| 87 | + Justification = "Method contains conditional compilation blocks for multi-framework support")] |
85 | 88 | public static RouteHandlerBuilder MapSse<T>(this IEndpointRouteBuilder endpoints, string pattern, |
86 | 89 | Func<T, object> payloadSelector, Func<T, string> eventTypeSelector) where T : class |
87 | 90 | { |
88 | 91 | #if NET10_0_OR_GREATER |
89 | | - return endpoints.MapGet(pattern, (ISseStream<T> stream, HttpContext context) => |
| 92 | + // Excluded from coverage: Only compiled on .NET 10+, covered by tests running on .NET 10 |
| 93 | + [ExcludeFromCodeCoverage] |
| 94 | + RouteHandlerBuilder BuildNet10Route() |
90 | 95 | { |
91 | | - var clientId = Guid.NewGuid(); |
92 | | - var reader = stream.Subscribe(clientId); |
93 | | - context.RequestAborted.Register(() => stream.Unsubscribe(clientId)); |
| 96 | + return endpoints.MapGet(pattern, (ISseStream<T> stream, HttpContext context) => |
| 97 | + { |
| 98 | + var clientId = Guid.NewGuid(); |
| 99 | + var reader = stream.Subscribe(clientId); |
| 100 | + context.RequestAborted.Register(() => stream.Unsubscribe(clientId)); |
94 | 101 |
|
95 | | - return TypedResults.ServerSentEvents(StreamEventsAsync(context.RequestAborted)); |
| 102 | + return TypedResults.ServerSentEvents(StreamEventsAsync(context.RequestAborted)); |
96 | 103 |
|
97 | | - async IAsyncEnumerable<SseItem<object>> StreamEventsAsync([EnumeratorCancellation] CancellationToken ct) |
98 | | - { |
99 | | - await foreach (var item in reader.ReadAllAsync(ct)) |
| 104 | + async IAsyncEnumerable<SseItem<object>> StreamEventsAsync([EnumeratorCancellation] CancellationToken ct) |
100 | 105 | { |
101 | | - var payload = payloadSelector(item); |
102 | | - var eventType = eventTypeSelector(item); |
103 | | - yield return new SseItem<object>(payload, eventType); |
| 106 | + await foreach (var item in reader.ReadAllAsync(ct)) |
| 107 | + { |
| 108 | + var payload = payloadSelector(item); |
| 109 | + var eventType = eventTypeSelector(item); |
| 110 | + yield return new SseItem<object>(payload, eventType); |
| 111 | + } |
104 | 112 | } |
105 | | - } |
106 | | - }); |
| 113 | + }); |
| 114 | + } |
| 115 | + |
| 116 | + return BuildNet10Route(); |
107 | 117 | #else |
108 | | - return endpoints.MapGet(pattern, async (ISseStream<T> stream, HttpContext context, CancellationToken ct) => |
| 118 | + // Excluded from coverage: Only compiled on .NET 8/9, covered by tests running on .NET 8/9 |
| 119 | + [ExcludeFromCodeCoverage] |
| 120 | + RouteHandlerBuilder BuildFallbackRoute() |
109 | 121 | { |
110 | | - var clientId = Guid.NewGuid(); |
111 | | - var reader = stream.Subscribe(clientId); |
112 | | - try |
| 122 | + return endpoints.MapGet(pattern, async (ISseStream<T> stream, HttpContext context, CancellationToken ct) => |
113 | 123 | { |
114 | | - context.Response.Headers.CacheControl = "no-cache"; |
115 | | - context.Response.Headers.Connection = "keep-alive"; |
116 | | - context.Response.Headers.ContentType = "text/event-stream"; |
| 124 | + var clientId = Guid.NewGuid(); |
| 125 | + var reader = stream.Subscribe(clientId); |
| 126 | + try |
| 127 | + { |
| 128 | + context.Response.Headers.CacheControl = "no-cache"; |
| 129 | + context.Response.Headers.Connection = "keep-alive"; |
| 130 | + context.Response.Headers.ContentType = "text/event-stream"; |
117 | 131 |
|
118 | | - // Start the response so headers are sent immediately, even before the first event |
119 | | - await context.Response.StartAsync(ct).ConfigureAwait(false); |
| 132 | + // Start the response so headers are sent immediately, even before the first event |
| 133 | + await context.Response.StartAsync(ct).ConfigureAwait(false); |
120 | 134 |
|
121 | | - await foreach (var item in reader.ReadAllAsync(ct).ConfigureAwait(false)) |
| 135 | + await foreach (var item in reader.ReadAllAsync(ct).ConfigureAwait(false)) |
| 136 | + { |
| 137 | + var payload = payloadSelector(item); |
| 138 | + var eventType = eventTypeSelector(item); |
| 139 | + var json = JsonSerializer.Serialize(payload); |
| 140 | + await context.Response.WriteAsync($"event: {eventType}\n", ct).ConfigureAwait(false); |
| 141 | + await context.Response.WriteAsync($"data: {json}\n\n", ct).ConfigureAwait(false); |
| 142 | + await context.Response.Body.FlushAsync(ct).ConfigureAwait(false); |
| 143 | + } |
| 144 | + } |
| 145 | + finally |
122 | 146 | { |
123 | | - var payload = payloadSelector(item); |
124 | | - var eventType = eventTypeSelector(item); |
125 | | - var json = JsonSerializer.Serialize(payload); |
126 | | - await context.Response.WriteAsync($"event: {eventType}\n", ct).ConfigureAwait(false); |
127 | | - await context.Response.WriteAsync($"data: {json}\n\n", ct).ConfigureAwait(false); |
128 | | - await context.Response.Body.FlushAsync(ct).ConfigureAwait(false); |
| 147 | + stream.Unsubscribe(clientId); |
129 | 148 | } |
130 | | - } |
131 | | - finally |
132 | | - { |
133 | | - stream.Unsubscribe(clientId); |
134 | | - } |
135 | | - }); |
| 149 | + }); |
| 150 | + } |
| 151 | + |
| 152 | + return BuildFallbackRoute(); |
136 | 153 | #endif |
137 | 154 | } |
138 | 155 | } |
0 commit comments