Condividi tramite


Usare HttpContext in ASP.NET Core

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere Criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 8 di questo articolo.

HttpContext incapsula tutte le informazioni su una singola richiesta e risposta HTTP. Un'istanza HttpContext viene inizializzata quando viene ricevuta una richiesta HTTP. L'istanza HttpContext è accessibile dal middleware e dai framework dell'app, ad esempio controller API Web, Razor Pagine, SignalR, gRPC e altro ancora.

Per altre informazioni sull'accesso a HttpContext, vedere Accedere a HttpContext in ASP.NET Core.

HttpRequest

HttpContext.Request fornisce l'accesso a HttpRequest. HttpRequest contiene informazioni sulla richiesta HTTP in ingresso e viene inizializzata quando viene ricevuta una richiesta HTTP dal server. HttpRequest non è di sola lettura e il middleware può modificare i valori delle richieste nella pipeline middleware.

I membri comunemente usati in HttpRequest includono:

Proprietà Descrizione Esempio
HttpRequest.Path Percorso della richiesta. /en/article/getstarted
HttpRequest.Method Metodo di richiesta. GET
HttpRequest.Headers Raccolta di intestazioni di richiesta. user-agent=Edge
x-custom-header=MyValue
HttpRequest.RouteValues Raccolta di valori di route. La raccolta viene impostata quando la richiesta viene abbinata a una route. language=en
article=getstarted
HttpRequest.Query Raccolta di valori di query analizzati da QueryString. filter=hello
page=1
HttpRequest.ReadFormAsync() Metodo che legge il corpo della richiesta come maschera e restituisce un insieme di valori del modulo. Per informazioni sul motivo per cui ReadFormAsync usare per accedere ai dati del modulo, vedere Prefer ReadFormAsync over Request.Form. email=user@contoso.com
HttpRequest.Body Oggetto Stream per la lettura del corpo della richiesta. Payload JSON UTF-8

Ottenere le intestazioni della richiesta

HttpRequest.Headers fornisce l'accesso alle intestazioni della richiesta inviate con la richiesta HTTP. Esistono due modi per accedere alle intestazioni usando questa raccolta:

  • Specificare il nome dell'intestazione all'indicizzatore nella raccolta di intestazioni. Il nome dell'intestazione non fa distinzione tra maiuscole e minuscole. L'indicizzatore può accedere a qualsiasi valore di intestazione.
  • La raccolta di intestazioni include anche proprietà per ottenere e impostare intestazioni HTTP di uso comune. Le proprietà offrono un modo rapido basato su IntelliSense per accedere alle intestazioni.
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();

Per informazioni sulla gestione efficiente delle intestazioni che appaiono più volte, vedere Una breve panoramica di StringValues.

Leggere il corpo della richiesta

Una richiesta HTTP può includere un corpo della richiesta. Il corpo della richiesta è costituito da dati associati alla richiesta, ad esempio il contenuto di un modulo HTML, un payload JSON UTF-8 o un file.

HttpRequest.Body consente di leggere il corpo della richiesta con 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 può essere letto direttamente o usato con altre API che accettano il flusso.

Nota

Le API minime supportano l'associazione HttpRequest.Body diretta a un Stream parametro.

Abilitare il buffer del corpo della richiesta

Il corpo della richiesta può essere letto una sola volta, dall'inizio alla fine. La lettura forward-only del corpo della richiesta evita il sovraccarico del buffering dell'intero corpo della richiesta e riduce l'utilizzo della memoria. In alcuni scenari, tuttavia, è necessario leggere più volte il corpo della richiesta. Ad esempio, il middleware potrebbe dover leggere il corpo della richiesta e quindi riavvolgerlo in modo che sia disponibile per l'endpoint.

Il EnableBuffering metodo di estensione abilita il buffer del corpo della richiesta HTTP ed è il modo consigliato per abilitare più letture. Poiché una richiesta può essere di qualsiasi dimensione, EnableBuffering supporta le opzioni per memorizzare nel buffer i corpi delle richieste di grandi dimensioni su disco o rifiutarli completamente.

Middleware nell'esempio seguente:

  • Abilita più letture con EnableBuffering. Deve essere chiamato prima di leggere il corpo della richiesta.
  • Legge il corpo della richiesta.
  • Riavvolge il corpo della richiesta all'avvio in modo che altri middleware o l'endpoint possano leggerlo.
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

Un modo alternativo per leggere il corpo della richiesta consiste nell'usare la HttpRequest.BodyReader proprietà . La BodyReader proprietà espone il corpo della richiesta come .PipeReader Questa API proviene dalle pipeline di I/O, un modo avanzato e ad alte prestazioni per leggere il corpo della richiesta.

Il lettore accede direttamente al corpo della richiesta e gestisce la memoria per conto del chiamante. A differenza di HttpRequest.Body, il lettore non copia i dati della richiesta in un buffer. Tuttavia, un lettore è più complicato da usare rispetto a un flusso e deve essere usato con cautela.

Per informazioni su come leggere il contenuto da BodyReader, vedere Pipeline di I/O PipeReader.

HttpResponse

HttpContext.Response fornisce l'accesso a HttpResponse. HttpResponse viene usato per impostare le informazioni sulla risposta HTTP inviata al client.

I membri comunemente usati in HttpResponse includono:

Proprietà Descrizione Esempio
HttpResponse.StatusCode Codice di risposta. Deve essere impostato prima di scrivere nel corpo della risposta. 200
HttpResponse.ContentType Intestazione della risposta content-type . Deve essere impostato prima di scrivere nel corpo della risposta. application/json
HttpResponse.Headers Raccolta di intestazioni di risposta. Deve essere impostato prima di scrivere nel corpo della risposta. server=Kestrel
x-custom-header=MyValue
HttpResponse.Body Oggetto Stream per la scrittura del corpo della risposta. Pagina Web generata

Impostare le intestazioni di risposta

HttpResponse.Headers fornisce l'accesso alle intestazioni di risposta inviate con la risposta HTTP. Esistono due modi per accedere alle intestazioni usando questa raccolta:

  • Specificare il nome dell'intestazione all'indicizzatore nella raccolta di intestazioni. Il nome dell'intestazione non fa distinzione tra maiuscole e minuscole. L'indicizzatore può accedere a qualsiasi valore di intestazione.
  • La raccolta di intestazioni include anche proprietà per ottenere e impostare intestazioni HTTP di uso comune. Le proprietà offrono un modo rapido basato su IntelliSense per accedere alle intestazioni.
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();

Un'app non può modificare le intestazioni dopo l'avvio della risposta. Una volta avviata la risposta, le intestazioni vengono inviate al client. Una risposta viene avviata scaricando il corpo della risposta o chiamando HttpResponse.StartAsync(CancellationToken). La HttpResponse.HasStarted proprietà indica se la risposta è stata avviata. Quando si tenta di modificare le intestazioni dopo l'avvio della risposta, viene generato un errore:

System.InvalidOperationException: le intestazioni sono di sola lettura, la risposta è già stata avviata.

Nota

A meno che il buffer delle risposte non sia abilitato, tutte le operazioni di scrittura ,ad esempio , WriteAsyncscaricano internamente il corpo della risposta e contrassegnano la risposta come avviata. Il buffer delle risposte è disabilitato per impostazione predefinita.

Scrivere il corpo della risposta

Una risposta HTTP può includere un corpo della risposta. Il corpo della risposta è costituito da dati associati alla risposta, ad esempio il contenuto della pagina Web generata, il payload JSON UTF-8 o un file.

HttpResponse.Body consente di scrivere il corpo della risposta con 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 può essere scritto direttamente o usato con altre API che scrivono in un flusso.

BodyWriter

Un modo alternativo per scrivere il corpo della risposta consiste nell'usare la HttpResponse.BodyWriter proprietà . La BodyWriter proprietà espone il corpo della risposta come .PipeWriter Questa API proviene dalle pipeline di I/O ed è un modo avanzato e ad alte prestazioni per scrivere la risposta.

Il writer fornisce accesso diretto al corpo della risposta e gestisce la memoria per conto del chiamante. A differenza di HttpResponse.Body, la scrittura non copia i dati della richiesta in un buffer. Tuttavia, un writer è più complicato da usare rispetto a un flusso e codice writer deve essere testato accuratamente.

Per informazioni su come scrivere contenuto in BodyWriter, vedere Pipeline di I/O PipeWriter.

Impostare trailer di risposta

HTTP/2 e HTTP/3 supportano trailer di risposta. I trailer sono intestazioni inviate con la risposta dopo il completamento del corpo della risposta. Poiché i trailer vengono inviati dopo il corpo della risposta, i trailer possono essere aggiunti alla risposta in qualsiasi momento.

Il codice seguente imposta i trailer usando 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

Il HttpContext.RequestAborted token di annullamento può essere usato per notificare che la richiesta HTTP è stata interrotta dal client o dal server. Il token di annullamento deve essere passato alle attività a esecuzione prolungata in modo che possano essere annullate se la richiesta viene interrotta. Ad esempio, l'interruzione di una query di database o di una richiesta HTTP per ottenere i dati da restituire nella risposta.

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

Non è necessario usare il RequestAborted token di annullamento per le operazioni di lettura del corpo della richiesta perché le letture generano sempre immediatamente quando la richiesta viene interrotta. Il RequestAborted token in genere non è necessario quando si scrivono corpi di risposta, perché le scritture immediatamente non operative quando la richiesta viene interrotta.

In alcuni casi, il passaggio del RequestAborted token alle operazioni di scrittura può essere un modo pratico per forzare l'uscita anticipata di un ciclo di scrittura con un oggetto OperationCanceledException. Tuttavia, in genere è preferibile passare il RequestAborted token in qualsiasi operazione asincrona responsabile del recupero del contenuto del corpo della risposta.

Nota

Le API minime supportano l'associazione HttpContext.RequestAborted diretta a un CancellationToken parametro.

Abort()

Il HttpContext.Abort() metodo può essere usato per interrompere una richiesta HTTP dal server. L'interruzione della richiesta HTTP attiva immediatamente il HttpContext.RequestAborted token di annullamento e invia una notifica al client che il server ha interrotto la richiesta.

Middleware nell'esempio seguente:

  • Aggiunge un controllo personalizzato per le richieste dannose.
  • Interrompe la richiesta HTTP se la richiesta è dannosa.
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

La HttpContext.User proprietà viene utilizzata per ottenere o impostare l'utente, rappresentato da ClaimsPrincipal, per la richiesta. Viene ClaimsPrincipal in genere impostato da ASP.NET'autenticazione 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();

Nota

Le API minime supportano l'associazione HttpContext.User diretta a un ClaimsPrincipal parametro.

Features

La HttpContext.Features proprietà fornisce l'accesso alla raccolta di interfacce di funzionalità per la richiesta corrente. Poiché l'insieme di funzionalità è modificabile anche all'interno del contesto di una richiesta, è possibile usare il middleware per modificare la raccolta e aggiungere il supporto di altre funzionalità. Alcune funzionalità avanzate sono disponibili solo accedendo all'interfaccia associata tramite la raccolta di funzionalità.

L'esempio seguente:

  • Ottiene IHttpMinRequestBodyDataRateFeature dalla raccolta di funzionalità.
  • Imposta MinDataRate su Null. In questo modo viene rimossa la frequenza di dati minima che il corpo della richiesta deve essere inviato dal client per questa richiesta 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();

Per altre informazioni sull'uso delle funzionalità di richiesta e HttpContext, vedere Funzionalità delle richieste in ASP.NET Core.

HttpContext non è thread-safe

Questo articolo illustra principalmente l'uso HttpContext del flusso di richiesta e risposta da Razor Pagine, controller, middleware e così via. Quando si usa HttpContext all'esterno del flusso di richiesta e risposta, tenere presente quanto segue:

  • non HttpContext è thread-safe, l'accesso da più thread può comportare eccezioni, danneggiamento dei dati e risultati generalmente imprevedibili.
  • L'interfaccia IHttpContextAccessor deve essere usata con cautela. Come sempre, non HttpContext deve essere acquisito all'esterno del flusso della richiesta. IHttpContextAccessor:
    • Si basa su AsyncLocal<T> cui può avere un impatto negativo sulle prestazioni sulle chiamate asincrone.
    • Crea una dipendenza dallo "stato di ambiente" che può rendere più difficile il test.
  • IHttpContextAccessor.HttpContext può essere null se si accede all'esterno del flusso della richiesta.
  • Per accedere alle informazioni dall'esterno HttpContext del flusso di richiesta, copiare le informazioni all'interno del flusso di richiesta. Prestare attenzione a copiare i dati effettivi e non solo i riferimenti. Ad esempio, anziché copiare un riferimento a un IHeaderDictionaryoggetto , copiare i valori di intestazione pertinenti o copiare l'intera chiave del dizionario per chiave prima di uscire dal flusso della richiesta.
  • Non acquisire IHttpContextAccessor.HttpContext in un costruttore.

L'esempio seguente registra i rami di GitHub quando richiesto dall'endpoint /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();

L'API GitHub richiede due intestazioni. L'intestazione User-Agent viene aggiunta dinamicamente da 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();

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

Nel codice precedente, quando è HttpContext null, la userAgent stringa è impostata su "Unknown". Se possibile, HttpContext deve essere passato in modo esplicito al servizio. Passaggio esplicito dei HttpContext dati:

  • Rende l'API del servizio più utilizzabile all'esterno del flusso di richiesta.
  • È migliore per le prestazioni.
  • Semplifica la comprensione e il motivo del codice rispetto allo stato di ambiente.

Quando il servizio deve accedere HttpContexta , deve tenere conto della possibilità di essere null quando non viene chiamato da un thread di HttpContext richiesta.

L'applicazione include PeriodicBranchesLoggerServiceanche , che registra i rami GitHub aperti del repository specificato ogni 30 secondi:

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 è un servizio ospitato, che viene eseguito all'esterno del flusso di richiesta e risposta. La registrazione da ha PeriodicBranchesLoggerService un valore Null HttpContext. L'oggetto PeriodicBranchesLoggerService è stato scritto per non dipendere da 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 =>
{