다음을 통해 공유


ASP.NET Core에서 HttpContext 사용

참고 항목

이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

Warning

이 버전의 ASP.NET Core는 더 이상 지원되지 않습니다. 자세한 내용은 .NET 및 .NET Core 지원 정책을 참조 하세요. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

Important

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.

현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

HttpContext은 개별 HTTP 요청 및 응답에 대한 모든 정보를 캡슐화합니다. HttpContext 인스턴스는 HTTP 요청을 수신할 때 초기화됩니다. HttpContext 인스턴스는 Web API 컨트롤러, Razor Pages, SignalR, gRPC 등과 같은 미들웨어 및 앱 프레임워크에서 액세스할 수 있습니다.

HttpContext에 액세스하는 방법에 대한 자세한 내용은 ASP.NET Core에서 HttpContext 액세스를 참조하세요.

HttpRequest

HttpContext.RequestHttpRequest에 대한 액세스를 제공합니다. HttpRequest에는 들어오는 HTTP 요청에 대한 정보가 있으며, 서버에서 HTTP 요청을 수신할 때 초기화됩니다. HttpRequest는 읽기 전용이 아니며, 미들웨어는 미들웨어 파이프라인에서 요청 값을 변경할 수 있습니다.

HttpRequest에서 일반적으로 사용되는 멤버는 다음과 같습니다.

속성 Description 예시
HttpRequest.Path 요청 경로입니다. /en/article/getstarted
HttpRequest.Method 요청 방법입니다. GET
HttpRequest.Headers 요청 헤더의 컬렉션입니다. user-agent=Edge
x-custom-header=MyValue
HttpRequest.RouteValues 경로 값의 컬렉션. 요청이 경로와 일치하면 컬렉션이 설정됩니다. language=en
article=getstarted
HttpRequest.Query QueryString에서 구문 분석된 쿼리 값의 컬렉션. filter=hello
page=1
HttpRequest.ReadFormAsync() 요청 본문을 폼으로 읽고 양식 값 컬렉션을 반환하는 메서드. 양식 데이터에 액세스하는 데 ReadFormAsync을 사용해야 하는 이유에 대한 자세한 내용은 Request.Form보다 ReadFormAsync 선호를 참조하세요. email=user@contoso.com
HttpRequest.Body 요청 본문을 읽기 위한 Stream. UTF-8 JSON 페이로드

요청 헤더를 가져옵니다.

HttpRequest.Headers는 HTTP 요청과 함께 전송된 요청 헤더에 대한 액세스를 제공합니다. 이 컬렉션을 사용하여 헤더에 액세스하는 방법에는 두 가지가 있습니다.

  • 헤더 컬렉션의 인덱서에 헤더 이름을 제공합니다. 헤더 이름은 대/소문자를 구분하지 않습니다. 인덱서는 모든 헤더 값에 액세스할 수 있습니다.
  • 헤더 컬렉션에는 일반적으로 사용되는 HTTP 헤더를 가져오고 설정하는 속성도 있습니다. 속성은 헤더에 액세스하는 빠른 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();

두 번 이상 표시되는 헤더를 효율적으로 처리하는 방법에 대한 자세한 내용은 StringValues를 간략하게 살펴보세요.

요청 본문 읽기

HTTP 요청에는 요청 본문이 포함될 수 있습니다. 요청 본문은 HTML 양식, UTF-8 JSON 페이로드 또는 파일의 콘텐츠와 같이 요청과 연결된 데이터입니다.

HttpRequest.Body를 사용하여 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();

스트림을 허용하는 다른 API와 함께 HttpRequest.Body을 직접 읽거나 사용할 수 있습니다.

참고 항목

최소 APIStream 매개 변수에 직접 HttpRequest.Body을 바인딩하는 것을 지원합니다.

요청 본문 버퍼링 사용

요청 본문은 처음부터 끝까지 한 번만 읽을 수 있습니다. 요청 본문의 전달 전용 읽기는 전체 요청 본문을 버퍼링하는 오버헤드를 방지하고 메모리 사용량을 줄입니다. 그러나 일부 시나리오에서는 요청 본문을 여러 번 읽어야 합니다. 예를 들어, 미들웨어는 요청 본문을 읽은 다음, 엔드포인트에 사용할 수 있도록 되감아야 할 수 있습니다.

EnableBuffering 확장 메서드를 사용하면 HTTP 요청 본문을 버퍼링할 수 있으며 여러 번 읽기를 사용하도록 설정하는 것이 좋습니다. 요청은 모든 크기일 수 있으므로 EnableBuffering는 큰 요청 본문을 디스크에 버퍼링하거나 완전히 거부하는 옵션을 지원합니다.

다음 예제의 미들웨어에는 다음이 적용됩니다.

  • EnableBuffering를 사용하여 여러 번 읽기를 사용하도록 설정합니다. 요청 본문을 읽기 전에 호출해야 합니다.
  • 요청 본문을 읽습니다.
  • 다른 미들웨어 또는 엔드포인트에서 읽을 수 있도록 요청 본문을 시작으로 되감습니다.
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

요청 본문을 읽는 다른 방법은 HttpRequest.BodyReader 속성을 사용하는 것입니다. BodyReader 속성은 요청 본문을 PipeReader로 노출합니다. 이 API는 요청 본문을 읽는 고급 고성능 방법인 I/O 파이프라인에서 제공됩니다.

판독기는 요청 본문에 직접 액세스하고 호출자를 대신하여 메모리를 관리합니다. HttpRequest.Body과 달리, 판독기는 요청 데이터를 버퍼에 복사하지 않습니다. 그러나 판독기는 스트림보다 사용하기가 더 복잡하며 주의해서 사용해야 합니다.

BodyReader에서 콘텐츠를 읽는 방법에 대한 자세한 내용은 I/O 파이프라인 PipeReader를 참조하세요.

HttpResponse

HttpContext.ResponseHttpResponse에 대한 액세스를 제공합니다. HttpResponse는 클라이언트로 다시 전송된 HTTP 응답에 대한 정보를 설정하는 데 사용됩니다.

HttpResponse에서 일반적으로 사용되는 멤버는 다음과 같습니다.

속성 Description 예시
HttpResponse.StatusCode 응답 코드. 응답 본문에 쓰기 전에 설정해야 합니다. 200
HttpResponse.ContentType 응답 content-type 헤더. 응답 본문에 쓰기 전에 설정해야 합니다. application/json
HttpResponse.Headers 응답 헤더의 컬렉션. 응답 본문에 쓰기 전에 설정해야 합니다. server=Kestrel
x-custom-header=MyValue
HttpResponse.Body 응답 본문을 작성하기 위한 Stream. 생성된 웹 페이지

응답 헤더 설정

HttpResponse.Headers는 HTTP 응답과 함께 전송된 응답 헤더에 대한 액세스를 제공합니다. 이 컬렉션을 사용하여 헤더에 액세스하는 방법에는 두 가지가 있습니다.

  • 헤더 컬렉션의 인덱서에 헤더 이름을 제공합니다. 헤더 이름은 대/소문자를 구분하지 않습니다. 인덱서는 모든 헤더 값에 액세스할 수 있습니다.
  • 헤더 컬렉션에는 일반적으로 사용되는 HTTP 헤더를 가져오고 설정하는 속성도 있습니다. 속성은 헤더에 액세스하는 빠른 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();

응답이 시작된 후에는 앱에서 헤더를 수정할 수 없습니다. 응답이 시작되면 헤더가 클라이언트로 전송됩니다. 응답 본문을 플러시하거나 HttpResponse.StartAsync(CancellationToken)를 호출하여 응답이 시작됩니다. HttpResponse.HasStarted 속성은 응답이 시작되었는지 여부를 나타냅니다. 응답이 시작된 후 헤더를 수정하려고 하면 오류가 발생합니다.

System.InvalidOperationException: 헤더는 읽기 전용이며 응답이 이미 시작되었습니다.

참고 항목

응답 버퍼링을 사용하지 않는 한 모든 쓰기 작업(예 WriteAsync: )은 응답 본문을 내부적으로 플러시하고 응답을 시작으로 표시합니다. 응답 버퍼링은 기본적으로 사용하지 않도록 설정됩니다.

응답 본문 작성

HTTP 응답에는 응답 본문이 포함될 수 있습니다. 응답 본문은 생성된 웹 페이지 콘텐츠, UTF-8 JSON 페이로드 또는 파일과 같은 응답과 연결된 데이터입니다.

HttpResponse.Body을 사용하여 응답 본문을 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는 스트림에 쓰는 다른 API와 함께 직접 작성하거나 사용할 수 있습니다.

Bodywriter

응답 본문을 작성하는 다른 방법은 HttpResponse.BodyWriter 속성을 사용하는 것입니다. BodyWriter 속성은 응답 본문을 PipeWriter로 노출합니다. 이 API는 I/O 파이프라인에서 생성되며 응답을 작성하는 고급 고성능 방법입니다.

작성기는 응답 본문에 대한 직접 액세스를 제공하고 호출자를 대신하여 메모리를 관리합니다. HttpResponse.Body과 달리, 작성기는 요청 데이터를 버퍼에 복사하지 않습니다. 그러나 작성기는 철저히 테스트해야 하는 스트림 및 기록기 코드보다 사용하기가 더 복잡합니다.

BodyWriter에 콘텐츠를 쓰는 방법에 대한 자세한 내용은 I/O 파이프라인 PipeWriter를 참조하세요.

응답 트레일러 설정

HTTP/2 및 HTTP/3은 응답 트레일러를 지원합니다. 트레일러는 응답 본문이 완료된 후 응답과 함께 전송되는 헤더입니다. 트레일러는 응답 본문 이후에 전송되므로 언제든지 트레일러를 응답에 추가할 수 있습니다.

다음 코드는 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

HttpContext.RequestAborted 취소 토큰을 사용하여 클라이언트 또는 서버에서 HTTP 요청이 중단되었음을 알릴 수 있습니다. 요청이 중단된 경우 취소할 수 있도록 취소 토큰을 장기 실행 작업에 전달해야 합니다. 예를 들어, 응답에서 반환할 데이터를 가져오기 위해 데이터베이스 쿼리 또는 HTTP 요청을 중단합니다.

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

요청이 중단될 때 읽기가 항상 즉시 throw되므로 RequestAborted 취소 토큰을 요청 본문 읽기 작업에 사용할 필요가 없습니다. 요청이 중단될 때 즉시 no-op을 쓰기 때문에, 응답 본문을 작성할 때 RequestAborted 토큰도 일반적으로 필요하지 않습니다.

경우에 따라 쓰기 작업에 RequestAborted 토큰을 전달하는 것이 쓰기 루프가 OperationCanceledException를 사용하여 일찍 종료되도록 강제하는 편리한 방법이 될 수 있습니다. 그러나 일반적으로 대신 응답 본문 콘텐츠를 검색하는 비동기 작업에 RequestAborted 토큰을 전달하는 것이 좋습니다.

참고 항목

최소 APICancellationToken 매개 변수에 직접 HttpContext.RequestAborted을 바인딩하는 것을 지원합니다.

Abort()

HttpContext.Abort() 메서드를 사용하여 서버에서 HTTP 요청을 중단할 수 있습니다. HTTP 요청을 중단하면 HttpContext.RequestAborted 취소 토큰이 즉시 트리거되고 서버가 요청을 중단했다는 알림을 클라이언트에 보냅니다.

다음 예제의 미들웨어에는 다음이 적용됩니다.

  • 악의적인 요청에 대한 사용자 지정 검사를 추가합니다.
  • 요청이 악의적인 경우 HTTP 요청을 중단합니다.
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

HttpContext.User 속성은 요청에 대해 ClaimsPrincipal로 표시되는 사용자를 가져오기 또는 설정하는 데 사용됩니다. ClaimsPrincipal은 일반적으로 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();

참고 항목

최소 APIClaimsPrincipal 매개 변수에 직접 HttpContext.User을 바인딩하는 것을 지원합니다.

Features

HttpContext.Features 속성은 현재 요청의 기능 인터페이스 컬렉션에 대한 액세스를 제공합니다. 기능 컬렉션은 요청 컨텍스트 내에서도 변경할 수 있기 때문에, 미들웨어를 사용하여 콜렉션을 수정하고 추가 기능에 대한 지원을 추가할 수 있습니다. 일부 고급 기능은 기능 컬렉션을 통해 연결된 인터페이스에 액세스해야 사용할 수 있습니다.

다음 예제를 참조하세요.

  • 기능 컬렉션에서 IHttpMinRequestBodyDataRateFeature을 가져옵니다.
  • MinDataRate을 null로 설정합니다. 이렇게 하면 이 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();

요청 기능 및 HttpContext 사용에 대한 자세한 내용은 ASP.NET Core에서 기능 요청을 참조하세요.

HttpContext가 스레드로부터 안전하지 않음

이 문서에서는 주로 Razor Pages, 컨트롤러, 미들웨어 등의 요청 및 응답 흐름에 HttpContext을(를) 사용하는 방법에 대해 설명합니다. 요청 및 응답 흐름 외부에서 HttpContext을(를) 사용할 때 다음 사항을 고려합니다.

  • HttpContext은(는) 스레드가 안전하지 않으므로 여러 스레드에서 액세스하면 예외, 데이터 손상 및 일반적으로 예측할 수 없는 결과가 발생할 수 있습니다.
  • IHttpContextAccessor 인터페이스는 주의해서 사용해야 합니다. 항상 그렇듯이 HttpContext은(는) 요청 흐름 외부에서 캡처해서는 안 됩니다. IHttpContextAccessor:
    • 비동기 호출에 부정적인 성능 영향을 미칠 수 있는 AsyncLocal<T> 기능을 사용합니다.
    • 테스트를 더 어렵게 만들 수 있는 "주변 상태"에 대한 종속성을 만듭니다.
  • IHttpContextAccessor.HttpContext은(는) 요청 흐름 외부에서 액세스하는 경우 null일 수 있습니다.
  • 요청 흐름 외부의 HttpContext에서 정보에 액세스하려면 요청 흐름 내에서 정보를 복사합니다. 참조뿐만 아니라 실제 데이터를 주의해서 복사해야 합니다. 예를 들어 참조를 IHeaderDictionary(으)로 복사하는 대신, 관련 헤더 값을 복사하거나 요청 흐름을 벗어나기 전에 키별로 전체 사전 키를 복사합니다.
  • 생성자에서 IHttpContextAccessor.HttpContext을(를) 캡처하지 마세요.

다음 샘플은 /branch 엔드포인트에서 요청된 경우 GitHub 분기를 기록합니다.

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

GitHub API에는 두 개의 헤더가 필요합니다. User-Agent 헤더는 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);
    }
}

앞의 코드에서 HttpContext이(가) null인 경우, userAgent 문자열이 "Unknown"로 설정됩니다. 가능하면 HttpContext은(는) 서비스에 명시적으로 전달되어야 합니다. HttpContext 데이터를 명시적으로 전달합니다.

  • 요청 흐름 외부에서 서비스 API를 더 쉽게 사용할 수 있게 합니다.
  • 성능에 더 좋습니다.
  • 앰비언트 상태에 의존하는 것보다 코드를 더 쉽게 이해하고 추론할 수 있습니다.

서비스가 HttpContext에 액세스 해야 하는 경우, 요청 스레드에서 호출되지 않으면, HttpContext이(가) null일 가능성을 고려해야 합니다.

또한 애플리케이션에는 지정된 리포지토리의 열린 GitHub 분기를 30초마다 기록하는 PeriodicBranchesLoggerService도 포함됩니다.

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은(는) 요청 및 응답 흐름 외부에서 실행되는 호스트된 서비스입니다. PeriodicBranchesLoggerService에서 로깅에 null HttpContext이(가) 있습니다. PeriodicBranchesLoggerService은(는) 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 =>
{