Share via


Verwenden von HttpContext in ASP.NET Core

HttpContext kapselt sämtliche Informationen zu einer einzelnen HTTP-Anforderung und -Antwort. Eine HttpContext-Instanz wird initialisiert, wenn eine HTTP-Anforderung empfangen wird. Auf die HttpContext-Instanz kann von Middleware und App-Frameworks wie Web-API-Controllern, Razor Pages, SignalR, gRPC und mehr zugegriffen werden.

Weitere Informationen zum Zugriff auf HttpContext finden Sie unter Zugreifen auf HttpContext in ASP.NET Core.

HttpRequest

HttpContext.Request ermöglicht den Zugriff auf HttpRequest. HttpRequest umfasst Informationen zur eingehenden HTTP-Anforderung und wird initialisiert, wenn eine HTTP-Anforderung vom Server empfangen wird. HttpRequest ist nicht schreibgeschützt, und die Middleware kann Anforderungswerte in der Middlewarepipeline ändern.

Gängige Member von HttpRequest sind unter anderem:

Eigenschaft BESCHREIBUNG Beispiel
HttpRequest.Path Der Anforderungspfad. /en/article/getstarted
HttpRequest.Method Der Anforderungsmethode. GET
HttpRequest.Headers Eine Auflistung von Anforderungsheadern. user-agent=Edge
x-custom-header=MyValue
HttpRequest.RouteValues Eine Sammlung von Routenwerten. Die Sammlung wird eingerichtet, wenn die Anforderung mit einer Route abgeglichen wird. language=en
article=getstarted
HttpRequest.Query Eine Sammlung von Abfragewerten, die aus QueryString geparst wurden. filter=hello
page=1
HttpRequest.ReadFormAsync() Eine Methode, die den Anforderungstext als Formular liest und eine Sammlung von Formularwerten zurückgibt. Informationen dazu, warum ReadFormAsync für den Zugriff auf Formulardaten verwendet werden sollte, finden Sie unter Bevorzugen von ReadFormAsync gegenüber Request.Form. email=user@contoso.com
password=TNkt4taM
HttpRequest.Body Ein Stream zum Lesen des Anforderungstexts. UTF-8 JSON-Nutzdaten

Abrufen von Anforderungsheadern

HttpRequest.Headers bietet Zugriff auf die mit der HTTP-Anforderung gesendeten Anforderungsheader. Es gibt zwei Möglichkeiten, mithilfe dieser Sammlung auf Header zuzugreifen:

  • Übergeben Sie den Headernamen an den Indexer für die Headersammlung. Beim Headernamen wird nicht zwischen Groß- und Kleinschreibung unterschieden. Der Indexer kann auf einen beliebigen Headerwert zugreifen.
  • Die Headersammlung enthält außerdem Eigenschaften zum Abrufen und Festlegen häufig verwendeter HTTP-Header. Die Eigenschaften ermöglichen einen schnellen, IntelliSense-basierten Zugriff auf die Header.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", (HttpRequest request) =>
{
    var userAgent = request.Headers.UserAgent;
    var customHeader = request.Headers["x-custom-header"];

    return Results.Ok(new { userAgent = userAgent, customHeader = customHeader });
});

app.Run();

Informationen zur effizienten Behandlung von Kopfzeilen, die mehrmals angezeigt werden, finden Sie unter Ein kurzer Einblick in StringValues.

Lesen des Anforderungstexts

Eine HTTP-Anforderung kann einen Anforderungstext enthalten. Der Anforderungstext besteht aus Daten, die mit der Anforderung verknüpft sind, z. B. Inhalte eines HTML-Formulars, UTF-8 JSON-Nutzdaten oder eine Datei.

HttpRequest.Body erlaubt das Lesen des Anforderungstexts mit Stream:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpContext context) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await context.Request.Body.CopyToAsync(writeStream);
});

app.Run();

HttpRequest.Body kann direkt gelesen oder mit anderen APIs verwendet werden, die „Stream“ akzeptieren.

Hinweis

Minimal-APIs unterstützen die direkte Bindung von HttpRequest.Body an einen Stream-Parameter.

Aktivieren der Anforderungstextpufferung

Der Anforderungstext kann nur einmal gelesen werden, von Anfang bis Ende. Durch das Vorwärtslesen des Anforderungstexts entfällt der Overhead, der durch das Zwischenspeichern des gesamten Anforderungstexts entsteht, sodass weniger Arbeitsspeicher benötigt wird. In einigen Szenarien ist es jedoch erforderlich, den Anforderungstext mehrmals zu lesen. Beispielsweise muss die Middleware den Anforderungstext lesen und dann auf den Anfang zurücksetzen, damit er für den Endpunkt verfügbar ist.

Die Erweiterungsmethode EnableBuffering ermöglicht die Pufferung des HTTP-Anforderungstexts und ist der empfohlene Weg, um mehrere Lesevorgänge zu ermöglichen. Da eine Anforderung eine beliebige Größe haben kann, unterstützt EnableBuffering Optionen für die Pufferung großer Anforderungskörper auf dem Datenträger oder die Ablehnung von Anforderungen.

Die Middleware übernimmt im folgenden Beispiel diese Aufgaben:

  • Sie aktiviert mehrere Lesevorgänge mit EnableBuffering. Der Aufruf muss vor dem Lesen des Anforderungstexts erfolgen.
  • Sie liest den Anforderungstext.
  • Sie setzt den Anforderungstext auf den Anfang zurück, damit andere Middleware oder der Endpunkt ihn lesen können.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    context.Request.EnableBuffering();
    await ReadRequestBody(context.Request.Body);
    context.Request.Body.Position = 0;
    
    await next.Invoke();
});

app.Run();

BodyReader

Eine alternative Möglichkeit zum Lesen des Anforderungstexts ist die Verwendung der Eigenschaft HttpRequest.BodyReader. Die Eigenschaft BodyReader macht den Anforderungstext als PipeReader verfügbar. Diese API stammt aus System.IO.Pipelines, einer fortschrittlichen, leistungsstarken Methode zum Lesen des Anforderungstexts.

Der Reader greift direkt auf den Anforderungstext zu und verwaltet den Arbeitsspeicher im Namen des Aufrufers. Im Gegensatz zu HttpRequest.Body kopiert der Reader die Anforderungsdaten nicht in einen Puffer. Ein Reader ist jedoch in der Anwendung komplexer als ein Stream und sollte mit Bedacht eingesetzt werden.

Informationen zum Lesen von Inhalten aus BodyReader finden Sie unter E/A-Pipelines: PipeReader.

HttpResponse

HttpContext.Response ermöglicht den Zugriff auf HttpResponse. HttpResponse wird verwendet, um Informationen für die an den Client zurückgesendete HTTP-Antwort festzulegen.

Gängige Member von HttpResponse sind unter anderem:

Eigenschaft BESCHREIBUNG Beispiel
HttpResponse.StatusCode Der Antwortcode. Muss vor dem Schreibvorgang in den Antworttext festgelegt werden. 200
HttpResponse.ContentType Der content-type-Header der Antwort. Muss vor dem Schreibvorgang in den Antworttext festgelegt werden. application/json
HttpResponse.Headers Die Sammlung von Antwortheadern. Muss vor dem Schreibvorgang in den Antworttext festgelegt werden. server=Kestrel
x-custom-header=MyValue
HttpResponse.Body Ein Stream zum Schreiben des Antworttexts. Generierte Webseite

Festlegen von Antwortheadern

HttpResponse.Headers bietet Zugriff auf die mit der HTTP-Antwort gesendeten Antwortheader. Es gibt zwei Möglichkeiten, mithilfe dieser Sammlung auf Header zuzugreifen:

  • Übergeben Sie den Headernamen an den Indexer für die Headersammlung. Beim Headernamen wird nicht zwischen Groß- und Kleinschreibung unterschieden. Der Indexer kann auf einen beliebigen Headerwert zugreifen.
  • Die Headersammlung enthält außerdem Eigenschaften zum Abrufen und Festlegen häufig verwendeter HTTP-Header. Die Eigenschaften ermöglichen einen schnellen, IntelliSense-basierten Zugriff auf die Header.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", (HttpResponse response) =>
{
    response.Headers.CacheControl = "no-cache";
    response.Headers["x-custom-header"] = "Custom value";

    return Results.File(File.OpenRead("helloworld.txt"));
});

app.Run();

Eine Anwendung kann die Header nach dem Start des Antwortvorgangs nicht mehr ändern. Sobald die Antwort gestartet wird, werden die Header an den Client gesendet. Ein Antwortvorgang wird durch das Leeren des Antworttexts oder durch den Aufruf von HttpResponse.StartAsync(CancellationToken) gestartet. Die Eigenschaft HttpResponse.HasStarted zeigt an, ob der Antwortvorgang gestartet wurde. Der Versuch, nach dem Start des Antwortvorgangs einen Header zu ändern, führt zu einem Fehler:

System.InvalidOperationException: Header sind schreibgeschützt, der Antwortvorgang wurde bereits gestartet.

Hinweis

Wenn die Antwortpufferung nicht aktiviert ist, leeren alle Schreibvorgänge (z. B. WriteAsync) den Antworttext intern, und markieren Sie die Antwort als gestartet. Die Antwortpufferung ist standardmäßig deaktiviert.

Schreiben des Antworttexts

Eine HTTP-Antwort kann einen Antworttext enthalten. Der Antworttext besteht aus Daten, die mit der Antwort verknüpft sind, z. B. generierte Webseiteninhalte, UTF-8 JSON-Nutzdaten oder eine Datei.

HttpResponse.Body erlaubt das Schreiben des Antworttexts mit Stream:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/downloadfile", async (IConfiguration config, HttpContext context) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], "helloworld.txt");

    await using var fileStream = File.OpenRead(filePath);
    await fileStream.CopyToAsync(context.Response.Body);
});

app.Run();

HttpResponse.Body kann direkt geschrieben oder mit anderen APIs verwendet werden, die in einen Stream schreiben.

BodyWriter

Eine alternative Möglichkeit zum Schreiben des Antworttexts ist die Verwendung der Eigenschaft HttpResponse.BodyWriter. Die Eigenschaft BodyWriter macht den Antworttext als PipeWriter verfügbar. Diese API stammt aus System.IO.Pipelines, einer fortschrittlichen, leistungsstarken Methode zum Schreiben des Anforderungstexts.

Der Writer bietet direkten Zugriff auf den Antworttext und verwaltet den Arbeitsspeicher im Namen des Aufrufers. Im Gegensatz zu HttpResponse.Body kopiert der Writer die Anforderungsdaten nicht in einen Puffer. Ein Writer ist jedoch in der Anwendung komplexer als ein Stream, und der Writercode sollte umfassend getestet werden.

Informationen zum Schreiben von Inhalten in BodyWriter finden Sie unter E/A-Pipelines: PipeWriter.

Festlegen eines Antwortnachspanns

HTTP/2 und HTTP/3 bieten Unterstützung für einen Antwortnachspann. Ein Nachspann ist ein Header, der mit der Antwort gesendet wird, nachdem der Antworttext vollständig übermittelt wurde. Da ein Nachspann nach dem Antworttext gesendet wird, kann er jederzeit zur Antwort hinzugefügt werden.

Der folgende Code legt einen Nachspann mithilfe von AppendTrailer fest:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", (HttpResponse response) =>
{
    // Write body
    response.WriteAsync("Hello world");

    if (response.SupportsTrailers())
    {
        response.AppendTrailer("trailername", "TrailerValue");
    }
});

app.Run();

RequestAborted

Mit dem Abbruchtoken HttpContext.RequestAborted können Sie mitteilen, dass die HTTP-Anforderung vom Client oder Server abgebrochen wurde. Das Abbruchtoken sollte an Aufgaben mit langer Ausführungsdauer übergeben werden, damit diese bei einer Stornierung der Anforderung abgebrochen werden können. Ein Beispiel ist der Abbruch einer Datenbankabfrage oder einer HTTP-Anforderung, um Daten für die Rückgabe in der Antwort zu erhalten.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var httpClient = new HttpClient();
app.MapPost("/books/{bookId}", async (int bookId, HttpContext context) =>
{
    var stream = await httpClient.GetStreamAsync(
        $"http://contoso/books/{bookId}.json", context.RequestAborted);

    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();

Das Abbruchtoken RequestAborted muss nicht für Lesevorgänge des Anforderungstexts verwendet werden, da Lesevorgänge stets sofort abgebrochen werden, wenn die Anforderung storniert wird. Das Token RequestAborted wird in der Regel auch beim Schreiben eines Antworttexts nicht benötigt, da Schreibvorgänge sofort abgebrochen werden, wenn die Anforderung storniert wird.

In einigen Fällen kann die Übergabe des Tokens RequestAborted an Schreibvorgänge ein bequemer Weg sein, um eine Schreibschleife mit einem OperationCanceledException vorzeitig zu beenden. In der Regel ist es jedoch besser, das Token RequestAborted an alle asynchronen Vorgänge zu übergeben, die für den Abruf des Antworttexts verantwortlich sind.

Hinweis

Minimal-APIs unterstützen die direkte Bindung von HttpContext.RequestAborted an einen CancellationToken-Parameter.

Abort()

Die Methode HttpContext.Abort() kann verwendet werden, um eine HTTP-Anforderung des Servers abzubrechen. Die Stornierung der HTTP-Anforderung löst sofort das Abbruchtoken HttpContext.RequestAborted aus, und der Client erhält eine Benachrichtigung, dass der Server die Anforderung abgebrochen hat.

Die Middleware übernimmt im folgenden Beispiel diese Aufgaben:

  • Sie fügt eine benutzerdefinierte Prüfung auf schädliche Anforderungen hinzu.
  • Sie bricht die HTTP-Anforderung ab, wenn es sich um eine schädliche Anforderung handelt.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    if (RequestAppearsMalicious(context.Request))
    {
        // Malicious requests don't even deserve an error response (e.g. 400).
        context.Abort();
        return;
    }

    await next.Invoke();
});

app.Run();

User

Mithilfe der Eigenschaft HttpContext.User wird der durch ClaimsPrincipal repräsentierte Benutzer für die Anforderung abgerufen oder festgelegt. Der ClaimsPrincipal wird normalerweise über die ASP.NET Core-Authentifizierung festgelegt.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/user/current", [Authorize] async (HttpContext context) =>
{
    var user = await GetUserAsync(context.User.Identity.Name);
    return Results.Ok(user);
});

app.Run();

Hinweis

Minimal-APIs unterstützen die direkte Bindung von HttpContext.User an einen ClaimsPrincipal-Parameter.

Features

Die Eigenschaft HttpContext.Features bietet Zugriff auf die Sammlung der Featureschnittstellen für die aktuelle Anforderung. Da die Featuresammlung auch im Kontext einer Anforderung änderbar ist, kann Middleware verwendet werden, um die Sammlung zu modifizieren und Unterstützung für zusätzliche Features hinzuzufügen. Einige erweiterte Features sind nur verfügbar, wenn Sie über die Featuresammlung auf die zugehörige Schnittstelle zugreifen.

Im Beispiel unten geschieht Folgendes:

  • Ruft IHttpMinRequestBodyDataRateFeature aus der Featuresammlung ab.
  • Legt MinDataRate auf Null fest. Dadurch wird die Mindestdatenrate für den Anforderungstext aufgehoben, die der Client für diese HTTP-Anforderung senden muss.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/long-running-stream", async (HttpContext context) =>
{
    var feature = context.Features.Get<IHttpMinRequestBodyDataRateFeature>();
    if (feature != null)
    {
        feature.MinDataRate = null;
    }

    // await and read long-running stream from request body.
    await Task.Yield();
});

app.Run();

Weitere Informationen zur Verwendung von Anforderungsfeatures und HttpContext finden Sie unter Anforderungsfeatures in ASP.NET Core.

HttpContext ist nicht threadsicher

In diesem Artikel wird hauptsächlich die Verwendung von HttpContext im Rahmen des Anforderungs- und Antwortflusses von Razor-Seiten, -Controllern, -Middleware usw. erläutert. Berücksichtigen Sie Folgendes, wenn Sie HttpContext außerhalb des Anforderungs- und Antwortflusses verwenden:

  • HttpContext ist NICHT threadsicher: der Zugriff darauf von mehreren Threads kann zu Ausnahmen, Datenbeschädigungen und im Allgemeinen unvorhersehbaren Ergebnissen führen.
  • Die IHttpContextAccessor-Schnittstelle sollte mit Vorsicht angewendet werden. Wie immer darf HttpContextnicht außerhalb des Anforderungsflusses erfasst werden. IHttpContextAccessor:
    • Basiert auf AsyncLocal<T>, was negative Leistungseinbußen auf asynchrone Aufrufe als Auswirkung haben könnte.
    • Erstellt eine Abhängigkeit vom „Umgebungszustand“, der das Testen erschweren kann.
  • IHttpContextAccessor.HttpContext kann null sein, wenn darauf außerhalb des Anforderungsflusses zugegriffen wird.
  • Um auf Informationen auf HttpContext von außerhalb des Anforderungsflusses zuzugreifen, kopieren Sie die Informationen innerhalb des Anforderungsflusses. Achten Sie darauf, die tatsächlichen Daten und nicht nur Verweise zu kopieren. Anstatt beispielsweise einen Verweis auf ein IHeaderDictionary zu kopieren, kopieren Sie die entsprechenden Kopfzeilenwerte oder kopieren Sie das gesamte Wörterbuch Schlüssel für Schlüssel, bevor Sie den Anforderungsfluss verlassen.
  • Erfassen Sie IHttpContextAccessor.HttpContext nicht in einem Konstruktor.

Im folgenden Beispiel werden GitHub-Verzweigungen protokolliert, wenn sie vom /branch-Endpunkt angefordert werden:

using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // The GitHub API requires two headers. The Use-Agent header is added
    // dynamically through UserAgentHeaderHandler
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();

builder.Services.AddTransient<UserAgentHeaderHandler>();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,
                         HttpContext context, Logger<Program> logger) =>
{
    var httpClient = httpClientFactory.CreateClient("GitHub");
    var httpResponseMessage = await httpClient.GetAsync(
        "repos/dotnet/AspNetCore.Docs/branches");

    if (!httpResponseMessage.IsSuccessStatusCode) 
        return Results.BadRequest();

    await using var contentStream =
        await httpResponseMessage.Content.ReadAsStreamAsync();

    var response = await JsonSerializer.DeserializeAsync
        <IEnumerable<GitHubBranch>>(contentStream);

    app.Logger.LogInformation($"/branches request: " +
                              $"{JsonSerializer.Serialize(response)}");

    return Results.Ok(response);
});

app.Run();

Die GitHub-API erfordert zwei Kopfzeilen. Die User-Agent-Kopfzeile wird dynamisch durch UserAgentHeaderHandler hinzugefügt:

using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // The GitHub API requires two headers. The Use-Agent header is added
    // dynamically through UserAgentHeaderHandler
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();

builder.Services.AddTransient<UserAgentHeaderHandler>();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,
                         HttpContext context, Logger<Program> logger) =>
{
    var httpClient = httpClientFactory.CreateClient("GitHub");
    var httpResponseMessage = await httpClient.GetAsync(
        "repos/dotnet/AspNetCore.Docs/branches");

    if (!httpResponseMessage.IsSuccessStatusCode) 
        return Results.BadRequest();

    await using var contentStream =
        await httpResponseMessage.Content.ReadAsStreamAsync();

    var response = await JsonSerializer.DeserializeAsync
        <IEnumerable<GitHubBranch>>(contentStream);

    app.Logger.LogInformation($"/branches request: " +
                              $"{JsonSerializer.Serialize(response)}");

    return Results.Ok(response);
});

app.Run();

Die UserAgentHeaderHandler:

using Microsoft.Net.Http.Headers;

namespace HttpContextInBackgroundThread;

public class UserAgentHeaderHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ILogger _logger;

    public UserAgentHeaderHandler(IHttpContextAccessor httpContextAccessor,
                                  ILogger<UserAgentHeaderHandler> logger)
    {
        _httpContextAccessor = httpContextAccessor;
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> 
                                    SendAsync(HttpRequestMessage request, 
                                    CancellationToken cancellationToken)
    {
        var contextRequest = _httpContextAccessor.HttpContext?.Request;
        string? userAgentString = contextRequest?.Headers["user-agent"].ToString();
        
        if (string.IsNullOrEmpty(userAgentString))
        {
            userAgentString = "Unknown";
        }

        request.Headers.Add(HeaderNames.UserAgent, userAgentString);
        _logger.LogInformation($"User-Agent: {userAgentString}");

        return await base.SendAsync(request, cancellationToken);
    }
}

Wenn HttpContext im vorherigen Code null ist, wird die Zeichenfolge userAgent auf "Unknown" festgelegt. Wenn möglich, sollte HttpContext explizit an den Dienst übergeben werden. Explizite Übergabe von Daten in HttpContext:

  • Das macht die Dienst-API außerhalb des Anforderungsflusses benutzerfreundlicher.
  • Das ist besser für die Leistung.
  • Das macht den Code leichter verständlich und nachvollziehbar, als wenn man sich auf den Umgebungszustand verlässt.

Wenn der Dienst auf HttpContext zugreifen muss, sollte er die Möglichkeit berücksichtigen, dass HttpContextnull sein kann, wenn die Anforderung nicht von einem Anforderungsthread aufgerufen wird.

Die Anwendung umfasst auch PeriodicBranchesLoggerService, das alle 30 Sekunden die offenen GitHub-Zweige des angegebenen Repositorys protokolliert:

using System.Text.Json;

namespace HttpContextInBackgroundThread;

public class PeriodicBranchesLoggerService : BackgroundService
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger _logger;
    private readonly PeriodicTimer _timer;

    public PeriodicBranchesLoggerService(IHttpClientFactory httpClientFactory,
                                         ILogger<PeriodicBranchesLoggerService> logger)
    {
        _httpClientFactory = httpClientFactory;
        _logger = logger;
        _timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (await _timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                // Cancel sending the request to sync branches if it takes too long
                // rather than miss sending the next request scheduled 30 seconds from now.
                // Having a single loop prevents this service from sending an unbounded
                // number of requests simultaneously.
                using var syncTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
                syncTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
                
                var httpClient = _httpClientFactory.CreateClient("GitHub");
                var httpResponseMessage = await httpClient.GetAsync("repos/dotnet/AspNetCore.Docs/branches",
                                                                    stoppingToken);

                if (httpResponseMessage.IsSuccessStatusCode)
                {
                    await using var contentStream =
                        await httpResponseMessage.Content.ReadAsStreamAsync(stoppingToken);

                    // Sync the response with preferred datastore.
                    var response = await JsonSerializer.DeserializeAsync<
                        IEnumerable<GitHubBranch>>(contentStream, cancellationToken: stoppingToken);

                    _logger.LogInformation(
                        $"Branch sync successful! Response: {JsonSerializer.Serialize(response)}");
                }
                else
                {
                    _logger.LogError(1, $"Branch sync failed! HTTP status code: {httpResponseMessage.StatusCode}");
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(1, ex, "Branch sync failed!");
            }
        }
    }

    public override Task StopAsync(CancellationToken stoppingToken)
    {
        // This will cause any active call to WaitForNextTickAsync() to return false immediately.
        _timer.Dispose();
        // This will cancel the stoppingToken and await ExecuteAsync(stoppingToken).
        return base.StopAsync(stoppingToken);
    }
}

PeriodicBranchesLoggerService ist ein gehosteter Dienst, der außerhalb des Anforderungs- und Antwortflusses ausgeführt wird. Die Protokollierung vom PeriodicBranchesLoggerService hat ein Null-HttpContext. PeriodicBranchesLoggerService wurde so geschrieben, um nicht von HttpContext abzuhängen.

using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();

builder.Services.AddHttpClient("GitHub", httpClient =>
{