Utiliser HttpContext dans ASP.NET Core
Remarque
Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.
Avertissement
Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la Stratégie de prise en charge de .NET et .NET Core. Pour la version actuelle, consultez la version .NET 8 de cet article.
Important
Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.
Pour la version actuelle, consultez la version .NET 8 de cet article.
HttpContext encapsule toutes les informations sur une requête et une réponse HTTP individuelles. Une instance HttpContext
est initialisée lorsqu’une requête HTTP est reçue. L’instance HttpContext
est accessible par les intergiciels et frameworks d’application comme les contrôleurs d’API web, Razor Pages, SignalR, gRPC, etc.
Pour plus d’informations sur l’accès au HttpContext
, consultez Accéder à HttpContext dans ASP.NET Core.
HttpRequest
HttpContext.Request fournit l’accès à HttpRequest. HttpRequest
contient des informations sur la requête HTTP entrante et est initialisée lorsqu’une requête HTTP est reçue par le serveur. HttpRequest
n’est pas en lecture seule, et les intergiciels peuvent modifier les valeurs de requête dans le pipeline d’intergiciels.
Les membres couramment utilisés sur HttpRequest
incluent :
Propriété | Description | Exemple |
---|---|---|
HttpRequest.Path | chemin d'accès de la requête. | /en/article/getstarted |
HttpRequest.Method | Méthode de demande. | GET |
HttpRequest.Headers | Collection d'en-têtes de requêtes. | user-agent=Edge x-custom-header=MyValue |
HttpRequest.RouteValues | Une collection de valeurs d’itinéraire. La collection est définie lorsque la requête est mise en correspondance avec un itinéraire. | language=en article=getstarted |
HttpRequest.Query | Collection de valeurs de requête analysées à partir de QueryString. | filter=hello page=1 |
HttpRequest.ReadFormAsync() | Méthode qui lit le corps de la requête en tant que formulaire et retourne une collection de valeurs de formulaire. Pour plus d’informations sur la raison pour laquelle ReadFormAsync doit être utilisé pour accéder aux données de formulaire, consultez Préférer ReadFormAsync à Request.Form. |
email=user@contoso.com |
HttpRequest.Body | Un Stream pour lire le corps de la requête. | Charge utile JSON UTF-8 |
Obtenir les en-têtes de requête
HttpRequest.Headers fournit l’accès aux en-têtes de requête envoyés avec la requête HTTP. Il existe deux façons d’accéder aux en-têtes à l’aide de cette collection :
- Indiquez le nom de l’en-tête à l’indexeur sur la collection d’en-têtes. Le nom de l’en-tête ne respecte pas la casse. L’indexeur peut accéder à n’importe quelle valeur d’en-tête.
- La collection d’en-têtes possède également des propriétés permettant d’obtenir et de définir des en-têtes HTTP couramment utilisés. Les propriétés offrent un moyen rapide et piloté par IntelliSense d’accéder aux en-têtes.
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();
Pour plus d’informations sur une gestion efficace des en-têtes qui s’affichent plusieurs fois, consultez Un bref aperçu de StringValues.
Corps de la requête de lecture
Une requête HTTP peut inclure un corps de requête. Le corps de la requête se compose de données associées à la requête, comme le contenu d’un formulaire HTML, la charge utile JSON UTF-8 ou un fichier.
HttpRequest.Body permet de lire le corps de la requête avec 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
peut être lu directement ou utilisé avec d’autres API qui acceptent le flux.
Notes
Les API minimales prennent en charge la liaison de HttpRequest.Body directement à un paramètre Stream.
Activer la mise en mémoire tampon du corps de la requête
Le corps de la requête ne peut être lu qu’une seule fois, du début à la fin. La lecture avant uniquement du corps de la requête évite la surcharge liée à la mise en mémoire tampon de l’ensemble du corps de la requête et réduit l’utilisation de la mémoire. Toutefois, dans certains scénarios, il est nécessaire de lire le corps de la requête plusieurs fois. Par exemple, l’intergiciel peut avoir besoin de lire le corps de la requête, puis du rembobiner afin d’être disponible pour le point de terminaison.
La méthode d’extension EnableBuffering active la mise en mémoire tampon du corps de la requête HTTP et est la méthode recommandée pour activer les lectures multiples. Étant donné qu’une requête peut être de n’importe quelle taille, EnableBuffering
prend en charge les options permettant de mettre en mémoire tampon les corps de requête volumineux sur le disque ou de les rejeter entièrement.
L’intergiciel dans l’exemple suivant :
- Active les lectures multiples avec
EnableBuffering
. Vous devez l’appeler avant de lire le corps de la requête. - Lit le corps de la requête.
- Rembobine le corps de la requête au début afin que d’autres intergiciels ou le point de terminaison puissent le lire.
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
Une autre façon de lire le corps de la requête consiste à utiliser la propriété HttpRequest.BodyReader. La propriété BodyReader
expose le corps de la requête en tant que PipeReader. Cette API provient de pipelines d’E/S, un moyen avancé et à hautes performances de lire le corps de la requête.
Le lecteur accède directement au corps de la requête et gère la mémoire pour le compte de l’appelant. Contrairement à HttpRequest.Body
, le lecteur ne copie pas les données de requête dans une mémoire tampon. Toutefois, un lecteur est plus compliqué à utiliser qu’un flux et doit être manié avec précaution.
Pour plus d’informations sur la lecture du contenu à partir de BodyReader
, consultez PipeReader pour les pipelines d’E/S.
HttpResponse
HttpContext.Response fournit l’accès à HttpResponse. HttpResponse
est utilisé pour définir des informations sur la réponse HTTP renvoyée au client.
Les membres couramment utilisés sur HttpResponse
incluent :
Propriété | Description | Exemple |
---|---|---|
HttpResponse.StatusCode | Le code de réponse. Doit être défini avant d’écrire dans le corps de la réponse. | 200 |
HttpResponse.ContentType | L’en-tête content-type de la réponse. Doit être défini avant d’écrire dans le corps de la réponse. |
application/json |
HttpResponse.Headers | Une collection des en-têtes de réponse. Doit être défini avant d’écrire dans le corps de la réponse. | server=Kestrel x-custom-header=MyValue |
HttpResponse.Body | Un Stream pour écrire le corps de la réponse. | Page web générée |
Définir les en-têtes de réponse
HttpResponse.Headers fournit l’accès aux en-têtes de réponse envoyés avec la réponse HTTP. Il existe deux façons d’accéder aux en-têtes à l’aide de cette collection :
- Indiquez le nom de l’en-tête à l’indexeur sur la collection d’en-têtes. Le nom de l’en-tête ne respecte pas la casse. L’indexeur peut accéder à n’importe quelle valeur d’en-tête.
- La collection d’en-têtes possède également des propriétés permettant d’obtenir et de définir des en-têtes HTTP couramment utilisés. Les propriétés offrent un moyen rapide et piloté par IntelliSense d’accéder aux en-têtes.
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();
Une application ne peut pas modifier les en-têtes une fois la réponse démarrée. Une fois la réponse démarrée, les en-têtes sont envoyés au client. Une réponse est démarrée en vidant le corps de la réponse ou en appelant HttpResponse.StartAsync(CancellationToken). La propriété HttpResponse.HasStarted indique si la réponse a démarré. Une erreur est générée lors de la tentative de modification des en-têtes après le démarrage de la réponse :
System.InvalidOperationException : les en-têtes sont en lecture seule, la réponse a déjà démarré.
Remarque
Sauf si la mise en mémoire tampon des réponses est activée, toutes les opérations d’écriture (par exemple WriteAsync) vident le corps de la réponse en interne et marquent la réponse comme étant démarrée. La mise en mémoire tampon des réponses est désactivée par défaut.
Écrire le corps de la réponse
Une réponse HTTP peut inclure un corps de réponse. Le corps de la réponse est des données associées à la réponse, comme le contenu de page web généré, la charge utile JSON UTF-8 ou un fichier.
HttpResponse.Body permet d’écrire le corps de la réponse avec 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
peut être écrit directement ou utilisé avec d’autres API qui écrivent dans un flux.
BodyWriter
Une autre façon d’écrire le corps de la réponse consiste à utiliser la propriété HttpResponse.BodyWriter. La propriété BodyWriter
expose le corps de la réponse en tant que PipeWriter. Cette API provient de pipelines d’E/S, et il s’agit d’un moyen avancé et à hautes performances d’écrire la réponse.
L’enregistreur fournit un accès direct au corps de la réponse et gère la mémoire pour le compte de l’appelant. Contrairement à HttpResponse.Body
, l’enregistreur ne copie pas les données de requête dans une mémoire tampon. Toutefois, un enregistreur est plus compliqué à utiliser qu’un flux et le code de l’enregistreur doit être testé de façon exhaustive.
Pour plus d’informations sur l’écriture de contenu dans BodyWriter
, consultez PipeWriter pour les pipelines d’E/S.
Définir des amorces de réponse
HTTP/2 et HTTP/3 prennent en charge les amorces de réponse. Les amorces sont des en-têtes envoyés avec la réponse une fois le corps de la réponse terminé. Étant donné que les amorces sont envoyées après le corps de la réponse, elles peuvent être ajoutées à la réponse à tout moment.
Le code suivant définit des amorces à l’aide de 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
Le jeton d’annulation HttpContext.RequestAborted peut être utilisé pour avertir que la requête HTTP a été abandonnée par le client ou le serveur. Le jeton d’annulation doit être passé aux tâches de longue durée afin qu’elles puissent être annulées si la requête est abandonnée. Par exemple, l’abandon d’une requête de base de données ou d’une requête HTTP pour obtenir des données à retourner dans la réponse.
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();
Le jeton d’annulation RequestAborted
n’a pas besoin d’être utilisé pour les opérations de lecture du corps de la requête, car les lectures réagissent toujours immédiatement lorsque la requête est abandonnée. Le jeton RequestAborted
est généralement inutile lors de l’écriture des corps de réponse, car les écritures s’arrêtent immédiatement lorsque la requête est abandonnée.
Dans certains cas, le passage du jeton RequestAborted
aux opérations d’écriture peut être un moyen pratique de forcer une boucle d’écriture à quitter tôt avec un OperationCanceledException. Toutefois, il est généralement préférable de passer le jeton RequestAborted
dans toutes les opérations asynchrones responsables de la récupération du contenu du corps de la réponse à la place.
Notes
Les API minimales prennent en charge la liaison de HttpContext.RequestAborted directement à un paramètre CancellationToken.
Abort()
La méthode HttpContext.Abort() peut être utilisée pour abandonner une requête HTTP à partir du serveur. L’abandon de la requête HTTP déclenche immédiatement le jeton d’annulation HttpContext.RequestAborted et envoie une notification au client que le serveur a abandonné la requête.
L’intergiciel dans l’exemple suivant :
- Ajoute une vérification personnalisée pour les requêtes malveillantes.
- Abandonne la requête HTTP si celle-ci est malveillante.
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 propriété HttpContext.User est utilisée pour obtenir ou définir l’utilisateur, représenté par ClaimsPrincipal, pour la requête. Le ClaimsPrincipal est généralement défini par l’authentification 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();
Notes
Les API minimales prennent en charge la liaison de HttpContext.User directement à un paramètre ClaimsPrincipal.
Features
La propriété HttpContext.Features fournit l’accès à la collection d’interfaces de fonctionnalités pour la requête actuelle. Étant donné que la collection de fonctionnalités est mutable même dans le contexte d’une requête, il est possible d’utiliser un intergiciel (middleware) pour modifier la collection et ajouter la prise en charge de fonctionnalités supplémentaires. Certaines fonctionnalités avancées sont uniquement disponibles en accédant à l’interface associée via la collection de fonctionnalités.
L’exemple suivant :
- Obtient le IHttpMinRequestBodyDataRateFeature à partir de la collection de fonctionnalités.
- Met MinDataRate sur la valeur Null. Cela supprime l’exigence de débit de données minimal que le corps de la requête doit recevoir du client pour cette requête 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();
Pour plus d’informations sur l’utilisation des fonctionnalités de requête et de HttpContext
, consultez Fonctionnalités de requête dans ASP.NET Core.
HttpContext n’est pas thread-safe
Cet article traite principalement de l’utilisation de HttpContext
dans le flux de requête et de réponse de Razor Pages, des contrôleurs, des intergiciels, etc. Tenez compte des éléments suivants lors de l’utilisation de HttpContext
en dehors du flux de demande et de réponse :
- Le
HttpContext
n’est PAS thread-safe ; l’accès à celui-ci à partir de plusieurs threads peut entraîner des exceptions, une altération des données et des résultats généralement imprévisibles. - L’interface IHttpContextAccessor doit être utilisée avec précaution. Comme toujours, le
HttpContext
doit pas être capturé en dehors du flux de requête.IHttpContextAccessor
:- S’appuie sur AsyncLocal<T>, ce qui peut avoir un impact négatif sur les performances sur les appels asynchrones.
- Crée une dépendance sur « l’état ambiant », ce qui peut rendre les tests plus difficiles.
- IHttpContextAccessor.HttpContext peut être
null
en cas d’accès en dehors du flux de requête. - Pour accéder aux informations de
HttpContext
en dehors du flux de requête, copiez les informations à l’intérieur du flux de requête. Veillez à copier les données réelles et pas seulement les références. Par exemple, au lieu de copier une référence à unIHeaderDictionary
, copiez les valeurs d’en-tête appropriées ou copiez le dictionnaire entier clé par clé avant de quitter le flux de requête. - Ne capturez pas
IHttpContextAccessor.HttpContext
dans un constructeur.
L’exemple suivant journalise les branches GitHub lorsque vous le demandez à partir du point de terminaison /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 nécessite deux en-têtes. L’en-tête User-Agent
est ajouté dynamiquement par 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);
}
}
Dans le code précédent, lorsque le HttpContext
est null
, la chaîne userAgent
est définie sur "Unknown"
. Si possible, HttpContext
doit être explicitement passé au service. La transmission explicite de données HttpContext
:
- Rend l’API de service plus utilisable en dehors du flux de requête.
- Est préférable pour les performances.
- Rend le code plus facile à comprendre et expliquer qu’en s’appuyant sur l’état ambiant.
Lorsque le service doit accéder à HttpContext
, il doit tenir compte de la possibilité que HttpContext
soit null
lorsqu’il n’est pas appelé à partir d’un thread de requête.
L’application inclut également PeriodicBranchesLoggerService
, qui journalise les branches GitHub ouvertes du référentiel spécifié toutes les 30 secondes :
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
est un service hébergé, qui s’exécute en dehors du flux de requête et de réponse. La journalisation à partir du PeriodicBranchesLoggerService
a un HttpContext
null. Le PeriodicBranchesLoggerService
a été écrit pour ne pas dépendre du 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 =>
{