< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Models.KestrunContext
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Models/KestrunContext.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
49%
Covered lines: 73
Uncovered lines: 74
Coverable lines: 147
Total lines: 484
Line coverage: 49.6%
Branch coverage
33%
Covered branches: 25
Total branches: 74
Branch coverage: 33.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/12/2025 - 03:43:11 Line coverage: 100% (9/9) Branch coverage: 62.5% (5/8) Total lines: 54 Tag: Kestrun/Kestrun@d160286e3020330b1eb862d66a37db2e26fc904210/13/2025 - 16:52:37 Line coverage: 100% (30/30) Branch coverage: 62.5% (5/8) Total lines: 108 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e10/15/2025 - 21:27:26 Line coverage: 40% (30/75) Branch coverage: 25% (5/20) Total lines: 247 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559410/17/2025 - 15:48:30 Line coverage: 39.4% (30/76) Branch coverage: 25% (5/20) Total lines: 252 Tag: Kestrun/Kestrun@b8199aff869a847b75e185d0527ba45e04a43d8611/19/2025 - 02:25:56 Line coverage: 27.5% (30/109) Branch coverage: 11.3% (5/44) Total lines: 359 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 20.1% (22/109) Branch coverage: 11.3% (5/44) Total lines: 359 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/15/2025 - 02:23:46 Line coverage: 27.5% (30/109) Branch coverage: 11.3% (5/44) Total lines: 359 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833001/02/2026 - 00:16:25 Line coverage: 33% (39/118) Branch coverage: 25% (13/52) Total lines: 385 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/12/2026 - 18:03:06 Line coverage: 48.9% (67/137) Branch coverage: 35% (21/60) Total lines: 454 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/24/2026 - 19:35:59 Line coverage: 49.6% (73/147) Branch coverage: 33.7% (25/74) Total lines: 484 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa51 09/12/2025 - 03:43:11 Line coverage: 100% (9/9) Branch coverage: 62.5% (5/8) Total lines: 54 Tag: Kestrun/Kestrun@d160286e3020330b1eb862d66a37db2e26fc904210/13/2025 - 16:52:37 Line coverage: 100% (30/30) Branch coverage: 62.5% (5/8) Total lines: 108 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e10/15/2025 - 21:27:26 Line coverage: 40% (30/75) Branch coverage: 25% (5/20) Total lines: 247 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559410/17/2025 - 15:48:30 Line coverage: 39.4% (30/76) Branch coverage: 25% (5/20) Total lines: 252 Tag: Kestrun/Kestrun@b8199aff869a847b75e185d0527ba45e04a43d8611/19/2025 - 02:25:56 Line coverage: 27.5% (30/109) Branch coverage: 11.3% (5/44) Total lines: 359 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 20.1% (22/109) Branch coverage: 11.3% (5/44) Total lines: 359 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/15/2025 - 02:23:46 Line coverage: 27.5% (30/109) Branch coverage: 11.3% (5/44) Total lines: 359 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833001/02/2026 - 00:16:25 Line coverage: 33% (39/118) Branch coverage: 25% (13/52) Total lines: 385 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/12/2026 - 18:03:06 Line coverage: 48.9% (67/137) Branch coverage: 35% (21/60) Total lines: 454 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/24/2026 - 19:35:59 Line coverage: 49.6% (73/147) Branch coverage: 33.7% (25/74) Total lines: 484 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa51

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%88100%
get_Host()100%11100%
get_Logger()100%11100%
get_Request()100%11100%
get_Response()100%11100%
get_HttpContext()100%11100%
get_MapRouteOptions()100%11100%
get_Session()100%22100%
get_HasSession()100%11100%
TryGetSession(...)100%11100%
get_Ct()100%11100%
get_Items()100%11100%
get_HasRequestCulture()50%44100%
get_Culture()0%4260%
get_LocalizedStrings()50%44100%
get_Strings()100%210%
get_User()100%11100%
get_Connection()100%210%
get_TraceIdentifier()100%11100%
get_Parameters()100%11100%
ToString()50%66100%
BroadcastLogAsync()100%44100%
BroadcastLog(...)100%210%
BroadcastEventAsync()50%6450%
BroadcastEvent(...)100%210%
BroadcastToGroupAsync()50%6450%
BroadcastToGroup(...)100%210%
Challenge(...)100%210%
Challenge(...)0%4260%
Challenge(...)0%620%
ChallengeAsync(...)0%4260%
SignOut(...)100%210%
SignOut(...)0%2040%
SignOut(...)0%4260%
StartSse()100%210%
WriteSseEventAsync()0%7280%
WriteSseEvent(...)100%210%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Models/KestrunContext.cs

#LineLine coverage
 1
 2
 3using System.Collections;
 4using System.Globalization;
 5using System.Security.Claims;
 6using Kestrun.Hosting.Options;
 7using Kestrun.SignalR;
 8using Kestrun.Utilities;
 9using Microsoft.AspNetCore.Authentication;
 10using Microsoft.AspNetCore.Http.Features;
 11
 12namespace Kestrun.Models;
 13
 14/// <summary>
 15/// Represents the context for a Kestrun request, including the request, response, HTTP context, and host.
 16/// </summary>
 17public sealed record KestrunContext
 18{
 119    private static readonly IReadOnlyDictionary<string, string> EmptyStrings =
 120        new Dictionary<string, string>(StringComparer.Ordinal);
 21    /// <summary>
 22    /// Initializes a new instance of the <see cref="KestrunContext"/> class.
 23    /// This constructor is used when creating a new KestrunContext from an existing HTTP context.
 24    /// It initializes the KestrunRequest and KestrunResponse based on the provided HttpContext
 25    /// </summary>
 26    /// <param name="host">The Kestrun host.</param>
 27    /// <param name="httpContext">The associated HTTP context.</param>
 14628    public KestrunContext(Hosting.KestrunHost host, HttpContext httpContext)
 29    {
 14630        ArgumentNullException.ThrowIfNull(host);
 14631        ArgumentNullException.ThrowIfNull(httpContext);
 32
 14633        Host = host;
 14634        HttpContext = httpContext;
 35        // Initialize TraceIdentifier, Request, and Response
 14636        TraceIdentifier = HttpContext.TraceIdentifier;
 14637        Request = KestrunRequest.NewRequestSync(HttpContext);
 38
 39        // Ensure contexts created via this constructor always have a valid response.
 14640        Response = new KestrunResponse(this, 8192);
 41        // Routing metadata may not always be available (e.g., middleware/tests/exception handlers).
 42        // Fall back to the request path if no RouteEndpoint is present.
 14643        var routeEndpoint = Request.HttpContext.GetEndpoint() as RouteEndpoint;
 44
 14645        var pattern = routeEndpoint?.RoutePattern.RawText;
 46
 14647        if (string.IsNullOrWhiteSpace(pattern))
 48        {
 2849            pattern = "/";
 50        }
 51
 14652        var verb = string.IsNullOrWhiteSpace(Request.Method)
 14653            ? HttpVerb.Get
 14654            : HttpVerbExtensions.FromMethodString(Request.Method);
 55
 14656        if (!Host.RegisteredRoutes.TryGetValue((pattern, verb), out var options))
 57        {
 58            // default options
 13459            options = new MapRouteOptions()
 13460            {
 13461                Pattern = pattern,
 13462                HttpVerbs = [verb]
 13463            };
 64        }
 65
 14666        MapRouteOptions = options;
 14667    }
 68
 69    /// <summary>
 70    /// The Kestrun host associated with this context.
 71    /// </summary>
 34372    public Hosting.KestrunHost Host { get; init; }
 73
 74    /// <summary>
 75    /// The logger associated with the Kestrun host.
 76    /// </summary>
 1477    public Serilog.ILogger Logger => Host.Logger;
 78    /// <summary>
 79    /// The Kestrun request associated with this context.
 80    /// </summary>
 94181    public KestrunRequest Request { get; init; }
 82    /// <summary>
 83    /// The Kestrun response associated with this context.
 84    /// </summary>
 26685    public KestrunResponse Response { get; private set; }
 86    /// <summary>
 87    /// The ASP.NET Core HTTP context associated with this Kestrun context.
 88    /// </summary>
 64689    public HttpContext HttpContext { get; init; }
 90
 91    /// <summary>
 92    /// Gets the route options associated with this response.
 93    /// </summary>
 15494    public MapRouteOptions MapRouteOptions { get; init; }
 95    /// <summary>
 96    /// Returns the ASP.NET Core session if the Session middleware is active; otherwise null.
 97    /// </summary>
 1498    public ISession? Session => HttpContext.Features.Get<ISessionFeature>()?.Session;
 99
 100    /// <summary>
 101    /// True if Session middleware is active for this request.
 102    /// </summary>
 4103    public bool HasSession => Session is not null;
 104
 105    /// <summary>
 106    /// Try pattern to get session without exceptions.
 107    /// </summary>
 108    public bool TryGetSession(out ISession? session)
 109    {
 2110        session = Session;
 2111        return session is not null;
 112    }
 113
 114    /// <summary>
 115    /// Gets the cancellation token that is triggered when the HTTP request is aborted.
 116    /// </summary>
 1117    public CancellationToken Ct => HttpContext.RequestAborted;
 118    /// <summary>
 119    /// Gets the collection of key/value pairs associated with the current HTTP context.
 120    /// </summary>
 3121    public IDictionary<object, object?> Items => HttpContext.Items;
 122
 123    /// <summary>
 124    /// Gets the resolved request culture when localization middleware is enabled.
 125    /// </summary>
 126    public bool HasRequestCulture =>
 8127        HttpContext.Items.TryGetValue("KrCulture", out var value) && value is string culture && !string.IsNullOrWhiteSpa
 128
 129    /// <summary>
 130    /// Gets the resolved request culture when localization middleware is enabled.
 131    /// </summary>
 132    public string Culture =>
 0133        HttpContext.Items.TryGetValue("KrCulture", out var value) && value is string culture && !string.IsNullOrWhiteSpa
 0134            ? culture
 0135            : CultureInfo.CurrentCulture.Name;
 136
 137    /// <summary>
 138    /// Gets the localized string table for the resolved culture when localization middleware is enabled.
 139    /// </summary>
 140    public IReadOnlyDictionary<string, string> LocalizedStrings =>
 4141        HttpContext.Items.TryGetValue("KrStrings", out var value) && value is IReadOnlyDictionary<string, string> string
 4142            ? strings
 4143            : EmptyStrings;
 144
 145    /// <summary>
 146    /// Gets the localized string table for the resolved culture when localization middleware is enabled.
 147    /// </summary>
 0148    public IReadOnlyDictionary<string, string> Strings => LocalizedStrings;
 149
 150    /// <summary>
 151    /// Gets the user associated with the current HTTP context.
 152    /// </summary>
 2153    public ClaimsPrincipal User => HttpContext.User;
 154
 155    /// <summary>
 156    /// Gets the connection information for the current HTTP context.
 157    /// </summary>
 0158    public ConnectionInfo Connection => HttpContext.Connection;
 159
 160    /// <summary>
 161    /// Gets the trace identifier for the current HTTP context.
 162    /// </summary>
 153163    public string TraceIdentifier { get; init; }
 164
 165    /// <summary>
 166    /// A dictionary to hold  parameters passed by user for use within the KestrunContext.
 167    /// </summary>
 184168    public ResolvedRequestParameters Parameters { get; internal set; } = new ResolvedRequestParameters();
 169
 170    /// <summary>
 171    /// Returns a string representation of the KestrunContext, including path, user, and session status.
 172    /// </summary>
 173    public override string ToString()
 2174        => $"KestrunContext{{ Host={Host}, Path={HttpContext.Request.Path}, User={User?.Identity?.Name ?? "<anon>"}, Has
 175
 176    /// <summary>
 177    /// Asynchronously broadcasts a log message to all connected SignalR clients using the IRealtimeBroadcaster service.
 178    /// </summary>
 179    /// <param name="level">The log level (e.g., Information, Warning, Error, Debug, Verbose).</param>
 180    /// <param name="message">The log message to broadcast.</param>
 181    /// <param name="cancellationToken">Optional: Cancellation token.</param>
 182    /// <returns>True if the log was broadcast successfully; otherwise, false.</returns>
 183    public async Task<bool> BroadcastLogAsync(string level, string message, CancellationToken cancellationToken = defaul
 184    {
 4185        var svcProvider = HttpContext.RequestServices;
 186
 4187        if (svcProvider == null)
 188        {
 1189            Logger.Warning("No service provider available to resolve IRealtimeBroadcaster.");
 1190            return false;
 191        }
 3192        if (svcProvider.GetService(typeof(IRealtimeBroadcaster)) is not IRealtimeBroadcaster broadcaster)
 193        {
 1194            Logger.Warning("IRealtimeBroadcaster service is not registered. Make sure SignalR is configured with Kestrun
 1195            return false;
 196        }
 197        try
 198        {
 2199            await broadcaster.BroadcastLogAsync(level, message, cancellationToken);
 1200            Logger.Debug("Broadcasted log message via SignalR: {Level} - {Message}", level, message);
 1201            return true;
 202        }
 1203        catch (Exception ex)
 204        {
 1205            Logger.Error(ex, "Failed to broadcast log message: {Level} - {Message}", level, message);
 1206            return false;
 207        }
 4208    }
 209
 210    /// <summary>
 211    /// Synchronous wrapper for BroadcastLogAsync.
 212    /// </summary>
 213    /// <param name="level">The log level (e.g., Information, Warning, Error, Debug, Verbose).</param>
 214    /// <param name="message">The log message to broadcast.</param>
 215    /// <param name="cancellationToken">Optional: Cancellation token.</param>
 216    /// <returns>True if the log was broadcast successfully; otherwise, false.</returns>
 217    public bool BroadcastLog(string level, string message, CancellationToken cancellationToken = default) =>
 0218        BroadcastLogAsync(level, message, cancellationToken).GetAwaiter().GetResult();
 219
 220    /// <summary>
 221    /// Asynchronously broadcasts a custom event to all connected SignalR clients using the IRealtimeBroadcaster service
 222    /// </summary>
 223    /// <param name="eventName">The event name (e.g., Information, Warning, Error, Debug, Verbose).</param>
 224    /// <param name="data">The event data to broadcast.</param>
 225    /// <param name="cancellationToken">Optional: Cancellation token.</param>
 226    /// <returns>True if the event was broadcast successfully; otherwise, false.</returns>
 227    public async Task<bool> BroadcastEventAsync(string eventName, object? data, CancellationToken cancellationToken = de
 228    {
 1229        var svcProvider = HttpContext.RequestServices;
 230
 1231        if (svcProvider == null)
 232        {
 0233            Logger.Warning("No service provider available to resolve IRealtimeBroadcaster.");
 0234            return false;
 235        }
 1236        if (svcProvider.GetService(typeof(IRealtimeBroadcaster)) is not IRealtimeBroadcaster broadcaster)
 237        {
 0238            Logger.Warning("IRealtimeBroadcaster service is not registered. Make sure SignalR is configured with Kestrun
 0239            return false;
 240        }
 241        try
 242        {
 1243            await broadcaster.BroadcastEventAsync(eventName, data, cancellationToken);
 1244            Logger.Debug("Broadcasted event via SignalR: {EventName} - {Data}", eventName, data);
 1245            return true;
 246        }
 0247        catch (Exception ex)
 248        {
 0249            Logger.Error(ex, "Failed to broadcast event: {EventName} - {Data}", eventName, data);
 0250            return false;
 251        }
 1252    }
 253
 254    /// <summary>
 255    /// Synchronous wrapper for BroadcastEventAsync.
 256    /// </summary>
 257    /// <param name="eventName">The event name (e.g., Information, Warning, Error, Debug, Verbose).</param>
 258    /// <param name="data">The event data to broadcast.</param>
 259    /// <param name="cancellationToken">Optional: Cancellation token.</param>
 260    /// <returns>True if the event was broadcast successfully; otherwise, false.</returns>
 261    public bool BroadcastEvent(string eventName, object? data, CancellationToken cancellationToken = default) =>
 0262      BroadcastEventAsync(eventName, data, cancellationToken).GetAwaiter().GetResult();
 263
 264    /// <summary>
 265    /// Asynchronously broadcasts a message to a specific group of SignalR clients using the IRealtimeBroadcaster servic
 266    /// </summary>
 267    /// <param name="groupName">The name of the group to broadcast the message to.</param>
 268    /// <param name="method">The name of the method to invoke on the client.</param>
 269    /// <param name="message">The message to broadcast.</param>
 270    /// <param name="cancellationToken">Optional: Cancellation token.</param>
 271    /// <returns>True if the message was broadcast successfully; otherwise, false.</returns>
 272    public async Task<bool> BroadcastToGroupAsync(string groupName, string method, object? message, CancellationToken ca
 273    {
 1274        var svcProvider = HttpContext.RequestServices;
 275
 1276        if (svcProvider == null)
 277        {
 0278            Logger.Warning("No service provider available to resolve IRealtimeBroadcaster.");
 0279            return false;
 280        }
 1281        if (svcProvider.GetService(typeof(IRealtimeBroadcaster)) is not IRealtimeBroadcaster broadcaster)
 282        {
 0283            Logger.Warning("IRealtimeBroadcaster service is not registered. Make sure SignalR is configured with Kestrun
 0284            return false;
 285        }
 286        try
 287        {
 1288            await broadcaster.BroadcastToGroupAsync(groupName, method, message, cancellationToken);
 1289            Logger.Debug("Broadcasted log message to group via SignalR: {GroupName} - {Method} - {Message}", groupName, 
 1290            return true;
 291        }
 0292        catch (Exception ex)
 293        {
 0294            Logger.Error(ex, "Failed to broadcast log message: {GroupName} - {Method} - {Message}", groupName, method, m
 0295            return false;
 296        }
 1297    }
 298
 299    /// <summary>
 300    /// Synchronous wrapper for BroadcastToGroupAsync.
 301    /// </summary>
 302    /// <param name="groupName">The name of the group to broadcast the message to.</param>
 303    /// <param name="method">The name of the method to invoke on the client.</param>
 304    /// <param name="message">The message to broadcast.</param>
 305    /// <returns>True if the message was broadcast successfully; otherwise, false.</returns>
 306    public bool BroadcastToGroup(string groupName, string method, object? message) =>
 0307      BroadcastToGroupAsync(groupName, method, message, default).GetAwaiter().GetResult();
 308
 309    /// <summary>
 310    /// Synchronous wrapper for HttpContext.ChallengeAsync.
 311    /// </summary>
 312    /// <param name="scheme">The authentication scheme to challenge.</param>
 313    /// <param name="properties">The authentication properties to include in the challenge.</param>
 0314    public void Challenge(string? scheme, AuthenticationProperties? properties) => HttpContext.ChallengeAsync(scheme, pr
 315
 316    /// <summary>
 317    /// Synchronous wrapper for HttpContext.ChallengeAsync using a Hashtable for properties.
 318    /// </summary>
 319    /// <param name="scheme">The authentication scheme to challenge.</param>
 320    /// <param name="properties">The authentication properties to include in the challenge.</param>
 321    public void Challenge(string? scheme, Hashtable? properties)
 322    {
 0323        var dict = new Dictionary<string, string?>();
 0324        if (properties != null)
 325        {
 0326            foreach (DictionaryEntry entry in properties)
 327            {
 0328                dict[entry.Key.ToString()!] = entry.Value?.ToString();
 329            }
 330        }
 0331        AuthenticationProperties authProps = new(dict);
 0332        HttpContext.ChallengeAsync(scheme, authProps).GetAwaiter().GetResult();
 0333    }
 334
 335    /// <summary>
 336    /// Synchronous wrapper for HttpContext.ChallengeAsync using a Dictionary for properties.
 337    /// </summary>
 338    /// <param name="scheme">The authentication scheme to challenge.</param>
 339    /// <param name="properties">The authentication properties to include in the challenge.</param>
 340    public void Challenge(string? scheme, Dictionary<string, string?>? properties)
 341    {
 0342        if (properties == null)
 343        {
 0344            HttpContext.ChallengeAsync(scheme).GetAwaiter().GetResult();
 0345            return;
 346        }
 347
 0348        AuthenticationProperties authProps = new(properties);
 0349        HttpContext.ChallengeAsync(scheme, authProps).GetAwaiter().GetResult();
 0350    }
 351
 352    /// <summary>
 353    /// Asynchronous wrapper for HttpContext.ChallengeAsync using a Hashtable for properties.
 354    /// </summary>
 355    /// <param name="scheme">The authentication scheme to challenge.</param>
 356    /// <param name="properties">The authentication properties to include in the challenge.</param>
 357    /// <returns>Task representing the asynchronous operation.</returns>
 358    public Task ChallengeAsync(string? scheme, Hashtable? properties)
 359    {
 0360        var dict = new Dictionary<string, string?>();
 0361        if (properties != null)
 362        {
 0363            foreach (DictionaryEntry entry in properties)
 364            {
 0365                dict[entry.Key.ToString()!] = entry.Value?.ToString();
 366            }
 367        }
 0368        AuthenticationProperties authProps = new(dict);
 0369        return HttpContext.ChallengeAsync(scheme, authProps);
 370    }
 371
 372    /// <summary>
 373    /// Synchronous wrapper for HttpContext.SignOutAsync.
 374    /// </summary>
 375    /// <param name="scheme">The authentication scheme to sign out.</param>
 0376    public void SignOut(string? scheme) => HttpContext.SignOutAsync(scheme).GetAwaiter().GetResult();
 377    /// <summary>
 378    /// Synchronous wrapper for HttpContext.SignOutAsync.
 379    /// </summary>
 380    /// <param name="scheme">The authentication scheme to sign out.</param>
 381    /// <param name="properties">The authentication properties to include in the sign-out.</param>
 382    public void SignOut(string? scheme, AuthenticationProperties? properties)
 383    {
 0384        HttpContext.SignOutAsync(scheme, properties).GetAwaiter().GetResult();
 0385        if (properties != null && !string.IsNullOrWhiteSpace(properties.RedirectUri))
 386        {
 0387            Response.WriteStatusOnly(302);
 388        }
 0389    }
 390
 391    /// <summary>
 392    /// Synchronous wrapper for HttpContext.SignOutAsync using a Hashtable for properties.
 393    /// </summary>
 394    /// <param name="scheme">The authentication scheme to sign out.</param>
 395    /// <param name="properties">The authentication properties to include in the sign-out.</param>
 396    public void SignOut(string? scheme, Hashtable? properties)
 397    {
 0398        AuthenticationProperties? authProps = null;
 399        // Convert Hashtable to Dictionary<string, string?> for AuthenticationProperties
 0400        if (properties is not null)
 401        {
 0402            var dict = new Dictionary<string, string?>();
 403            // Convert each entry in the Hashtable to a string key-value pair
 0404            foreach (DictionaryEntry entry in properties)
 405            {
 0406                dict[entry.Key.ToString()!] = entry.Value?.ToString();
 407            }
 408            // Create AuthenticationProperties from the dictionary
 0409            authProps = new AuthenticationProperties(dict);
 410        }
 411        // Call SignOut with the constructed AuthenticationProperties
 0412        SignOut(scheme, authProps);
 0413    }
 414    #region Sse Helpers
 415    /// <summary>
 416    /// Starts a Server-Sent Events (SSE) response by setting the appropriate headers.
 417    /// </summary>
 418    public void StartSse()
 419    {
 0420        HttpContext.Response.Headers.CacheControl = "no-cache";
 0421        HttpContext.Response.Headers.Connection = "keep-alive";
 0422        HttpContext.Response.Headers["X-Accel-Buffering"] = "no"; // helps with nginx
 0423        HttpContext.Response.ContentType = "text/event-stream";
 0424    }
 425
 426    /// <summary>
 427    /// Writes a Server-Sent Event (SSE) to the response stream.
 428    /// </summary>
 429    /// <param name="event">The event type</param>
 430    /// <param name="data">The data payload of the event</param>
 431    /// <param name="id"> The event ID.</param>
 432    /// <param name="retryMs">Reconnection time in milliseconds</param>
 433    /// <param name="ct">Cancellation token</param>
 434    /// <returns> Task representing the asynchronous write operation.</returns>
 435    public async Task WriteSseEventAsync(
 436        string? @event,
 437        string data,
 438        string? id = null,
 439        int? retryMs = null,
 440        CancellationToken ct = default)
 441    {
 442        // SSE fields are line based
 0443        if (retryMs is not null)
 444        {
 0445            await HttpContext.Response.WriteAsync($"retry: {retryMs}\n", ct);
 446        }
 447
 0448        if (id is not null)
 449        {
 0450            await HttpContext.Response.WriteAsync($"id: {id}\n", ct);
 451        }
 452
 0453        if (!string.IsNullOrWhiteSpace(@event))
 454        {
 0455            await HttpContext.Response.WriteAsync($"event: {@event}\n", ct);
 456        }
 457
 458        // data can be multi-line; each line must be prefixed with "data: "
 0459        using (var sr = new StringReader(data))
 460        {
 461            string? line;
 0462            while ((line = sr.ReadLine()) is not null)
 463            {
 0464                await HttpContext.Response.WriteAsync($"data: {line}\n", ct);
 465            }
 0466        }
 467
 0468        await HttpContext.Response.WriteAsync("\n", ct);          // end of event
 0469        await HttpContext.Response.Body.FlushAsync(ct);          // important!
 0470    }
 471
 472    /// <summary>
 473    /// Synchronous wrapper for WriteSseEventAsync.
 474    /// </summary>
 475    /// <param name="event">The name of the event.</param>
 476    /// <param name="data">The data payload of the event.</param>
 477    /// <param name="id"> The event ID.</param>
 478    /// <param name="retryMs">Reconnection time in milliseconds</param>
 479    public void WriteSseEvent(
 480      string? @event, string data, string? id = null, int? retryMs = null) =>
 0481        WriteSseEventAsync(@event, data, id, retryMs, CancellationToken.None).GetAwaiter().GetResult();
 482
 483    #endregion
 484}