Sdílet prostřednictvím


Použití httpContextu v ASP.NET Core

Poznámka:

Toto není nejnovější verze tohoto článku. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Upozorňující

Tato verze ASP.NET Core se už nepodporuje. Další informace najdete v tématu .NET a .NET Core Zásady podpory. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Důležité

Tyto informace se týkají předběžného vydání produktu, který může být podstatně změněn před komerčním vydáním. Microsoft neposkytuje žádné záruky, výslovné ani předpokládané, týkající se zde uváděných informací.

Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

HttpContext Zapouzdřuje všechny informace o jednotlivých požadavcích a odpovědích HTTP. Instance HttpContext se inicializuje při přijetí požadavku HTTP. Instance HttpContext je přístupná prostřednictvím middlewaru a aplikačních architektur, jako jsou kontrolery webového rozhraní API, Razor Stránky, SignalR, gRPC a další.

Další informace o přístupu k sadě HttpContext, naleznete v tématu Access HttpContext v ASP.NET Core.

HttpRequest

HttpContext.Request poskytuje přístup k HttpRequest. HttpRequest obsahuje informace o příchozím požadavku HTTP a inicializuje se při přijetí požadavku HTTP serverem. HttpRequest není jen pro čtení a middleware může měnit hodnoty požadavků v kanálu middlewaru.

Mezi běžně používané členy HttpRequest patří:

Vlastnost Popis Příklad
HttpRequest.Path Cesta požadavku. /en/article/getstarted
HttpRequest.Method Metoda požadavku. GET
HttpRequest.Headers Kolekce hlaviček požadavků. user-agent=Edge
x-custom-header=MyValue
HttpRequest.RouteValues Kolekce hodnot tras. Kolekce se nastaví, když se požadavek shoduje s trasou. language=en
article=getstarted
HttpRequest.Query Kolekce hodnot dotazu parsovaných z QueryString. filter=hello
page=1
HttpRequest.ReadFormAsync() Metoda, která čte tělo požadavku jako formulář a vrátí kolekci hodnot formuláře. Informace o tom, proč ReadFormAsync byste měli použít pro přístup k datům formuláře, naleznete v tématu Prefer ReadFormAsync přes Request.Form. email=user@contoso.com
HttpRequest.Body A Stream pro čtení textu požadavku. Datová část UTF-8 JSON

Získání hlaviček požadavků

HttpRequest.Headers poskytuje přístup k hlavičce požadavku odeslané pomocí požadavku HTTP. Pomocí této kolekce můžete získat přístup k hlavičce dvěma způsoby:

  • Zadejte název záhlaví indexeru v kolekci hlaviček. V názvu záhlaví se nerozlišuje malá a velká písmena. Indexer má přístup k libovolné hodnotě záhlaví.
  • Kolekce hlaviček má také vlastnosti pro získání a nastavení běžně používaných hlaviček HTTP. Vlastnosti poskytují rychlý a řízený způsob, jak získat přístup k hlavičce technologie IntelliSense.
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();

Informace o efektivním zpracování hlaviček, které se zobrazují více než jednou, najdete v stručné části StringValues.

Text požadavku pro čtení

Požadavek HTTP může obsahovat text požadavku. Text požadavku je data přidružená k požadavku, jako je například obsah formuláře HTML, datová část JSON UTF-8 nebo soubor.

HttpRequest.Bodyumožňuje čtení textu požadavku: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 lze číst přímo nebo používat s jinými rozhraními API, která přijímají stream.

Poznámka:

Minimální rozhraní API podporují vazbu HttpRequest.Body přímo s parametrem Stream .

Povolení ukládání textu požadavku do vyrovnávací paměti

Text požadavku lze číst pouze jednou od začátku do konce. Čtení textu požadavku jen pro předávání dál zabraňuje režii ukládání celého textu požadavku do vyrovnávací paměti a snižuje využití paměti. V některých scénářích je ale potřeba přečíst text požadavku několikrát. Middleware může například potřebovat přečíst text požadavku a potom ho převinout zpět, aby byl pro koncový bod dostupný.

Metoda EnableBuffering rozšíření umožňuje ukládání textu požadavku HTTP do vyrovnávací paměti a je doporučeným způsobem, jak povolit více čtení. Vzhledem k tomu, že požadavek může mít libovolnou velikost, EnableBuffering podporuje možnosti ukládání velkých těl požadavků do vyrovnávací paměti na disk nebo je zcela odmítnout.

Middleware v následujícím příkladu:

  • Povolí více čtení pomocí EnableBufferingfunkce . Před přečtením textu požadavku se musí volat.
  • Přečte text požadavku.
  • Převine text požadavku na začátek, aby ho mohl přečíst jiný middleware nebo koncový bod.
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

Alternativním způsobem čtení textu požadavku je použití HttpRequest.BodyReader vlastnosti. Vlastnost BodyReader zveřejňuje text požadavku jako PipeReader. Toto rozhraní API je z vstupně-výstupních kanálů, což je pokročilý a vysoce výkonný způsob čtení textu požadavku.

Čtenář přímo přistupuje k textu požadavku a spravuje paměť jménem volajícího. Na rozdíl od HttpRequest.Bodytoho čtenář data požadavku nekopíruje do vyrovnávací paměti. Čtenář je ale složitější než datový proud a měl by se používat s opatrností.

Informace o tom, jak číst obsah z BodyReader, naleznete v části I/O pipelines PipeReader.

HttpResponse

HttpContext.Response poskytuje přístup k HttpResponse. HttpResponse slouží k nastavení informací o odpovědi HTTP odeslané zpět klientovi.

Mezi běžně používané členy HttpResponse patří:

Vlastnost Popis Příklad
HttpResponse.StatusCode Kód odpovědi. Před zápisem do textu odpovědi je nutné nastavit nastavení. 200
HttpResponse.ContentType Hlavička odpovědi content-type . Před zápisem do textu odpovědi je nutné nastavit nastavení. application/json
HttpResponse.Headers Kolekce hlaviček odpovědí. Před zápisem do textu odpovědi je nutné nastavit nastavení. server=Kestrel
x-custom-header=MyValue
HttpResponse.Body A Stream pro napsání textu odpovědi. Vygenerovaná webová stránka

Nastavení hlaviček odpovědí

HttpResponse.Headers poskytuje přístup k hlavičce odpovědi odeslané pomocí odpovědi HTTP. Pomocí této kolekce můžete získat přístup k hlavičce dvěma způsoby:

  • Zadejte název záhlaví indexeru v kolekci hlaviček. V názvu záhlaví se nerozlišuje malá a velká písmena. Indexer má přístup k libovolné hodnotě záhlaví.
  • Kolekce hlaviček má také vlastnosti pro získání a nastavení běžně používaných hlaviček HTTP. Vlastnosti poskytují rychlý a řízený způsob, jak získat přístup k hlavičce technologie IntelliSense.
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();

Aplikace nemůže upravit hlavičky po spuštění odpovědi. Jakmile se odpověď spustí, hlavičky se odešlou klientovi. Odpověď se spustí vyprázdněním textu odpovědi nebo voláním HttpResponse.StartAsync(CancellationToken). Vlastnost HttpResponse.HasStarted označuje, zda byla odpověď spuštěna. Při pokusu o úpravu hlaviček po spuštění odpovědi dojde k chybě:

System.InvalidOperationException: Hlavičky jsou jen pro čtení, odpověď se už spustila.

Poznámka:

Pokud není povolené ukládání odpovědí do vyrovnávací paměti, všechny operace zápisu (například WriteAsync) vyprázdní text odpovědi interně a označí odpověď jako spuštěnou. Ukládání odpovědí do vyrovnávací paměti je ve výchozím nastavení zakázané.

Text odpovědi napsat

Odpověď HTTP může obsahovat text odpovědi. Tělo odpovědi je data přidružená k odpovědi, jako je vygenerovaný obsah webové stránky, datová část JSON UTF-8 nebo soubor.

HttpResponse.Body umožňuje zápis textu odpovědi pomocí 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 lze zapisovat přímo nebo používat s jinými rozhraními API, která zapisují do datového proudu.

BodyWriter

Alternativním způsobem zápisu textu odpovědi je použití HttpResponse.BodyWriter vlastnosti. Vlastnost BodyWriter zveřejňuje tělo odpovědi jako PipeWriter. Toto rozhraní API pochází z vstupně-výstupních kanálů a jedná se o pokročilý vysoce výkonný způsob, jak napsat odpověď.

Zapisovač poskytuje přímý přístup k textu odpovědi a spravuje paměť jménem volajícího. Na rozdíl od HttpResponse.Bodyzápisu data požadavku nekopíruje do vyrovnávací paměti. Zápis je ale složitější než datový proud a kód zapisovače by se měl důkladně testovat.

Informace o tom, jak psát obsah do BodyWriter, naleznete v části I/O pipelines PipeWriter.

Nastavení přívěsů pro odezvu

Přívěsy pro odpovědi podporují protokoly HTTP/2 a HTTP/3. Přívěsy jsou hlavičky odeslané s odpovědí po dokončení textu odpovědi. Protože přívěsy se posílají po těle odpovědi, mohou být k odpovědi kdykoli přidány přívěsy.

Následující kód nastavuje přívěsy pomocí 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 zrušení lze použít k upozornění, že požadavek HTTP byl přerušen klientem nebo serverem. Token zrušení by se měl předávat dlouhotrvajícím úkolům, aby se daly zrušit, pokud je požadavek přerušený. Například přerušením databázového dotazu nebo požadavku HTTP na získání dat, která se mají vrátit v odpovědi.

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 zrušení se nemusí používat pro operace čtení textu požadavku, protože čtení se vždy vyvolá okamžitě, když je požadavek přerušen. Token RequestAborted je také obvykle nepotřebný při zápisu textu odpovědi, protože zápis okamžitě no-op při přerušení požadavku.

V některých případech může předání tokenu k operacím zápisu RequestAborted být pohodlným způsobem, jak vynutit ukončení smyčky zápisu v rané fázi pomocí OperationCanceledException. Obvykle je ale lepší předat RequestAborted token do všech asynchronních operací zodpovědných za načtení obsahu těla odpovědi.

Poznámka:

Minimální rozhraní API podporují vazbu HttpContext.RequestAborted přímo s parametrem CancellationToken .

Abort()

Metodu HttpContext.Abort() lze použít k přerušení požadavku HTTP ze serveru. Přerušení požadavku HTTP okamžitě aktivuje HttpContext.RequestAborted token zrušení a odešle klientovi oznámení, že server žádost přerušil.

Middleware v následujícím příkladu:

  • Přidá vlastní kontrolu škodlivých požadavků.
  • Přeruší požadavek HTTP, pokud je požadavek škodlivý.
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

Vlastnost HttpContext.User slouží k získání nebo nastavení uživatele, reprezentované ClaimsPrincipal, pro požadavek. Obvykle se ClaimsPrincipal nastavuje ověřováním 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();

Poznámka:

Minimální rozhraní API podporují vazbu HttpContext.User přímo s parametrem ClaimsPrincipal .

Features

Vlastnost HttpContext.Features poskytuje přístup k kolekci rozhraní funkcí pro aktuální požadavek. Vzhledem k tomu, že kolekce funkcí je proměnlivá i v kontextu požadavku, můžete middleware použít k úpravě kolekce a přidání podpory dalších funkcí. Některé pokročilé funkce jsou k dispozici pouze přístupem k přidruženému rozhraní prostřednictvím kolekce funkcí.

Následující příklad:

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

Další informace o používání funkcí žádostí a HttpContextnaleznete v tématu Funkce žádosti v ASP.NET Core.

HttpContext není bezpečný pro přístup z více vláken

Tento článek primárně popisuje použití HttpContext v toku požadavků a odpovědí ze Razor stránek, kontrolerů, middlewaru atd. Při použití HttpContext mimo požadavek a tok odpovědi zvažte následující skutečnosti:

  • Není HttpContext bezpečné vlákno, přístup k němu z více vláken může vést k výjimkám, poškození dat a obecně nepředvídatelným výsledkům.
  • Rozhraní IHttpContextAccessor by mělo být použito s opatrností. Stejně jako vždy HttpContext nesmí být zachyceno mimo tok požadavku. IHttpContextAccessor:
    • Spoléhá na to, který může mít negativní dopad na AsyncLocal<T> výkon asynchronních volání.
    • Vytvoří závislost na okolním stavu, což může ztížit testování.
  • IHttpContextAccessor.HttpContext může být null v případě přístupu mimo tok požadavku.
  • Pokud chcete získat přístup k informacím mimo HttpContext tok požadavku, zkopírujte informace uvnitř toku požadavku. Dávejte pozor, abyste zkopírovali skutečná data, a ne jenom odkazy. Například místo kopírování odkazu na , IHeaderDictionaryzkopírujte příslušné hodnoty záhlaví nebo zkopírujte celý klíč slovníku podle klíče před opuštěním toku požadavku.
  • Nezachycujte IHttpContextAccessor.HttpContext je v konstruktoru.

Následující ukázkové protokoly protokolují větve GitHubu při vyžádání z koncového /branch bodu:

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

Rozhraní API GitHubu vyžaduje dvě hlavičky. Záhlaví User-Agent se přidává dynamicky pomocí 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();

Pomocná rutina 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);
    }
}

V předchozím kódu, pokud HttpContext je null, userAgent řetězec je nastaven na "Unknown". Pokud je to možné, HttpContext mělo by se službě explicitně předat. Explicitní předávání HttpContext dat:

  • Zpřístupňuje rozhraní API služby lépe použitelné mimo tok požadavku.
  • Je lepší pro výkon.
  • Usnadňuje pochopení kódu a důvod, než aby se spoléhal na okolní stav.

Když musí služba přistupovat HttpContext, měla by mít za to, HttpContext null že při nevolaném vlákně žádosti by měla být.

Aplikace také zahrnuje PeriodicBranchesLoggerServiceprotokoly otevřené větve GitHubu zadaného úložiště každých 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);
    }
}

PeriodicBranchesLoggerServiceje hostovaná služba, která běží mimo tok požadavku a odpovědi. Protokolování z objektu PeriodicBranchesLoggerService má hodnotu null HttpContext. Bylo PeriodicBranchesLoggerService zapsáno, že nezávisí na 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 =>
{