ASP.NET Core 성능 모범 사례

Mike Rousos 작성

이 문서에서는 ASP.NET Core 성능 모범 사례에 대한 지침을 제공합니다.

적극적으로 캐시

캐싱은 이 문서의 여러 부분에서 설명합니다. 자세한 내용은 ASP.NET Core 응답 캐싱를 참조하세요.

핫 코드 경로 이해

이 문서에서 실행 코드 경로는 자주 호출되고 대부분의 실행 시간이 발생하는 코드 경로로 정의됩니다. 핫 코드 경로는 일반적으로 앱 스케일 아웃 및 성능을 제한하며 이 문서의 여러 부분에서 설명합니다.

차단되는 호출 방지

ASP.NET Core 앱은 많은 요청을 동시에 처리하도록 설계되어야 합니다. 비동기 API를 사용하면 차단 호출을 기다리지 않고 작은 스레드 풀에서 수천 개의 동시 요청을 처리할 수 있습니다. 스레드는 장기 실행 동기 작업이 완료되기를 기다리는 대신 다른 요청에서 작업할 수 있습니다.

ASP.NET Core 앱의 일반적인 성능 문제는 비동기일 수 있는 호출을 차단하는 것입니다. 많은 동기 차단 호출은 스레드 풀 결핍 및 응답 시간 저하로 이어집니다.

수행하지 않아야 할 작업:

  • Task.Wait 또는 Task.Result를 호출하여 비동기 실행을 차단합니다.
  • 공통 코드 경로에서 잠금을 획득합니다. ASP.NET Core 앱은 코드를 병렬로 실행하도록 설계될 때 가장 많은 성과를 발휘합니다.
  • Task.Run을 호출하고 즉시 기다립니다. ASP.NET Core는 이미 일반 스레드 풀 스레드에서 앱 코드를 실행하므로 Task.Run을 호출하면 불필요한 스레드 풀 일정만 추가로 발생합니다. 예약된 코드가 스레드를 차단하더라도 Task.Run은 이를 방지하지 않습니다.

해야 할 일:

  • 핫 코드 경로를 비동기식으로 만듭니다.
  • 비동기 API를 사용할 수 있는 경우 데이터 액세스, I/O 및 장기 실행 작업 API를 비동기적으로 호출합니다. Task.Run을 사용하여 동기 API를 비동기로 만들지 마십시오.
  • Razor컨트롤러/페이지 작업을 비동기식으로 만듭니다. 전체 호출 스택은 async/await 패턴을 활용하기 위해 비동기식입니다.

PerfView와 같은 프로파일러를 사용하여 스레드 풀에 자주 추가되는 스레드를 찾을 수 있습니다. Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start 이벤트는 스레드 풀에 추가된 스레드를 나타냅니다.

여러 개의 작은 페이지에서 큰 컬렉션 반환

웹 페이지는 대량의 데이터를 한 번에 로드하면 안 됩니다. 개체 컬렉션을 반환할 때 성능 문제가 발생할 수 있는지 여부를 고려합니다. 디자인이 다음과 같은 좋지 못한 결과를 생성할 수 있는지 확인합니다.

이전 시나리오를 완화하려면 페이지 지정을 추가하세요. 개발자는 페이지 크기 및 페이지 인덱스 매개 변수를 사용하여 부분 결과를 반환하는 디자인을 선호해야 합니다. 철저한 결과가 필요한 경우 페이지 매김을 사용하여 서버 리소스 잠금을 방지하기 위해 결과의 일괄 처리를 비동기적으로 채워야 합니다.

페이징 및 반환 된 레코드 수를 제한에 대 한 자세한 내용은 참조 하세요.

또는 반환 IEnumerable<T>``IAsyncEnumerable<T>

작업에서 를 IEnumerable<T> 반환하면 serializer가 동기 컬렉션 반복을 수행합니다. 그 결과는 호출 차단이며 스레드 풀 고갈의 가능성도 있습니다. 동기 열거를 방지하려면 ToListAsync 열거형을 반환하기 전에 를 사용합니다.

ASP.NET Core 3.0부터 를 IAsyncEnumerable<T> 비동기적으로 열거하는 대신 사용할 수 IEnumerable<T> 있습니다. 자세한 내용은 컨트롤러 작업 반환 형식을 참조하세요.

큰 개체 할당 최소화

.NET Core 가비지 수집기는 ASP.NET Core 앱에서 자동으로 메모리 할당 및 해제를 관리합니다. 일반적으로 자동 가비지 수집은 개발자가 메모리가 해제되는 방법이나 시기에 대해 걱정할 필요가 없다는 것을 의미합니다. 그러나 참조되지 않은 개체를 정리하는 데는 CPU 시간이 걸리므로 개발자는 핫 코드 경로 에서 개체 할당을 최소화해야 합니다. 가비지 수집은 큰 개체(> 85K바이트)에서 특히 비용이 많이 듭니다. 큰 개체는 큰 개체 힙에 저장되며 정리하려면 전체(2세대) 가비지 수집이 필요합니다. 0세대 및 1세대 컬렉션과 달리 2세대 컬렉션은 앱 실행을 일시적으로 일시 중단해야 합니다. 큰 개체를 자주 할당하고 할당을 해제하면 성능이 일관되지 않게 될 수 있습니다.

권장 사항:

  • 자주 사용되는 큰 개체를 캐싱하는 것이 좋습니다. 큰 개체를 캐시하는 것은 비용이 많이 드는 할당을 방지합니다.
  • ArrayPool을 <T> 사용하여 큰 배열을 저장하여 버퍼를 수행합니다.
  • 핫 코드 경로 에 수명이 짧은 많은 큰 개체를 할당하지 마십시오.

이전과 같은 메모리 문제는 PerfView에서 GC(가비지 수집) 통계를 검토하고 다음을 검사하여 진단할 수 있습니다.

  • 가비지 수집 일시 중지 시간입니다.
  • 가비지 수집에 소요되는 프로세서 시간의 백분율입니다.
  • 0세대, 1세대 및 2세대 가비지 수집 수입니다.

자세한 내용은 가비지 수집 및 성능을 참조하세요.

데이터 액세스 및 I/O 최적화

데이터 저장소 및 기타 원격 서비스와의 상호 작용은 ASP.NET Core 앱에서 가장 느린 부분인 경우가 많습니다. 데이터를 효율적으로 읽고 쓰는 것은 좋은 성능을 위해 매우 중요합니다.

권장 사항:

  • 모든 데이터 액세스 API를 비동기적으로 호출합니다.
  • 필요한 것보다 많은 데이터를 검색하지 마십시오. 쿼리를 작성하여 현재 HTTP 요청에 필요한 데이터만 반환합니다.
  • 약간 오래된 데이터가 허용되는 경우 데이터베이스 또는 원격 서비스에서 검색되는 자주 액세스하는 데이터를 캐싱하는 것이 좋습니다. 시나리오에 따라 MemoryCache 또는 DistributedCache를 사용합니다. 자세한 내용은 ASP.NET Core 응답 캐싱를 참조하세요.
  • 네트워크 왕복을 최소화합니다. 목표는 여러 번의 호출이 아닌 단일 호출에서 필요한 데이터를 검색하는 것입니다.
  • 읽기 전용으로 데이터에 액세스할 때는 Entity Framework Core 비 추적 쿼리를 사용하십시오. EF Core 비 추적 쿼리의 결과를 보다 효율적으로 반환할 수 있습니다.
  • 데이터베이스에서 필터링을 수행하게 되도록 LINQ .Where 쿼리(예: .Select , 또는 .Sum 문 포함)를 필터링하고 집계합니다.
  • EF Core 클라이언트에서 일부 쿼리 연산자를 확인하여 비효율적인 쿼리 실행이 발생할 수 있다는 점을 고려해야 합니다. 자세한 내용은 클라이언트 평가 성능 문제 를 참조하세요.
  • 컬렉션에서 프로젝션 쿼리를 사용하지 마십시오. 이렇게 하면 "N + 1" SQL 쿼리가 실행됩니다. 자세한 내용은 상관 관계가 있는 하위 검색 최적화를 참조하세요.

대규모 앱에서 성능을 향상시킬 수 있는 방법은 EF 고성능을 참조하세요.

코드베이스를 커밋하기 전에 이전 고성능 접근 방식의 영향을 측정하는 것이 좋습니다. 컴파일된 쿼리의 추가 복잡성으로 성능 향상이 정당화되지 않을 수 있습니다.

Application Insights 또는 프로파일링 도구를 사용하여 데이터에 액세스하는 데 소요된 시간을 검토하여 쿼리 문제를 검색할 수 있습니다. 또한 대부분의 데이터베이스는 자주 실행되는 쿼리와 관련된 통계를 사용할 수 있도록 합니다.

HttpClientFactory를 통해 풀 HTTP 연결

HttpClient는 인터페이스를 구현하지만 IDisposable 다시 사용할 수 있도록 설계되었습니다. HttpClient닫힌 인스턴스는 소켓을 짧은 기간 동안 상태로 열어 TIME_WAIT 둡니다. 개체를 만들고 삭제하는 코드 HttpClient 경로가 자주 사용되는 경우 앱은 사용 가능한 소켓을 소모할 수 있습니다. HttpClientFactory는 이 문제에 대한 솔루션으로 ASP.NET Core 2.1에서 도입되었습니다. 풀링 HTTP 연결을 처리하여 성능 및 안정성을 최적화합니다.

권장 사항:

일반적인 코드 경로를 빠르게 유지

모든 코드를 빠르게 사용할 수 있습니다. 자주 호출되는 코드 경로는 최적화에 가장 중요합니다. 이러한 위협은 다음과 같습니다.

  • 앱의 요청 처리 파이프라인에 있는 미들웨어 구성 요소, 특히 미들웨어는 파이프라인 초기에 실행됩니다. 이러한 구성 요소는 성능에 큰 영향을 미칩니다.
  • 모든 요청에 대해 또는 요청당 여러 번 실행되는 코드입니다. 예를 들어 사용자 지정 로깅, 권한 부여 처리기 또는 임시 서비스 초기화가 있습니다.

권장 사항:

HTTP 요청 외부에서 장기 실행 작업 완료

필요한 서비스를 호출하고 HTTP 응답을 반환하는 컨트롤러 또는 페이지 모델에서 ASP.NET Core 앱에 대한 대부분의 요청을 처리할 수 있습니다. 장기 실행 작업이 포함된 일부 요청의 경우 전체 요청-응답 프로세스를 비동기식으로 만드는 것이 좋습니다.

권장 사항:

  • 일반 HTTP 요청 처리의 일부로 장기 실행 작업이 완료되기를 기다리지 마십시오.
  • 백그라운드 서비스를 통해 또는 Azure 함수를 통해 장기 실행 요청을 처리하는 것이 좋습니다. out-of-process 작업을 완료하면 CPU를 많이 사용하는 작업에 특히 유용합니다.
  • 와 같은 실시간 통신 옵션을 사용하여 SignalR 클라이언트와 비동기적으로 통신합니다.

클라이언트 자산 축소

복잡한 프런트 엔드가 있는 ASP.NET Core 앱은 많은 JavaScript, CSS 또는 이미지 파일을 제공하는 경우가 많습니다. 초기 로드 요청의 성능은 다음을 통해 향상될 수 있습니다.

  • 여러 파일을 하나로 결합하는 번들링.
  • 축소 - 공백 및 주석을 제거하여 파일 크기를 줄입니다.

권장 사항:

  • 호환되는 도구를 언급하고 ASP.NET Core 태그를 사용하여 및 환경을 모두 처리하는 방법을 보여주는 번들링및 축소 지침을 environment Development Production 사용하세요.
  • 복잡한 클라이언트 자산 관리를 위해 Webpack과같은 다른 타사 도구를 고려하세요.

응답 압축

응답 크기를 줄이면 일반적으로 앱의 응답성이 크게 증가합니다. 페이로드 크기를 줄이는 한 가지 방법은 앱의 응답을 압축하는 것입니다. 자세한 내용은 응답 압축을 참조하세요.

최신 ASP.NET Core 릴리스 사용

새로운 ASP.NET Core 릴리스마다 성능 향상이 포함됩니다. .NET Core 및 ASP.NET Core 최적화는 최신 버전이 일반적으로 이전 버전보다 우수하다는 것을 의미합니다. 예를 들어 .NET Core 2.1은 컴파일된 정규식에 대한 지원을 추가하고 Span <T> 을 활용했습니다. ASP.NET Core 2.2에는 HTTP/2에 대한 지원이 추가되었습니다. ASP.NET Core 3.0에는 메모리 사용량을 줄이고 처리량을 향상시키는 많은 개선이 추가되었습니다. 성능이 우선 순위인 경우 현재 버전의 ASP.NET Core 업그레이드하는 것이 좋습니다.

예외 최소화

예외는 드물어야 합니다. 예외를 throw하고 포착하는 것은 다른 코드 흐름 패턴에 비해 느립니다. 이로 인해 예외는 정상적인 프로그램 흐름을 제어하는 데 사용하면 안 됩니다.

권장 사항:

  • 특히 핫 코드 경로에서 예외를 throw하거나 포착하는 것은 정상적인 프로그램 흐름의 수단으로 사용하지 마십시오.
  • 예외를 발생시키는 조건을 검색하고 처리하려면 앱에 논리를 포함해야 합니다.
  • 비정상적이거나 예기치 않은 조건에 대한 예외를 throw하거나 catch합니다.

애플리케이션 Insights 같은 앱 진단 도구는 성능에 영향을 줄 수 있는 앱의 일반적인 예외를 식별하는 데 도움이 될 수 있습니다.

성능 및 안정성

다음 섹션에서는 성능 팁과 알려진 안정성 문제 및 솔루션을 제공합니다.

HttpRequest/HttpResponse 본문에서 동기 읽기 또는 쓰기 방지

ASP.NET Core 모든 I/O는 비동기입니다. 서버는 Stream 동기 및 비동기 오버로드를 모두 포함하는 인터페이스를 구현합니다. 스레드 풀 스레드를 차단하지 않으려면 비동기 스레드를 선호해야 합니다. 스레드 차단은 스레드 풀 고갈로 이어질 수 있습니다.

이렇게 하지 마십시오. 다음 예제에서는 ReadToEnd 을 사용합니다. 현재 스레드가 결과를 기다리도록 차단합니다. 비동기 에 대한 동기화의예입니다.

public class BadStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public ActionResult<ContosoData> Get()
    {
        var json = new StreamReader(Request.Body).ReadToEnd();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }
}

위의 코드에서 는 Get 전체 HTTP 요청 본문을 메모리로 동기적으로 읽습니다. 클라이언트가 느리게 업로드하는 경우 앱은 비동기에서 동기화를 수행합니다. 는 동기 읽기를 지원하지 않으므로 앱은 비동기에서 Kestrel 동기화를 수행합니다.

이 작업을 수행합니다. 다음 예제에서는 및 를 사용하여 ReadToEndAsync 읽는 동안 스레드를 차단하지 않습니다.

public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        var json = await new StreamReader(Request.Body).ReadToEndAsync();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }

}

위의 코드는 전체 HTTP 요청 본문을 메모리로 비동기적으로 읽습니다.

경고

요청이 큰 경우 전체 HTTP 요청 본문을 메모리로 읽으면 OOM(메모리 부족) 상태가 발생할 수 있습니다. OOM으로 인해 서비스 거부가 발생할 수 있습니다. 자세한 내용은 이 문서의 큰 요청 본문 또는 응답 본문을 메모리로 읽지 않도록 을 참조하세요.

이 작업을 수행합니다. 다음 예제는 버퍼링되지 않은 요청 본문을 사용하는 완전히 비동기식입니다.

public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        return await JsonSerializer.DeserializeAsync<ContosoData>(Request.Body);
    }
}

위의 코드는 요청 본문을 C# 개체로 비동기적으로 직렬화합니다.

Request.Form보다 ReadFormAsync를 선호합니다.

HttpContext.Request.Form 대신 HttpContext.Request.ReadFormAsync을(를) 사용합니다. HttpContext.Request.Form 는 다음 조건에서만 안전하게 읽을 수 있습니다.

  • , 및 호출을 통해 양식을 읽었습니다. ReadFormAsync
  • 를 사용하여 캐시된 양식 값을 읽고 있습니다. HttpContext.Request.Form

이렇게 하지 마십시오. 다음 예제에서는 HttpContext.Request.Form 를 사용합니다. HttpContext.Request.Form비동기를 통해 동기화를 사용하며 스레드 풀 고갈로 이어질 수 있습니다.

public class BadReadController : Controller
{
    [HttpPost("/form-body")]
    public IActionResult Post()
    {
        var form =  HttpContext.Request.Form;

        Process(form["id"], form["name"]);

        return Accepted();
    }

이 작업을 수행합니다. 다음 예제에서는 를 사용하여 HttpContext.Request.ReadFormAsync 양식 본문을 비동기적으로 읽습니다.

public class GoodReadController : Controller
{
    [HttpPost("/form-body")]
    public async Task<IActionResult> Post()
    {
       var form = await HttpContext.Request.ReadFormAsync();

        Process(form["id"], form["name"]);

        return Accepted();
    }

큰 요청 신체 또는 응답 신체를 메모리로 읽지 않도록 방지

.NET에서 85KB를 초과하는 모든 개체 할당은 큰 개체 힙(LOH)에 포함됩니다. 큰 개체는 다음 두 가지 방법으로 비용이 많이 듭니다.

  • 새로 할당된 큰 개체에 대한 메모리를 지워야 하므로 할당 비용이 높습니다. CLR은 새로 할당된 모든 개체에 대한 메모리가 지워지게 합니다.
  • LOH는 나머지 힙과 함께 수집됩니다. LOH에는 전체 가비지 수집 또는 Gen2 수집이 필요합니다.

블로그 게시물에서는 문제를 간결하게 설명합니다.

큰 개체가 할당되면 Gen 2 개체로 표시됩니다. 작은 개체의 경우 Gen 0이 아닙니다. 그 결과 LOH에서 메모리가 부족하면 GC는 LOH뿐만 아니라 관리되는 전체 힙을 정리합니다. 따라서 LOH를 포함하여 Gen 0, Gen 1 및 Gen 2를 정리합니다. 이를 전체 가비지 수집이라고 하며 시간이 가장 많이 걸리는 가비지 수집입니다. 많은 애플리케이션에서 허용될 수 있습니다. 그러나 평균 웹 요청을 처리하는 데 필요한 큰 메모리 버퍼가 거의 없는 고성능 웹 서버의 경우는 아닙니다(소켓에서 읽기, 압축 풀기, JSON 디코딩 & 추가).

큰 요청 또는 응답 본문을 단일 또는 에 순차적으로 저장합니다. byte[] string

  • LOH에서 공간이 빠르게 부족할 수 있습니다.
  • 전체 GC가 실행 중이므로 앱의 성능 문제가 발생할 수 있습니다.

동기 데이터 처리 API 작업

동기 읽기 및 쓰기를 지 원하는 serializer/직렬 변환기를 사용 하는 경우 (예: JSON.NET):

  • 데이터를 직렬 변환기/역직렬화로 전달 하기 전에 비동기적으로 메모리에 버퍼링 합니다.

경고

요청이 크면 OOM (메모리 부족) 조건이 발생할 수 있습니다. OOM은 서비스 거부를 유발할 수 있습니다. 자세한 내용은이 문서의 메모리에 대 한 대량 요청 본문 또는 응답 본문 읽기 방지 를 참조 하세요.

ASP.NET Core 3.0는 System.Text.Json 기본적으로 JSON serialization에 사용 됩니다. System.Text.Json:

  • JSON을 비동기적으로 읽고 씁니다.
  • UTF-8 텍스트에 최적화되어 있습니다.
  • 일반적으로 Newtonsoft.Json보다 성능이 높습니다.

IHttpContextAccessor를 필드에 저장 하지 마십시오.

IHttpContextAccessorHttpContext 요청 스레드에서 액세스 될 때 활성 요청의를 반환 합니다. 는 IHttpContextAccessor.HttpContext 필드 또는 변수에 저장 하면 안 됩니다 .

이 작업을 수행 하지 마십시오. 다음 예에서는를 HttpContext 필드에 저장 한 다음 나중에 사용 하려고 시도 합니다.

public class MyBadType
{
    private readonly HttpContext _context;
    public MyBadType(IHttpContextAccessor accessor)
    {
        _context = accessor.HttpContext;
    }

    public void CheckAdmin()
    {
        if (!_context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}

위의 코드는 생성자에서 null 또는 잘못 된를 캡처하는 경우가 많습니다 HttpContext .

다음 작업을 수행 합니다. 다음 예제를 수행 합니다.

  • IHttpContextAccessor 필드에 저장 합니다.
  • HttpContext는 필드를 올바른 시간에 사용 하 고를 확인 null 합니다.
public class MyGoodType
{
    private readonly IHttpContextAccessor _accessor;
    public MyGoodType(IHttpContextAccessor accessor)
    {
        _accessor = accessor;
    }

    public void CheckAdmin()
    {
        var context = _accessor.HttpContext;
        if (context != null && !context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}

여러 스레드에서 HttpContext에 액세스 하지 않음

HttpContext 는 스레드로부터 안전 하지 않습니다 . HttpContext여러 스레드에서 병렬로 액세스 하면 중단, 충돌 및 데이터 손상과 같은 정의 되지 않은 동작이 발생할 수 있습니다.

이 작업을 수행 하지 마십시오. 다음 예제에서는 세 개의 병렬 요청을 만들고 나가는 HTTP 요청 전후에 들어오는 요청 경로를 로깅합니다. 요청 경로는 여러 스레드에서 동시에 액세스할 수 있습니다.

public class AsyncBadSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        var query1 = SearchAsync(SearchEngine.Google, query);
        var query2 = SearchAsync(SearchEngine.Bing, query);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }       

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.", 
                                    HttpContext.Request.Path);
            searchResults = _searchService.Search(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", 
                                    HttpContext.Request.Path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", 
                             HttpContext.Request.Path);
        }

        return await searchResults;
    }

다음 작업을 수행 합니다. 다음 예에서는 세 개의 병렬 요청을 만들기 전에 들어오는 요청에서 모든 데이터를 복사 합니다.

public class AsyncGoodSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        string path = HttpContext.Request.Path;
        var query1 = SearchAsync(SearchEngine.Google, query,
                                 path);
        var query2 = SearchAsync(SearchEngine.Bing, query, path);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query,
                                                  string path)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.",
                                   path);
            searchResults = await _searchService.SearchAsync(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", path);
        }

        return await searchResults;
    }

요청이 완료 된 후에 HttpContext를 사용 하지 마십시오.

HttpContext는 ASP.NET Core 파이프라인에 활성 HTTP 요청이 있는 경우에만 유효 합니다. 전체 ASP.NET Core 파이프라인은 모든 요청을 실행 하는 대리자의 비동기 체인입니다. Task이 체인에서 반환 된이 완료 되 면이 HttpContext 재활용 됩니다.

이 작업을 수행 하지 마십시오. 다음 예에서는를 사용 하 여 async void 첫 번째에 도달할 때 HTTP 요청이 완료 되도록 합니다 await .

  • ASP.NET Core 앱에서 항상 잘못 된 관행입니다.
  • HTTP 요청이 완료 된 후에에 액세스 합니다 HttpResponse .
  • 프로세스를 중단 합니다.
public class AsyncBadVoidController : Controller
{
    [HttpGet("/async")]
    public async void Get()
    {
        await Task.Delay(1000);

        // The following line will crash the process because of writing after the 
        // response has completed on a background thread. Notice async void Get()

        await Response.WriteAsync("Hello World");
    }
}

다음 작업을 수행 합니다. 다음 예제에서는 Task 프레임 워크에 대해를 반환 하므로 작업이 완료 될 때까지 HTTP 요청이 완료 되지 않습니다.

public class AsyncGoodTaskController : Controller
{
    [HttpGet("/async")]
    public async Task Get()
    {
        await Task.Delay(1000);

        await Response.WriteAsync("Hello World");
    }
}

백그라운드 스레드에서 HttpContext를 캡처하지 마십시오.

이 작업을 수행 하지 마십시오. 다음 예제에서는 속성에서를 캡처하는 방법을 보여 줍니다 HttpContext Controller . 작업 항목에서 다음을 수행할 수 있기 때문에 잘못 된 사례입니다.

  • 요청 범위 외부에서를 실행 합니다.
  • 잘못 된를 읽으려고 했습니다 HttpContext .
[HttpGet("/fire-and-forget-1")]
public IActionResult BadFireAndForget()
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        var path = HttpContext.Request.Path;
        Log(path);
    });

    return Accepted();
}

다음 작업을 수행 합니다. 다음 예제를 수행 합니다.

  • 요청 하는 동안 백그라운드 작업에 필요한 데이터를 복사 합니다.
  • 는 컨트롤러에서 아무것도 참조 하지 않습니다.
[HttpGet("/fire-and-forget-3")]
public IActionResult GoodFireAndForget()
{
    string path = HttpContext.Request.Path;
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        Log(path);
    });

    return Accepted();
}

백그라운드 작업은 호스팅된 서비스로 구현 되어야 합니다. 자세한 내용은 호스티드 서비스를 사용하는 백그라운드 작업을 참조하세요.

백그라운드 스레드에서 컨트롤러에 삽입 된 서비스를 캡처하지 마십시오.

이 작업을 수행 하지 마십시오. 다음 예제에서는 DbContext 작업 매개 변수에서를 캡처하는 방법을 보여 줍니다 Controller . 이는 잘못 된 방법입니다. 작업 항목은 요청 범위 외부에서 실행 될 수 있습니다. ContosoDbContext이 요청으로 범위가 지정 되 면이 발생 ObjectDisposedException 합니다.

[HttpGet("/fire-and-forget-1")]
public IActionResult FireAndForget1([FromServices]ContosoDbContext context)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        context.Contoso.Add(new Contoso());
        await context.SaveChangesAsync();
    });

    return Accepted();
}

다음 작업을 수행 합니다. 다음 예제를 수행 합니다.

  • IServiceScopeFactory백그라운드 작업 항목에서 범위를 만들기 위해를 삽입 합니다. IServiceScopeFactory 는 단일 항목입니다.
  • 백그라운드 스레드에서 새 종속성 주입 범위를 만듭니다.
  • 는 컨트롤러에서 아무것도 참조 하지 않습니다.
  • ContosoDbContext들어오는 요청에서를 캡처하지 않습니다.
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        using (var scope = serviceScopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

강조 표시 된 코드는 다음과 같습니다.

  • 백그라운드 작업의 수명 범위를 만들고 해당 작업에서 서비스를 확인 합니다.
  • ContosoDbContext는 올바른 범위에서를 사용 합니다.
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        using (var scope = serviceScopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

응답 본문이 시작 된 후 상태 코드 또는 헤더를 수정 하지 마십시오.

ASP.NET Core는 HTTP 응답 본문을 버퍼링 하지 않습니다. 처음 응답을 쓸 때:

  • 헤더는 해당 본문의 청크와 함께 클라이언트에 전송 됩니다.
  • 더 이상 응답 헤더를 변경할 수 없습니다.

이 작업을 수행 하지 마십시오. 다음 코드는 응답이 이미 시작 된 후 응답 헤더를 추가 하려고 합니다.

app.Use(async (context, next) =>
{
    await next();

    context.Response.Headers["test"] = "test value";
});

위의 코드에서 context.Response.Headers["test"] = "test value"; 가 응답에 기록 되는 경우에서 예외를 throw next() 합니다.

다음 작업을 수행 합니다. 다음 예에서는 헤더를 수정 하기 전에 HTTP 응답이 시작 되었는지 확인 합니다.

app.Use(async (context, next) =>
{
    await next();

    if (!context.Response.HasStarted)
    {
        context.Response.Headers["test"] = "test value";
    }
});

다음 작업을 수행 합니다. 다음 예제에서는를 사용 하 여 HttpResponse.OnStarting 응답 헤더가 클라이언트에 플러시될 때까지 헤더를 설정 합니다.

응답을 시작 하지 않은 경우 응답 헤더를 쓰기 직전에 호출 되는 콜백을 등록할 수 있는지 확인 합니다. 응답이 시작 되지 않은 경우를 확인 하는 중:

  • 헤더를 적시에 추가 하거나 재정의 하는 기능을 제공 합니다.
  • 파이프라인의 다음 미들웨어에 대 한 지식이 필요 하지 않습니다.
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});

응답 본문에 대 한 쓰기를 이미 시작한 경우 next ()를 호출 하지 마십시오.

구성 요소는 응답을 처리 하 고 조작할 수 있는 경우에만 호출 될 것으로 예상 됩니다.

IIS를 사용 하 여 in-process 호스팅 사용

In-Process 호스팅을 사용하면 ASP.NET Core 앱은 IIS 작업자 프로세스와 동일한 프로세스에서 실행됩니다. In-process 호스트는 루프백 어댑터를 통해 요청이 프록시 되지 않으므로 out-of-process 호스팅을 통해 향상 된 성능을 제공 합니다. 루프백 어댑터는 나가는 네트워크 트래픽을 동일한 컴퓨터로 다시 반환 하는 네트워크 인터페이스입니다. IIS는 Windows Process Activation Service(WAS)를 사용하여 프로세스 관리를 처리합니다.

프로젝트는 ASP.NET Core 3.0 이상에서 in-process 호스팅 모델로 기본 됩니다.

자세한 내용은 IIS를 사용 하 여 Windows에서 호스트 ASP.NET Core 를 참조 하세요.