Udostępnij za pośrednictwem


Używanie obiektu HttpContext w ASP.NET Core

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Ostrzeżenie

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

HttpContext Hermetyzuje wszystkie informacje dotyczące pojedynczego żądania HTTP i odpowiedzi. Wystąpienie HttpContext jest inicjowane po odebraniu żądania HTTP. Wystąpienie HttpContext jest dostępne przez oprogramowanie pośredniczące i struktury aplikacji, takie jak kontrolery interfejsu API sieci Web, Razor strony, SignalR, gRPC i inne.

Aby uzyskać więcej informacji na temat uzyskiwania dostępu do obiektu , zobacz Access HttpContext in ASP.NET Core (Uzyskiwanie dostępu do obiektu HttpContextHttpContext w programie ASP.NET Core).

HttpRequest

HttpContext.Request zapewnia dostęp do HttpRequestelementu . HttpRequest zawiera informacje o przychodzącym żądaniu HTTP i jest inicjowany po odebraniu żądania HTTP przez serwer. HttpRequest program nie jest tylko do odczytu, a oprogramowanie pośredniczące może zmieniać wartości żądań w potoku oprogramowania pośredniczącego.

Często używane elementy członkowskie HttpRequest obejmują:

Właściwości opis Przykład
HttpRequest.Path Ścieżka żądania. /en/article/getstarted
HttpRequest.Method Metoda żądania. GET
HttpRequest.Headers Kolekcja nagłówków żądań. user-agent=Edge
x-custom-header=MyValue
HttpRequest.RouteValues Kolekcja wartości tras. Kolekcja jest ustawiana po dopasowaniu żądania do trasy. language=en
article=getstarted
HttpRequest.Query Kolekcja wartości zapytania przeanalizowanych z elementu QueryString. filter=hello
page=1
HttpRequest.ReadFormAsync() Metoda, która odczytuje treść żądania jako formularz i zwraca kolekcję wartości formularza. Aby uzyskać informacje o tym, dlaczego ReadFormAsync należy używać do uzyskiwania dostępu do danych formularza, zobacz Prefer ReadFormAsync over Request.Form (Preferuj funkcję ReadFormAsync za pośrednictwem pliku Request.Form). email=user@contoso.com
HttpRequest.Body Element Stream do odczytywania treści żądania. Ładunek JSON UTF-8

Pobieranie nagłówków żądań

HttpRequest.Headers zapewnia dostęp do nagłówków żądań wysyłanych za pomocą żądania HTTP. Istnieją dwa sposoby uzyskiwania dostępu do nagłówków przy użyciu tej kolekcji:

  • Podaj nazwę nagłówka indeksatorowi w kolekcji nagłówków. Nazwa nagłówka nie uwzględnia wielkości liter. Indeksator może uzyskać dostęp do dowolnej wartości nagłówka.
  • Kolekcja nagłówków zawiera również właściwości pobierania i ustawiania często używanych nagłówków HTTP. Właściwości zapewniają szybki, oparty na funkcji IntelliSense sposób uzyskiwania dostępu do nagłówków.
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();

Aby uzyskać informacje na temat wydajnej obsługi nagłówków, które pojawiają się więcej niż raz, zobacz Krótkie spojrzenie na StringValues.

Treść żądania odczytu

Żądanie HTTP może zawierać treść żądania. Treść żądania to dane skojarzone z żądaniem, takie jak zawartość formularza HTML, ładunek JSON UTF-8 lub plik.

HttpRequest.Body umożliwia odczytanie treści żądania za pomocą polecenia 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 można odczytywać bezpośrednio lub używać z innymi interfejsami API, które akceptują strumień.

Uwaga

Minimalne interfejsy API obsługują wiązanie HttpRequest.Body bezpośrednio z parametrem Stream .

Włączanie buforowania treści żądania

Treść żądania może być odczytywana tylko raz, od początku do końca. Odczyt treści żądania tylko do przodu pozwala uniknąć obciążeń związanych z buforowaniem całej treści żądania i zmniejsza użycie pamięci. Jednak w niektórych scenariuszach istnieje potrzeba wielokrotnego odczytania treści żądania. Na przykład oprogramowanie pośredniczące może wymagać odczytania treści żądania, a następnie przewijenie go, aby było dostępne dla punktu końcowego.

EnableBuffering Metoda rozszerzenia umożliwia buforowanie treści żądania HTTP i jest zalecanym sposobem włączenia wielu operacji odczytu. Ponieważ żądanie może mieć dowolny rozmiar, EnableBuffering obsługuje opcje buforowania dużych jednostek żądań na dysku lub ich całkowite odrzucenie.

Oprogramowanie pośredniczące w poniższym przykładzie:

  • Włącza wiele odczytów za pomocą polecenia EnableBuffering. Przed odczytaniem treści żądania należy go wywołać.
  • Odczytuje treść żądania.
  • Przewija treść żądania do początku, aby inne oprogramowanie pośredniczące lub punkt końcowy mógł go odczytać.
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

Alternatywnym sposobem odczytania treści żądania jest użycie HttpRequest.BodyReader właściwości . Właściwość BodyReader uwidacznia treść żądania jako PipeReader. Ten interfejs API pochodzi z potoków we/wy, czyli zaawansowanego, wysokiej wydajności sposobu odczytywania treści żądania.

Czytelnik uzyskuje bezpośredni dostęp do treści żądania i zarządza pamięcią w imieniu wywołującego. W przeciwieństwie do HttpRequest.Bodyelementu czytnik nie kopiuje danych żądania do buforu. Jednak czytelnik jest bardziej skomplikowany do użycia niż strumień i powinien być używany z ostrożnością.

Aby uzyskać informacje na temat odczytywania zawartości z BodyReaderusługi , zobacz Potoki we/wy PipeReader.

HttpResponse

HttpContext.Response zapewnia dostęp do HttpResponseelementu . HttpResponse służy do ustawiania informacji na temat odpowiedzi HTTP wysyłanej z powrotem do klienta.

Często używane elementy członkowskie HttpResponse obejmują:

Właściwości opis Przykład
HttpResponse.StatusCode Kod odpowiedzi. Należy ustawić przed zapisaniem w treści odpowiedzi. 200
HttpResponse.ContentType Nagłówek odpowiedzi content-type . Należy ustawić przed zapisaniem w treści odpowiedzi. application/json
HttpResponse.Headers Kolekcja nagłówków odpowiedzi. Należy ustawić przed zapisaniem w treści odpowiedzi. server=Kestrel
x-custom-header=MyValue
HttpResponse.Body A Stream do pisania treści odpowiedzi. Wygenerowana strona internetowa

Ustawianie nagłówków odpowiedzi

HttpResponse.Headers zapewnia dostęp do nagłówków odpowiedzi wysyłanych za pomocą odpowiedzi HTTP. Istnieją dwa sposoby uzyskiwania dostępu do nagłówków przy użyciu tej kolekcji:

  • Podaj nazwę nagłówka indeksatorowi w kolekcji nagłówków. Nazwa nagłówka nie uwzględnia wielkości liter. Indeksator może uzyskać dostęp do dowolnej wartości nagłówka.
  • Kolekcja nagłówków zawiera również właściwości pobierania i ustawiania często używanych nagłówków HTTP. Właściwości zapewniają szybki, oparty na funkcji IntelliSense sposób uzyskiwania dostępu do nagłówków.
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();

Aplikacja nie może modyfikować nagłówków po uruchomieniu odpowiedzi. Po uruchomieniu odpowiedzi nagłówki są wysyłane do klienta. Odpowiedź jest uruchamiana przez opróżnienie treści odpowiedzi lub wywołanie metody HttpResponse.StartAsync(CancellationToken). Właściwość HttpResponse.HasStarted wskazuje, czy odpowiedź została uruchomiona. Podczas próby zmodyfikowania nagłówków po rozpoczęciu odpowiedzi jest zgłaszany błąd:

System.InvalidOperationException: Nagłówki są tylko do odczytu, odpowiedź została już uruchomiona.

Uwaga

Jeśli buforowanie odpowiedzi nie jest włączone, wszystkie operacje zapisu (na przykład WriteAsync) opróżnić treść odpowiedzi wewnętrznie i oznaczyć odpowiedź jako uruchomioną. Buforowanie odpowiedzi jest domyślnie wyłączone.

Treść odpowiedzi zapisu

Odpowiedź HTTP może zawierać treść odpowiedzi. Treść odpowiedzi to dane skojarzone z odpowiedzią, takie jak wygenerowana zawartość strony internetowej, ładunek JSON UTF-8 lub plik.

HttpResponse.Body umożliwia zapisanie treści odpowiedzi za pomocą polecenia 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 można zapisywać bezpośrednio lub używać z innymi interfejsami API, które zapisują w strumieniu.

BodyWriter

Alternatywnym sposobem na napisanie treści odpowiedzi jest użycie HttpResponse.BodyWriter właściwości . Właściwość BodyWriter uwidacznia treść odpowiedzi jako PipeWriter. Ten interfejs API pochodzi z potoków we/wy i jest to zaawansowany, wysokowydajny sposób na napisanie odpowiedzi.

Składnik zapisywania zapewnia bezpośredni dostęp do treści odpowiedzi i zarządza pamięcią w imieniu wywołującego. W przeciwieństwie do HttpResponse.Bodyelementu zapis nie kopiuje danych żądania do buforu. Jednak składnik zapisywania jest bardziej skomplikowany do użycia niż strumień, a kod modułu zapisywania powinien być dokładnie przetestowany.

Aby uzyskać informacje na temat pisania zawartości w usłudze BodyWriter, zobacz Potoki we/wy PipeWriter.

Ustawianie zwiastunów odpowiedzi

Zwiastuny odpowiedzi http/2 i HTTP/3. Przyczepy są nagłówkami wysyłanymi z odpowiedzią po zakończeniu treści odpowiedzi. Ponieważ przyczepy są wysyłane po treści odpowiedzi, przyczepy można dodawać do odpowiedzi w dowolnym momencie.

Poniższy kod ustawia przyczepy przy użyciu polecenia AppendTrailer:

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

Token HttpContext.RequestAborted anulowania może służyć do powiadamiania o tym, że żądanie HTTP zostało przerwane przez klienta lub serwer. Token anulowania powinien zostać przekazany do długotrwałych zadań, aby można je było anulować, jeśli żądanie zostało przerwane. Na przykład przerwanie zapytania bazy danych lub żądania HTTP w celu pobrania danych do zwrócenia w odpowiedzi.

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();

Token RequestAborted anulowania nie musi być używany dla operacji odczytu treści żądania, ponieważ operacje odczytu treści żądania są zawsze zgłaszane natychmiast po przerwaniu żądania. Token RequestAborted jest również zwykle niepotrzebny podczas pisania treści odpowiedzi, ponieważ zapisuje natychmiast bez operacji po przerwaniu żądania.

W niektórych przypadkach przekazanie tokenu RequestAborted do operacji zapisu może być wygodnym sposobem wymuszenia zamknięcia pętli zapisu na wczesnym etapie za pomocą polecenia OperationCanceledException. Jednak zazwyczaj lepiej jest przekazać RequestAborted token do dowolnych operacji asynchronicznych odpowiedzialnych za pobieranie zawartości treści odpowiedzi.

Uwaga

Minimalne interfejsy API obsługują wiązanie HttpContext.RequestAborted bezpośrednio z parametrem CancellationToken .

Abort()

Metody HttpContext.Abort() można użyć do przerwania żądania HTTP z serwera. Przerwanie żądania HTTP natychmiast wyzwala HttpContext.RequestAborted token anulowania i wysyła powiadomienie do klienta, że serwer przerwał żądanie.

Oprogramowanie pośredniczące w poniższym przykładzie:

  • Dodaje niestandardowe sprawdzanie pod kątem złośliwych żądań.
  • Przerywa żądanie HTTP, jeśli żądanie jest złośliwe.
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

Właściwość służy do pobierania HttpContext.User lub ustawiania użytkownika reprezentowanego przez ClaimsPrincipalelement dla żądania . Element ClaimsPrincipal jest zwykle ustawiany przez uwierzytelnianie ASP.NET Core.

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();

Uwaga

Minimalne interfejsy API obsługują wiązanie HttpContext.User bezpośrednio z parametrem ClaimsPrincipal .

Features

Właściwość HttpContext.Features zapewnia dostęp do kolekcji interfejsów funkcji dla bieżącego żądania. Ponieważ kolekcja funkcji jest modyfikowalna nawet w kontekście żądania, oprogramowanie pośredniczące może służyć do modyfikowania kolekcji i dodawania obsługi dodatkowych funkcji. Niektóre zaawansowane funkcje są dostępne tylko przez uzyskanie dostępu do skojarzonego interfejsu za pośrednictwem kolekcji funkcji.

Poniższy przykład:

  • Pobiera IHttpMinRequestBodyDataRateFeature z kolekcji funkcji.
  • Ustawia MinDataRate wartość null. Spowoduje to usunięcie minimalnej szybkości danych, którą musi wysłać treść żądania przez klienta dla tego żądania HTTP.
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();

Aby uzyskać więcej informacji na temat korzystania z funkcji żądań i HttpContext, zobacz Request Features in ASP.NET Core (Funkcje żądań w programie ASP.NET Core).

HttpContext nie jest bezpieczny wątkiem

W tym artykule omówiono przede wszystkim użycie HttpContext przepływu żądań i odpowiedzi ze Razor stron, kontrolerów, oprogramowania pośredniczącego itp. Podczas korzystania z HttpContext przepływu żądania i odpowiedzi należy wziąć pod uwagę następujące kwestie:

  • Element HttpContext NIE jest bezpieczny wątkowo, dostęp do niego z wielu wątków może spowodować wyjątki, uszkodzenie danych i ogólnie nieprzewidywalne wyniki.
  • Interfejs IHttpContextAccessor powinien być używany z ostrożnością. Jak zawsze nie HttpContext można przechwycić elementu poza przepływem żądania. IHttpContextAccessor:
    • Zależy od AsyncLocal<T> tego, co może mieć negatywny wpływ na wydajność wywołań asynchronicznych.
    • Tworzy zależność od "stanu otoczenia", co może utrudnić testowanie.
  • IHttpContextAccessor.HttpContext może być null używany poza przepływem żądań.
  • Aby uzyskać dostęp do informacji HttpContext spoza przepływu żądania, skopiuj informacje wewnątrz przepływu żądania. Zachowaj ostrożność, aby skopiować rzeczywiste dane, a nie tylko odwołania. Na przykład zamiast kopiować odwołanie do IHeaderDictionaryelementu , skopiuj odpowiednie wartości nagłówka lub skopiuj cały klucz słownika według klucza przed opuszczeniem przepływu żądania.
  • Nie przechwytuj IHttpContextAccessor.HttpContext w konstruktorze.

Następujące przykładowe gałęzie usługi GitHub są dzienniki po żądaniu z punktu końcowego /branch :

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();

Interfejs API usługi GitHub wymaga dwóch nagłówków. Nagłówek User-Agent jest dodawany dynamicznie przez element UserAgentHeaderHandler:

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();

Pomocnik 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);
    }
}

W poprzednim kodzie, gdy HttpContext parametr to null, userAgent ciąg jest ustawiony na "Unknown"wartość . Jeśli to możliwe, HttpContext należy jawnie przekazać do usługi. Jawne przekazywanie HttpContext danych:

  • Sprawia, że interfejs API usługi jest bardziej możliwy do użycia poza przepływem żądań.
  • Jest lepszy pod kątem wydajności.
  • Ułatwia zrozumienie i uzasadnienie kodu niż poleganie na stanie otoczenia.

Gdy usługa musi uzyskać dostęp, HttpContextpowinna uwzględniać możliwość wywołania HttpContext null z wątku żądania.

Aplikacja zawiera PeriodicBranchesLoggerServicerównież element , który rejestruje otwarte gałęzie usługi GitHub określonego repozytorium co 30 sekund:

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);
    }
}

PeriodicBranchesLoggerServicejest usługą hostowaną, która działa poza przepływem żądania i odpowiedzi. Rejestrowanie z obiektu PeriodicBranchesLoggerService ma wartość null HttpContext. Tekst PeriodicBranchesLoggerService został zapisany, aby nie zależeć od .HttpContext

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 =>
{