다음을 통해 공유


ASP.NET Core 9.0의 새로운 기능

이 문서에서는 ASP.NET Core 9.0의 가장 중요한 변경 내용과 관련 설명서에 대한 링크를 강조 표시합니다.

이 문서는 .NET 9 미리 보기 7용으로 업데이트되었습니다.

Blazor

이 섹션에서는 .에 대한 새로운 기능에 대해 설명합니다 Blazor.

.NET MAUIBlazor Hybrid 및 웹앱 솔루션 템플릿

새 솔루션 템플릿을 사용하면 동일한 UI를 공유하는 네이티브 및 Blazor 웹 클라이언트 앱을 더 쉽게 만들 .NET MAUI 수 있습니다. 이 템플릿은 코드 재사용을 최대화하고 Android, iOS, Mac, Windows 및 웹을 대상으로 하는 클라이언트 앱을 만드는 방법을 보여 줍니다.

이 템플릿의 주요 기능은 다음과 같습니다.

  • 웹앱에 Blazor 대한 대화형 렌더링 모드를 선택하는 기능입니다.
  • (전역 대화형 자동 렌더링) 및 앱을 포함하여 Blazor Web App 적절한 프로젝트를 자동으로 만듭니다 .NET MAUIBlazor Hybrid .
  • 만든 프로젝트는 RCL(공유 Razor 클래스 라이브러리)을 사용하여 UI의 Razor 구성 요소를 유지 관리합니다.
  • 종속성 주입을 사용하여 앱 및 앱에 대한 다양한 인터페이스 구현을 Blazor Hybrid 제공하는 방법을 보여 주는 샘플 코드가 Blazor Web App포함되어 있습니다.

시작하려면 .NET 9 SDK설치하고 템플릿을 .NET MAUI 포함하는 워크로드를 설치합니다.

dotnet workload install maui

다음 명령을 사용하여 명령 셸의 프로젝트 템플릿에서 솔루션을 만듭니다.

dotnet new maui-blazor-web

템플릿은 Visual Studio에서도 사용할 수 있습니다.

참고 항목

현재 렌더링 모드가 페이지/구성 요소 수준에서 정의된 경우 Blazor 예외가 발생합니다. 자세한 내용은 ResolveComponentForRenderMode(dotnet/aspnetcore #51235)를 재정의할 수 있는 방법이 필요한 BlazorWebView를 참조하세요.

자세한 내용은 을 사용하여 앱 빌드를 .NET MAUIBlazor Hybrid Blazor Web App참조하세요.

정적 자산 배달 최적화

MapStaticAssets 는 앱을 포함하여 Blazor 모든 ASP.NET Core 앱에서 정적 자산의 배달을 최적화하는 데 도움이 되는 새로운 미들웨어입니다.

자세한 내용은 다음 리소스 중 하나를 참조하세요.

런타임에 렌더링 위치, 대화형 작업 및 할당된 렌더링 모드 검색

런타임에 구성 요소 상태를 쿼리하는 프로세스를 간소화하도록 설계된 새로운 API를 도입했습니다. 이 API는 다음과 같은 기능을 제공합니다.

  • 구성 요소의 현재 실행 위치를 결정합니다. 구성 요소 성능을 디버깅하고 최적화하는 데 특히 유용할 수 있습니다.
  • 구성 요소가 대화형 환경에서 실행되고 있는지 확인합니다. 이는 환경의 대화형 작업에 따라 동작이 다른 구성 요소에 유용할 수 있습니다.
  • 구성 요소에 대한 할당된 렌더링 모드 검색: 렌더링 모드를 이해하면 렌더링 프로세스를 최적화하고 구성 요소의 전반적인 성능을 개선하는 데 도움이 될 수 있습니다.

자세한 내용은 ASP.NET Core Blazor 렌더링 모드를 참조하세요.

향상된 서버 쪽 다시 연결 환경:

기본 서버 쪽 다시 연결 환경은 다음과 같이 향상되었습니다.

  • 사용자가 연결이 끊긴 회로가 있는 앱으로 다시 이동하면 다음 다시 연결 간격의 기간을 기다리지 않고 즉시 다시 연결이 시도됩니다. 이렇게 하면 절전 모드로 이동한 브라우저 탭에서 앱을 탐색할 때 사용자 환경이 향상됩니다.

  • 다시 연결 시도가 서버에 도달하지만 서버가 이미 회로를 해제한 경우 페이지 새로 고침이 자동으로 발생합니다. 이렇게 하면 다시 연결이 성공할 가능성이 있는 경우 사용자가 페이지를 수동으로 새로 고칠 필요가 없습니다.

  • 다시 연결 타이밍은 계산된 백오프 전략을 사용합니다. 기본적으로 처음 몇 번의 다시 연결 시도는 시도 간에 계산된 지연이 발생하기 전에 재시도 간격 없이 빠르게 연속적으로 수행됩니다. 다음 지수 백오프 예제와 같이 재시도 간격을 계산하는 함수를 지정하여 재시도 간격 동작을 사용자 지정할 수 있습니다.

    Blazor.start({
      circuit: {
        reconnectionOptions: {
          retryIntervalMilliseconds: (previousAttempts, maxRetries) => 
            previousAttempts >= maxRetries ? null : previousAttempts * 1000
        },
      },
    });
    
  • 기본 다시 연결 UI의 스타일이 현대화되었습니다.

자세한 내용은 ASP.NET Core BlazorSignalR 지침을 참조하세요.

s에 대한 Blazor Web App간소화된 인증 상태 직렬화

새 API를 사용하면 기존에 인증을 더 쉽게 추가할 수 있습니다 Blazor Web App. 개별 계정을 사용하여 인증을 사용하여 새 Blazor Web App 항목을 만들고 WebAssembly 기반 대화형 작업을 사용하도록 설정하면 프로젝트에는 서버 및 클라이언트 프로젝트 모두에 사용자 지정 AuthenticationStateProvider 이 포함됩니다.

이러한 공급자는 사용자의 인증 상태를 브라우저로 전달합니다. 클라이언트가 아닌 서버에서 인증하면 미리 렌더링하는 동안 및 .NET WebAssembly 런타임이 초기화되기 전에 앱이 인증 상태에 액세스할 수 있습니다.

사용자 지정 AuthenticationStateProvider 구현은 영구 구성 요소 상태 서비스(PersistentComponentState)를 사용하여 인증 상태를 HTML 주석으로 직렬화하고 WebAssembly에서 다시 읽어 새 AuthenticationState 인스턴스를 만듭니다.

이는 프로젝트 템플릿에서 Blazor Web App 시작하여 개별 계정 옵션을 선택한 경우 잘 작동하지만, 기존 프로젝트에 인증을 추가하려는 경우 직접 구현하거나 복사하는 코드가 많습니다. 이제 프로젝트 템플릿의 Blazor Web App 일부인 API가 서버 및 클라이언트 프로젝트에서 호출되어 이 기능을 추가할 수 있습니다.

  • AddAuthenticationStateSerialization: 서버의 인증 상태를 직렬화하는 데 필요한 서비스를 추가합니다.
  • AddAuthenticationStateDeserialization: 브라우저에서 인증 상태를 역직렬화하는 데 필요한 서비스를 추가합니다.

기본적으로 API는 브라우저에서 액세스에 대한 서버 쪽 이름 및 역할 클레임만 직렬화합니다. 모든 클레임을 포함하도록 AddAuthenticationStateSerialization 옵션을 전달할 수 있습니다.

자세한 내용은 Secure ASP.NET Core 서버 쪽 Blazor 앱의 다음 섹션을 참조하세요.

전역 대화형에 정적 SSR(서버 쪽 렌더링) 페이지 추가 Blazor Web App

.NET 9 릴리스에서는 이제 전역 대화형 작업을 채택하는 앱에 정적 SSR 페이지를 더 간단하게 추가할 수 있습니다.

이 방법은 앱에 대화형 서버 또는 WebAssembly 렌더링을 사용할 수 없는 특정 페이지가 있는 경우에만 유용합니다. 예를 들어 HTTP 쿠키 읽기/쓰기에 의존하며 대화형 렌더링 대신 요청/응답 주기에서만 작동할 수 있는 페이지에 이 방법을 채택합니다. 대화형 렌더링을 사용하는 페이지의 경우 최종 사용자에게 덜 효율적이고 응답성이 떨어지기 때문에 정적 SSR 렌더링을 사용하도록 강요해서는 안 됩니다.

지시 Razor 문과 함께 할당된 새 [ExcludeFromInteractiveRouting] 특성으로 @attributeRazor 구성 요소 페이지를 표시합니다.

@attribute [ExcludeFromInteractiveRouting]

특성을 적용하면 페이지 탐색이 대화형 라우팅에서 종료됩니다. 인바운드 탐색은 대화형 라우팅을 통해 페이지를 확인하는 대신 전체 페이지 다시 로드를 수행해야 합니다. 전체 페이지 다시 로드는 최상위 루트 구성 요소(일반적으로 App 구성 요소)App.razor가 서버에서 다시 렌더링되도록 하여 앱이 다른 최상위 렌더링 모드로 전환되도록 합니다.

HttpContext.AcceptsInteractiveRouting 확장 메서드를 사용하면 구성 요소가 현재 페이지에 적용되는지 여부를 [ExcludeFromInteractiveRouting] 검색할 수 있습니다.

구성 요소에서 App 다음 예제의 패턴을 사용합니다.

  • 전역 대화형 작업으로 렌더링 모드에 [ExcludeFromInteractiveRouting] 기본적으로 주석이 InteractiveServer 추가되지 않은 페이지입니다. 다른 기본 전역 렌더링 모드로 InteractiveWebAssembly 바꾸 InteractiveServer 거나 InteractiveAuto 지정할 수 있습니다.
  • 정적 SSR 채택으로 [ExcludeFromInteractiveRouting] 주석이 추가된 페이지(PageRenderMode 할당됨 null).
<!DOCTYPE html>
<html>
<head>
    ...
    <HeadOutlet @rendermode="@PageRenderMode" />
</head>
<body>
    <Routes @rendermode="@PageRenderMode" />
    ...
</body>
</html>

@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? PageRenderMode
        => HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}

확장 메서드를 HttpContext.AcceptsInteractiveRouting 사용하는 대안은 .를 사용하여 HttpContext.GetEndpoint()?.Metadata수동으로 엔드포인트 메타데이터를 읽는 것입니다.

이 기능은 ASP.NET Core Blazor 렌더링 모드의 참조 설명서에서 다룹니다.

생성자 주입

Razor 구성 요소는 생성자 주입을 지원합니다.

다음 예제에서 부분(코드 숨김) 클래스는 기본 생성자를 사용하여 서비스를 삽입합니다NavigationManager.

public partial class ConstructorInjection(NavigationManager navigation)
{
    private void HandleClick()
    {
        navigation.NavigateTo("/counter");
    }
}

자세한 내용은 ASP.NET Core Blazor 종속성 주입을 참조하세요.

대화형 서버 구성 요소에 대한 Websocket 압축

기본적으로 대화형 서버 구성 요소는 WebSocket 연결에 대한 압축을 사용하도록 설정하고 CSP(콘텐츠 보안 정책) 지시문으로 설정합니다 'self'frame-ancestors . 이 지시문은 압축을 사용하도록 설정하거나 WebSocket 컨텍스트에 대한 구성이 제공될 때 앱이 제공되는 원본에만 앱을 포함 <iframe> 할 수 있도록 허용합니다.

압축을 설정하여 사용하지 않도록 설정할 ConfigureWebSocketOptions 수 있습니다. 그러면 공격null앱의 취약성이 줄어들지만 성능이 저하될 수 있습니다.

.AddInteractiveServerRenderMode(o => o.ConfigureWebSocketOptions = null)

WebSocket 압축을 허용하지만 브라우저가 앱을 포함할 수 없도록 하는 값 'none' (작은따옴표 필요)으로 <iframe>frame-ancestors 엄격한 CSP를 구성합니다.

.AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'")

자세한 내용은 다음 리소스를 참조하세요.

에서 키보드 컴퍼지션 이벤트 처리 Blazor

KeyboardEventArgs.IsComposing 속성은 키보드 이벤트가 컴퍼지션 세션의 일부인지를 나타냅니다. 키보드 이벤트의 컴퍼지션 상태를 추적하는 것은 국제 문자 입력 메서드를 처리하는 데 매우 중요합니다.

에 매개 변수가 추가됨 OverscanCountQuickGrid

QuickGrid 이제 구성 요소는 가상화를 사용할 때 표시되는 영역 전후에 렌더링되는 추가 행 수를 지정하는 속성을 노출 OverscanCount 합니다.

기본값 OverscanCount 은 3입니다. 다음 예제에서는 4로 늘입니다 OverscanCount .

<QuickGrid ItemsProvider="itemsProvider" Virtualize="true" OverscanCount="4">
    ...
</QuickGrid>

InputNumber구성 요소는 특성을 지원합니다.type="range"

InputNumber<TValue> 이제 구성 요소는 모델 바인딩 및 양식 유효성 검사를 지원하는 범위 입력을 만드는 특성을 지원 type="range" 하며, 일반적으로 텍스트 상자가 아닌 슬라이더 또는 다이얼 컨트롤로 렌더링됩니다.

<EditForm Model="Model" OnSubmit="Submit" FormName="EngineForm">
    <div>
        <label>
            Nacelle Count (2-6): 
            <InputNumber @bind-Value="Model!.NacelleCount" max="6" min="2" 
                step="1" type="range" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private EngineSpecifications? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() {}

    public class EngineSpecifications
    {
        [Required, Range(minimum: 2, maximum: 6)]
        public int NacelleCount { get; set; }
    }
}

서버 프로젝트당 여러 Blazor Web Apps

서버당 여러 Blazor Web Apps 프로젝트에 대한 지원은 .NET 10(2025년 11월)에 대해 고려됩니다.

자세한 내용은 서버 프로젝트당 여러 Blazor 웹앱에 대한 지원(dotnet/aspnetcore#52216)을 참조하세요.

SignalR

이 섹션에서는 .에 대한 새로운 기능에 대해 설명합니다 SignalR.

허브의 SignalR 다형 형식 지원

이제 허브 메서드는 파생 클래스 대신 기본 클래스를 수락하여 다형 시나리오를 사용하도록 설정할 수 있습니다. 다형성을 허용하려면 기본 형식에 주석을 추가해야 합니다.

public class MyHub : Hub
{
    public void Method(JsonPerson person)
    {
        if (person is JsonPersonExtended)
        {
        }
        else if (person is JsonPersonExtended2)
        {
        }
        else
        {
        }
    }
}

[JsonPolymorphic]
[JsonDerivedType(typeof(JsonPersonExtended), nameof(JsonPersonExtended))]
[JsonDerivedType(typeof(JsonPersonExtended2), nameof(JsonPersonExtended2))]
private class JsonPerson
{
    public string Name { get; set; }
    public Person Child { get; set; }
    public Person Parent { get; set; }
}

private class JsonPersonExtended : JsonPerson
{
    public int Age { get; set; }
}

private class JsonPersonExtended2 : JsonPerson
{
    public string Location { get; set; }
}

에 대한 향상된 활동 SignalR

SignalR 이제 허브 메서드 호출에 대한 이벤트를 내보내는 ActivitySource 이름이 지정 Microsoft.AspNetCore.SignalR.Server 되었습니다.

  • 모든 메서드는 자체 작업이므로 허브 메서드 호출 중에 활동을 내보내는 모든 작업은 허브 메서드 작업 아래에 있습니다.
  • 허브 메서드 활동에는 부모가 없습니다. 즉, 장기 실행 SignalR 연결에서 번들로 묶이지 않습니다.

다음 예제에서는 대시보드 및 OpenTelemetry 패키지를 사용합니다.NET Aspire.

<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />

파일에 다음 시작 코드를 추가합니다 Program.cs .

// Set OTEL_EXPORTER_OTLP_ENDPOINT environment variable depending on where your OTEL endpoint is
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        if (builder.Environment.IsDevelopment())
        {
            // We want to view all traces in development
            tracing.SetSampler(new AlwaysOnSampler());
        }

        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("Microsoft.AspNetCore.SignalR.Server");
    });

builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());

다음은 Aspire 대시보드의 예제 출력입니다.

허브 메서드 호출 이벤트에 대한 SignalR 활동 목록

SignalR 는 트리밍 및 네이티브 AOT를 지원합니다.

.NET 8에서 시작된 네이티브 AOT 여정을 계속하면서 클라이언트 및 서버 시나리오 모두 SignalR 에 대한 트리밍 및 네이티브 AOT(Ahead-Of-Time) 컴파일 지원을 사용하도록 설정했습니다. 이제 실시간 웹 통신에 사용하는 애플리케이션에서 Native AOT를 사용할 SignalR 때의 성능 이점을 활용할 수 있습니다.

시작

최신 .NET 9 SDK를 설치합니다.

다음 명령을 사용하여 명령 셸의 템플릿에서 webapiaot 솔루션을 만듭니다.

dotnet new webapiaot -o SignalRChatAOTExample

파일의 Program.cs 내용을 다음 SignalR 코드로 바꿉니다.

using Microsoft.AspNetCore.SignalR;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.AddSignalR();
builder.Services.Configure<JsonHubProtocolOptions>(o =>
{
    o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

app.MapHub<ChatHub>("/chatHub");
app.MapGet("/", () => Results.Content("""
<!DOCTYPE html>
<html>
<head>
    <title>SignalR Chat</title>
</head>
<body>
    <input id="userInput" placeholder="Enter your name" />
    <input id="messageInput" placeholder="Type a message" />
    <button onclick="sendMessage()">Send</button>
    <ul id="messages"></ul>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.7/signalr.min.js"></script>
    <script>
        const connection = new signalR.HubConnectionBuilder()
            .withUrl("/chatHub")
            .build();

        connection.on("ReceiveMessage", (user, message) => {
            const li = document.createElement("li");
            li.textContent = `${user}: ${message}`;
            document.getElementById("messages").appendChild(li);
        });

        async function sendMessage() {
            const user = document.getElementById("userInput").value;
            const message = document.getElementById("messageInput").value;
            await connection.invoke("SendMessage", user, message);
        }

        connection.start().catch(err => console.error(err));
    </script>
</body>
</html>
""", "text/html"));

app.Run();

[JsonSerializable(typeof(string))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

앞의 예제에서는 10MB의 네이티브 Windows 실행 파일과 10.9MB의 Linux 실행 파일을 생성합니다.

제한 사항

  • 현재 JSON 프로토콜만 지원됩니다.
    • 앞의 코드와 같이 JSON serialization 및 Native AOT를 사용하는 앱은 원본 생성기를 사용해야 System.Text.Json 합니다.
    • 이는 최소 API와 동일한 접근 방식을 따릅니다.
  • 서버에서 SignalR 형식 IAsyncEnumerable<T> 의 허브 메서드 매개 변수와 ChannelReader<T> ValueType(struct)이 있는 위치 T 는 지원되지 않습니다. 이러한 형식을 사용하면 개발 및 게시된 앱에서 시작할 때 런타임 예외가 발생합니다. 자세한 내용은 다음을 참조하세요 SignalR. 네이티브 AOT에서 ValueTypes와 함께 IAsyncEnumerable<T> 및 ChannelReader<T> 사용(dotnet/aspnetcore #56179).
  • 강력한 형식의 허브는 네이 티브 AOT(PublishAot)에서 지원되지 않습니다. 네이티브 AOT에서 강력한 형식의 허브를 사용하면 빌드 및 게시 중에 경고가 발생하며 런타임 예외가 발생합니다. 트리밍(PublishedTrimmed)과 함께 강력한 형식의 허브를 사용할 수 있습니다.
  • 비동기 반환 형식에만 Task, Task<T>ValueTask또는 ValueTask<T> 지원됩니다.

최소 API

이 섹션에서는 최소 API에 대한 새로운 기능에 대해 설명합니다.

추가 InternalServerErrorInternalServerError<TValue>TypedResults

클래스 TypedResults 는 최소 API에서 강력한 형식의 HTTP 상태 코드 기반 응답을 반환하는 데 유용한 수단입니다. TypedResults 이제 엔드포인트에서 "500 내부 서버 오류" 응답을 반환하기 위한 팩터리 메서드 및 형식이 포함됩니다. 다음은 500 응답을 반환하는 예제입니다.

var app = WebApplication.Create();

app.MapGet("/", () => TypedResults.InternalServerError("Something went wrong!"));

app.Run();

통화 ProducesProblemProducesValidationProblem 경로 그룹

ProducesProblem 경로 그룹에 대한 사용을 지원하도록 확장 메서드 및 ProducesValidationProblem 확장 메서드가 업데이트되었습니다. 이러한 메서드는 경로 그룹의 모든 엔드포인트가 OpenAPI 메타데이터를 위해 반환 ProblemDetails 하거나 응답을 반환할 ValidationProblemDetails 수 있음을 나타냅니다.

var app = WebApplication.Create();

var todos = app.MapGroup("/todos")
    .ProducesProblem();

todos.MapGet("/", () => new Todo(1, "Create sample app", false));
todos.MapPost("/", (Todo todo) => Results.Ok(todo));

app.Run();

record Todo(int Id, string Title, boolean IsCompleted);

OpenAPI

이 섹션에서는 OpenAPI의 새로운 기능에 대해 설명합니다.

OpenAPI 문서 생성에 대한 기본 제공 지원

OpenAPI 사양은 HTTP API를 설명하는 표준입니다. 이 표준을 통해 개발자는 클라이언트 생성기, 서버 생성기, 테스트 도구, 설명서 등에 연결할 수 있는 API의 모양을 정의할 수 있습니다. .NET 9 Preview에서 ASP.NET Core는 Microsoft.AspNetCore.OpenApi 패키지를 통해 컨트롤러 기반 또는 최소 API를 나타내는 OpenAPI 문서 생성을 기본적으로 지원합니다.

다음 강조 표시된 코드 호출은 다음과 같습니다.

  • AddOpenApi 필요한 종속성을 앱의 DI 컨테이너에 등록합니다.
  • MapOpenApi 앱의 경로에 필요한 OpenAPI 엔드포인트를 등록하려면 입니다.
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/hello/{name}", (string name) => $"Hello {name}"!);

app.Run();

Microsoft.AspNetCore.OpenApi 다음 명령을 사용하여 프로젝트에 패키지를 설치합니다.

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

앱을 실행하고 생성된 OpenAPI 문서를 보려면 다음 openapi/v1.json 을 수행합니다.

OpenAPI 문서

패키지를 추가하여 Microsoft.Extensions.ApiDescription.Server 빌드 시 OpenAPI 문서를 생성할 수도 있습니다.

dotnet add package Microsoft.Extensions.ApiDescription.Server --prerelease

앱의 프로젝트 파일에 다음을 추가합니다.

<PropertyGroup>
  <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
  <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
</PropertyGroup>

프로젝트 디렉터리에서 생성된 JSON 파일을 실행하고 dotnet build 검사합니다.

빌드 시 OpenAPI 문서 생성

ASP.NET Core의 기본 제공 OpenAPI 문서 생성은 다양한 사용자 지정 및 옵션을 지원합니다. 문서 및 작업 변환기를 제공하며 동일한 애플리케이션에 대해 여러 OpenAPI 문서를 관리할 수 있습니다.

ASP.NET Core의 새로운 OpenAPI 문서 기능에 대한 자세한 내용은 새 Microsoft.AspNetCore.OpenApi 문서를 참조 하세요.

OpenAPI 패키지에 대한 Intellisense 완성 기능 향상

ASP.NET Core의 OpenAPI 지원은 이제 더 접근성이 높고 사용자 친화적입니다. OpenAPI API는 공유 프레임워크와는 별도로 독립 패키지로 제공됩니다. 지금까지 개발자는 OpenAPI API용 Intellisense와 같은 코드 완성 기능을 편리하게 사용할 수 없었습니다.

이러한 차이를 인식하여 새로운 완성 공급자 및 코드 픽서가 도입되었습니다. 이러한 도구는 OpenAPI API의 검색 및 사용을 용이하게 하여 개발자가 OpenAPI를 프로젝트에 쉽게 통합할 수 있도록 설계되었습니다. 완성 공급자는 실시간 코드 제안을 제공하지만 코드 해결기는 일반적인 실수를 수정하고 API 사용량을 개선하는 데 도움이 됩니다. 이러한 향상된 기능은 개발자 환경을 개선하고 API 관련 워크플로를 간소화하기 위한 지속적인 노력의 일환입니다.

사용자가 OpenAPI 관련 API를 사용할 수 있는 문을 입력하면 완성 공급자가 API에 대한 권장 사항을 표시합니다. 예를 들어 다음 스크린샷에서는 사용자가 지원되는 형식(예: IEndpointConventionBuilder)에 호출 문을 입력할 때 AddOpenApiMapOpenApi에 대한 완성이 제공됩니다.

OpenAPI 완성

완료가 수락되고 패키지가 Microsoft.AspNetCore.OpenApi 설치되지 않은 경우 코드 접두사는 프로젝트에 종속성을 자동으로 설치하기 위한 바로 가기를 제공합니다.

자동 패키지 설치

매개 변수 및 [DefaultValue] 속성에 대한 [Required] 지원 및 특성

[DefaultValue] 복합 형식 내의 매개 변수 또는 속성에 특성이 적용되는 경우 [Required] OpenAPI 구현은 매개 변수 또는 형식 스키마와 연결된 OpenAPI 문서의 속성 및 default 매개 변수에 매핑 required 됩니다.

예를 들어 다음 API는 형식에 대해 함께 제공되는 스키마를 Todo 생성합니다.

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapPost("/todos", (Todo todo) => { });

app.Run();

class Todo
{
	public int Id { get; init; }
	public required string Title { get; init; }
	[DefaultValue("A new todo")]
	public required string Description { get; init; }
	[Required]
	public DateTime CreatedOn { get; init; }
}
{
	"required": [
	  "title",
	  "description",
	  "createdOn"
	],
	"type": "object",
	"properties": {
	  "id": {
	    "type": "integer",
	    "format": "int32"
	  },
	  "title": {
	    "type": "string"
	  },
	  "description": {
	    "type": "string",
	    "default": "A new todo"
	  },
	  "createdOn": {
	    "type": "string",
	    "format": "date-time"
	  }
	}
}

OpenAPI 문서에서 스키마 변환기 지원

기본 제공 OpenAPI 지원은 이제 System.Text.Json 및 OpenAPI 구현에서 생성된 스키마를 수정하는 데 사용할 수 있는 스키마 변환기를 지원합니다. 문서 및 작업 변환기와 마찬가지로 스키마 변환기는 OpenApiOptions 개체에 등록할 수 있습니다. 예를 들어 다음 코드 샘플에서는 스키마 변환기를 사용하여 형식의 스키마에 예제를 추가하는 방법을 보여 줍니다.

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.OpenApi.Any;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.UseSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.Type == typeof(Todo))
        {
            schema.Example = new OpenApiObject
            {
                ["id"] = new OpenApiInteger(1),
                ["title"] = new OpenApiString("A short title"),
                ["description"] = new OpenApiString("A long description"),
                ["createdOn"] = new OpenApiDateTime(DateTime.Now)
            };
        }
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapPost("/todos", (Todo todo) => { });

app.Run();

class Todo
{
	public int Id { get; init; }
	public required string Title { get; init; }
	[DefaultValue("A new todo")]
	public required string Description { get; init; }
	[Required]
	public DateTime CreatedOn { get; init; }
}

Microsoft.AspNetCore.OpenApi의 변환기 등록 API 개선 사항

OpenAPI 변환기는 OpenAPI 문서, 문서 내 작업 또는 API의 형식과 연결된 스키마 수정을 지원합니다. OpenAPI 문서에 변환기를 등록하기 위한 API는 변환기를 등록하기 위한 다양한 옵션을 제공합니다.

이전에는 변압기를 등록하는 데 다음 API를 사용할 수 있었습니다.

OpenApiOptions UseTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions UseTransformer(IOpenApiDocumentTransformer transformer)
OpenApiOptions UseTransformer<IOpenApiDocumentTransformer>()
OpenApiOptions UseSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task>)
OpenApiOptions UseOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task>)

새 API 집합은 다음과 같습니다.

OpenApiOptions AddDocumentTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddDocumentTransformer(IOpenApiDocumentTransformer transformer)
OpenApiOptions AddDocumentTransformer<IOpenApiDocumentTransformer>()

OpenApiOptions AddSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddSchemaTransformer(IOpenApiSchemaTransformer transformer)
OpenApiOptions AddSchemaTransformer<IOpenApiSchemaTransformer>()

OpenApiOptions AddOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddOperationTransformer(IOpenApiOperationTransformer transformer)
OpenApiOptions AddOperationTransformer<IOpenApiOperationTransformer>()

Microsoft.AspNetCore.OpenApi는 트리밍 및 네이티브 AOT를 지원합니다.

ASP.NET Core의 새로운 기본 제공 OpenAPI 지원은 이제 트리밍 및 네이티브 AOT도 지원합니다.

시작하기

새 ASP.NET Core Web API(네이티브 AOT) 프로젝트를 만듭니다.

dotnet new webapiaot

Microsoft.AspNetCore.OpenAPI 패키지를 추가합니다.

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

이 미리 보기의 경우 경고를 자르지 않도록 최신 Microsoft.OpenAPI 패키지를 추가해야 합니다.

dotnet add package Microsoft.OpenApi

OpenAPI 문서를 생성할 수 있도록 Program.cs 업데이트합니다.

+ builder.Services.AddOpenApi();

var app = builder.Build();

+ app.MapOpenApi();

앱을 게시합니다.

dotnet publish

앱은 경고 없이 네이티브 AOT를 사용하여 게시합니다.

통화 ProducesProblemProducesValidationProblem 경로 그룹 지원

경로 그룹에 대해 ProducesProblemProducesValidationProblem 확장 메서드가 업데이트되었습니다. 이러한 메서드를 사용하여 경로 그룹의 모든 엔드포인트가 OpenAPI 메타데이터를 위해 반환하거나 응답을 반환 ProblemDetailsValidationProblemDetails 수 있음을 나타낼 수 있습니다.

var app = WebApplication.Create();

var todos = app.MapGroup("/todos")
    .ProducesProblem(StatusCodes.Status500InternalServerError);

todos.MapGet("/", () => new Todo(1, "Create sample app", false));
todos.MapPost("/", (Todo todo) => Results.Ok(todo));

app.Run();

record Todo(int Id, string Title, bool IsCompleted);

ProblemValidationProblem 결과 형식은 값을 사용하여 생성을 IEnumerable<KeyValuePair<string, object?>> 지원합니다.

.NET 9 이전에는 문제ValidationProblem 결과 형식을 최소 API로 생성하려면 errors 구현을 사용하여 IDictionary<string, object?>속성과 extensions 속성을 초기화해야 했습니다. 이 릴리스에서 이러한 생성 API는 사용하는 IEnumerable<KeyValuePair<string, object?>>오버로드를 지원합니다.

var app = WebApplication.Create();

app.MapGet("/", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions",
                                                       extensions: extensions);
});

이 기여에 대한 GitHub 사용자 joegoldman2 덕분에!

인증 및 권한 부여

이 섹션에서는 인증 및 권한 부여를 위한 새로운 기능에 대해 설명합니다.

OpenIdConnectHandler는 PAR(푸시된 권한 부여 요청)에 대한 지원을 추가합니다.

ASP.NET Core의 OpenIdConnectHandler에 PAR(푸시된 권한 부여 요청)를 추가한 Duende SoftwareJoe DeCock에게 감사드립니다. Joe는 다음과 같이 API 제안에서 PAR을 사용하도록 설정하는 배경과 동기를 설명했습니다.

PAR(푸시된 권한 부여 요청)은 앞 채널에서 뒤로 채널로 권한 부여 매개 변수를 이동하여 OAuth 및 OIDC 흐름의 보안을 향상시키는 비교적 새로운 OAuth 표준 입니다. 즉, 브라우저의 리디렉션 URL에서 백 엔드의 컴퓨터 http 호출로 권한 부여 매개 변수를 이동합니다.

이렇게 하면 브라우저의 사이버 공격이 다음에서 방지됩니다.

  • PII를 누수할 수 있는 권한 부여 매개 변수를 확인합니다.
  • 해당 매개 변수를 변조합니다. 예를 들어 사이버 공격은 요청되는 액세스 범위를 변경할 수 있습니다.

권한 부여 매개 변수를 푸시하면 요청 URL도 짧게 유지됩니다. 다양한 권한 부여 요청과 같은 더 복잡한 OAuth 및 OIDC 기능을 사용하는 경우 권한 부여 매개 변수가 매우 길어질 수 있습니다. 긴 URL은 많은 브라우저 및 네트워킹 인프라에서 문제를 일으킵니다.

PAR 사용은 OpenID Foundation 내의 FAPI 작업 그룹에 의해 권장됩니다. 예를 들어 FAPI2.0 보안 프로필 에는 PAR을 사용해야 합니다. 이 보안 프로필은 오픈 뱅킹(주로 유럽), 의료 및 높은 보안 요구 사항이 있는 다른 산업에서 작업하는 많은 그룹에서 사용됩니다.

PAR은 다음과 같은 여러 identity 공급자에서 지원됩니다.

.NET 9의 경우 공급자의 검색 문서가 PAR에 대한 지원을 보급하는 경우 identity 기본적으로 PAR을 사용하도록 설정하기로 결정했습니다. 이는 이를 지원하는 공급자에게 향상된 보안을 제공해야 하기 때문에. identity 공급자의 검색 문서는 일반적으로 .에서 찾을 수 있습니다.well-known/openid-configuration. 이로 인해 문제가 발생하는 경우 다음과 같이 OpenIdConnectOptions.PushedAuthorizationBehavior를 통해 PAR을 사용하지 않도록 설정할 수 있습니다.

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect("oidc", oidcOptions =>
    {
        // Other provider-specific configuration goes here.

        // The default value is PushedAuthorizationBehavior.UseIfAvailable.

        // 'OpenIdConnectOptions' does not contain a definition for 'PushedAuthorizationBehavior'
        // and no accessible extension method 'PushedAuthorizationBehavior' accepting a first argument
        // of type 'OpenIdConnectOptions' could be found
        oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable;
    });

PAR을 사용하는 경우에만 인증이 성공하도록 하려면 PushedAuthorizationBehavior.Require를 대신 사용합니다. 또한 이 변경으로 인해 푸시된 권한 부여 요청을 사용자 지정하거나 수동으로 처리할 수 있는 OpenIdConnectEvents에 새 OnPushAuthorization 이벤트가 도입되었습니다. 자세한 내용은 API 제안을 참조하세요.

OIDC 및 OAuth 매개 변수 사용자 지정

이제 OAuth 및 OIDC 인증 처리기에는 AdditionalAuthorizationParameters 일반적으로 리디렉션 쿼리 문자열의 일부로 포함된 권한 부여 메시지 매개 변수를 보다 쉽게 사용자 지정할 수 있는 옵션이 제공됩니다. .NET 8 이전 버전에서는 사용자 지정 처리기에서 사용자 지정 OnRedirectToIdentityProvider 콜백 또는 재정의된 BuildChallengeUrl 메서드가 필요합니다. 다음은 .NET 8 코드의 예입니다.

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.Events.OnRedirectToIdentityProvider = context =>
    {
        context.ProtocolMessage.SetParameter("prompt", "login");
        context.ProtocolMessage.SetParameter("audience", "https://api.example.com");
        return Task.CompletedTask;
    };
});

이제 앞의 예제를 다음 코드로 간소화할 수 있습니다.

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.AdditionalAuthorizationParameters.Add("prompt", "login");
    options.AdditionalAuthorizationParameters.Add("audience", "https://api.example.com");
});

HTTP.sys 확장 인증 플래그 구성

이제 HTTP.sys 새 EnableKerberosCredentialCaching 속성과 속성을 사용하여 플래그를 구성 HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING 하고 HTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL CaptureCredentials HTTP.sys AuthenticationManager Windows 인증 처리 방법을 최적화할 수 있습니다. 예시:

webBuilder.UseHttpSys(options =>
{
    options.Authentication.Schemes = AuthenticationSchemes.Negotiate;
    options.Authentication.EnableKerberosCredentialCaching = true;
    options.Authentication.CaptureCredentials = true;
});

기타

다음 섹션에서는 기타 새로운 기능에 대해 설명합니다.

HybridCache 라이브러리

API는 HybridCache 기존 IDistributedCache API와 IMemoryCache API의 일부 격차를 해소합니다. 또한 다음과 같은 새로운 기능도 추가합니다.

  • 동일한 작업의 병렬 인출을 방지하기 위한 "Stampede" 보호 입니다.
  • 구성 가능한 serialization입니다.

HybridCache 는 기존 IDistributedCacheIMemoryCache 사용을 위한 드롭인 대체 기능으로 설계되었으며 새 캐싱 코드를 추가하기 위한 간단한 API를 제공합니다. In-Process 및 Out-of-process 캐싱 모두에 대한 통합 API를 제공합니다.

API가 HybridCache 간소화되는 방식을 확인하려면 API를 사용하는 IDistributedCache코드와 비교합니다. 다음은 사용 IDistributedCache 이 어떻게 표시되는지에 대한 예입니다.

public class SomeService(IDistributedCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        var key = $"someinfo:{name}:{id}"; // Unique key for this combination.
        var bytes = await cache.GetAsync(key, token); // Try to get from cache.
        SomeInformation info;
        if (bytes is null)
        {
            // Cache miss; get the data from the real source.
            info = await SomeExpensiveOperationAsync(name, id, token);

            // Serialize and cache it.
            bytes = SomeSerializer.Serialize(info);
            await cache.SetAsync(key, bytes, token);
        }
        else
        {
            // Cache hit; deserialize it.
            info = SomeSerializer.Deserialize<SomeInformation>(bytes);
        }
        return info;
    }

    // This is the work we're trying to cache.
    private async Task<SomeInformation> SomeExpensiveOperationAsync(string name, int id,
        CancellationToken token = default)
    { /* ... */ }
}

직렬화와 같은 작업을 포함하여 매번 제대로 하기 위해 많은 작업이 수행됩니다. 그리고 캐시 누락 시나리오에서는 여러 개의 동시 스레드, 모든 캐시 누락 가져오기, 모든 기본 데이터 가져오기, 모든 직렬화, 해당 데이터를 캐시로 보내는 것으로 끝날 수 있습니다.

이 코드를 HybridCache단순화하고 개선하려면 먼저 새 라이브러리를 추가해야 합니다.Microsoft.Extensions.Caching.Hybrid

<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0" />

구현을 등록하는 HybridCache 것처럼 서비스를 등록합니다.IDistributedCache

builder.Services.AddHybridCache(); // Not shown: optional configuration API.

이제 대부분의 캐싱 문제를 다음으로 HybridCache오프로드할 수 있습니다.

public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // Unique key for this combination.
            async cancel => await SomeExpensiveOperationAsync(name, id, cancel),
            token: token
        );
    }
}

종속성 주입을 HybridCache 통해 추상 클래스의 구체적인 구현을 제공하지만 개발자가 API의 사용자 지정 구현을 제공할 수 있습니다. 구현은 HybridCache 동시 작업 처리를 포함하여 캐싱과 관련된 모든 것을 처리합니다. 여기서 토큰은 cancel 우리가 볼 수 있는 호출자의 취소뿐만 아니라 모든 동시 호출자의 결합된 취소를 나타냅니다. token

캡처된 변수 및 인스턴스별 콜백에서 약간의 오버헤드를 방지하기 위해 패턴을 사용하여 TState 높은 처리량 시나리오를 추가로 최적화할 수 있습니다.

public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync(string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // unique key for this combination
            (name, id), // all of the state we need for the final call, if needed
            static async (state, token) =>
                await SomeExpensiveOperationAsync(state.name, state.id, token),
            token: token
        );
    }
}

HybridCache 는 Redis를 사용하는 등 보조 Out-of-process 캐싱에 대해 구성된 IDistributedCache 구현(있는 경우)을 사용합니다. 그러나 서비스가 없 IDistributedCacheHybridCache 더라도 여전히 프로세스 내 캐싱 및 "스탬피드" 보호를 제공합니다.

개체 재사용에 대한 참고 사항

사용하는 IDistributedCache일반적인 기존 코드에서는 캐시에서 개체를 검색할 때마다 역직렬화가 발생합니다. 이 동작은 각 동시 호출자가 개체의 별도 인스턴스를 가져오며 다른 인스턴스와 상호 작용할 수 없음을 의미합니다. 동일한 개체 인스턴스를 동시에 수정할 위험이 없으므로 스레드 안전성도 발생합니다.

HybridCache 많은 사용량이 기존 IDistributedCache 코드 HybridCache 에서 조정되므로 동시성 버그가 발생하지 않도록 기본적으로 이 동작을 유지합니다. 그러나 지정된 사용 사례는 기본적으로 스레드로부터 안전합니다.

  • 캐시되는 형식을 변경할 수 없는 경우
  • 코드에서 수정하지 않는 경우

이러한 경우 다음을 통해 인스턴스를 다시 사용하는 것이 안전하다는 것을 알릴 HybridCache 수 있습니다.

  • 형식을 .로 표시 sealed합니다. C#의 키워드는 sealed 클래스를 상속할 수 없다는 것을 의미합니다.
  • 특성을 적용합니다 [ImmutableObject(true)] . 이 특성은 [ImmutableObject(true)] 개체를 만든 후에는 개체의 상태를 변경할 수 없다는 것을 나타냅니다.

인스턴스 HybridCache 를 다시 사용하면 호출별 역직렬화와 관련된 CPU 및 개체 할당의 오버헤드를 줄일 수 있습니다. 이렇게 하면 캐시된 개체가 크거나 자주 액세스하는 시나리오에서 성능이 향상될 수 있습니다.

기타 HybridCache 기능

예: IDistributedCacheHybridCache 메서드를 사용하여 키로 제거를 RemoveKeyAsync 지원합니다.

HybridCache또한 할당을 방지하기 byte[] 위해 구현에 대한 IDistributedCache 선택적 API를 제공합니다. 이 기능은 미리 보기 버전의 및 Microsoft.Extensions.Caching.SqlServer 패키지에 Microsoft.Extensions.Caching.StackExchangeRedis 의해 구현됩니다.

직렬화는 서비스 등록의 일부로 구성되며, 호출에서 AddHybridCache 연결된 메서드 및 메서드를 통해 WithSerializer 형식별 및 .WithSerializerFactory 일반화된 직렬 변환기를 지원합니다. 기본적으로 라이브러리는 내부적으로 처리 string 하고 byte[] 다른 모든 항목에 사용 System.Text.Json 하지만 protobuf, xml 또는 기타 모든 항목을 사용할 수 있습니다.

HybridCache 는 .NET Framework 4.7.2 및 .NET Standard 2.0까지 이전 .NET 런타임을 지원합니다.

자세한 HybridCache내용은 ASP.NET Core의 HybridCache 라이브러리를 참조 하세요.

개발자 예외 페이지 개선 사항

ASP.NET Core 개발자 예외 페이지는 개발 중에 앱이 처리되지 않은 예외를 throw할 때 표시됩니다. 개발자 예외 페이지에서는 예외 및 요청에 대한 자세한 정보를 제공합니다.

미리 보기 3에는 개발자 예외 페이지에 엔드포인트 메타데이터가 추가되었습니다. ASP.NET Core는 엔드포인트 메타데이터를 사용하여 라우팅, 응답 캐싱, 속도 제한, OpenAPI 생성 등과 같은 엔드포인트 동작을 제어합니다. 다음 이미지는 개발자 예외 페이지의 섹션에 Routing 있는 새 메타데이터 정보를 보여줍니다.

개발자 예외 페이지의 새 메타데이터 정보

개발자 예외 페이지를 테스트하는 동안 삶의 질이 약간 향상되었습니다. 미리 보기 4에서 제공되었습니다.

  • 텍스트 줄 래핑이 향상되었습니다. 긴 쿠키, 쿼리 문자열 값 및 메서드 이름은 더 이상 가로 브라우저 스크롤 막대를 추가하지 않습니다.
  • 최신 디자인에서 찾을 수있는 더 큰 텍스트.
  • 보다 일관된 테이블 크기입니다.

다음 애니메이션 이미지는 새 개발자 예외 페이지를 보여줍니다.

새 개발자 예외 페이지

사전 디버깅 개선 사항

사전 및 기타 키-값 컬렉션의 디버깅 표시에는 향상된 레이아웃이 있습니다. 키는 값과 연결되지 않고 디버거의 키 열에 표시됩니다. 다음 이미지는 디버거에서 사전의 이전 및 새 표시를 보여 줍니다.

이전:

이전 디버거 환경

이후:

새 디버거 환경

ASP.NET Core에는 많은 키-값 컬렉션이 있습니다. 이 향상된 디버깅 환경은 다음에 적용됩니다.

  • HTTP 헤더
  • 쿼리 문자열
  • 쿠키
  • 데이터 보기
  • 경로 데이터
  • 기능

IIS에서 앱을 재활용하는 동안 503 수정

기본적으로 IIS가 재활용 또는 종료에 대한 알림을 받는 시점과 ANCM이 관리되는 서버에 종료를 시작하라고 지시하는 시점 사이에는 1초의 지연이 있습니다. 지연은 환경 변수를 통해 또는 처리기 설정을 통해 ANCM_shutdownDelay shutdownDelay 구성할 수 있습니다. 두 값 모두 밀리초 단위입니다. 지연은 주로 다음과 같은 경합 가능성을 줄이기 위한 것입니다.

  • IIS는 새 앱으로 이동하라는 요청을 큐에 대기하기 시작하지 않았습니다.
  • ANCM은 이전 앱에 들어오는 새 요청을 거부하기 시작합니다.

CPU 사용량이 더 많은 느린 컴퓨터 또는 컴퓨터는 이 값을 조정하여 503 가능성을 줄일 수 있습니다.

설정 shutdownDelay의 예:

<aspNetCore processPath="dotnet" arguments="myapp.dll" stdoutLogEnabled="false" stdoutLogFile=".logsstdout">
  <handlerSettings>
    <!-- Milliseconds to delay shutdown by.
    this doesn't mean incoming requests will be delayed by this amount,
    but the old app instance will start shutting down after this timeout occurs -->
    <handlerSetting name="shutdownDelay" value="5000" />
  </handlerSettings>
</aspNetCore>

이 수정 사항은 호스팅 번들에서 제공되는 전역적으로 설치된 ANCM 모듈에 있습니다.

정적 웹 자산 배달 최적화

정적 자산을 제공하기 위한 프로덕션 모범 사례에 따라 상당한 양의 작업 및 기술 전문 지식이 필요합니다. 압축, 캐싱 및 지문과 같은 최적화가 없는 경우:

  • 브라우저는 모든 페이지 로드에 대해 추가 요청을 해야 합니다.
  • 필요한 것보다 많은 바이트가 네트워크를 통해 전송됩니다.
  • 경우에 따라 부실 버전의 파일이 클라이언트에 제공됩니다.

성능이 좋은 웹앱을 만들려면 브라우저에 대한 자산 배달을 최적화해야 합니다. 가능한 최적화는 다음과 같습니다.

  • 파일이 변경되거나 브라우저가 캐시를 지울 때까지 지정된 자산을 한 번 제공합니다. ETag 헤더를 설정합니다.
  • 앱이 업데이트된 후 브라우저에서 이전 또는 부실 자산을 사용하지 못하도록 합니다. 마지막으로 수정한 헤더를 설정합니다.
  • 적절한 캐싱 헤더를 설정합니다.
  • 캐싱 미들웨어를 사용합니다.
  • 가능하면 압축된 버전의 자산을 제공합니다.
  • CDN사용하여 사용자에게 더 가까운 자산을 제공합니다.
  • 브라우저에 제공되는 자산의 크기를 최소화합니다. 이 최적화에는 축소가 포함되지 않습니다.

MapStaticAssets 는 앱에서 정적 자산의 배달을 최적화하는 데 도움이 되는 새로운 미들웨어입니다. 페이지 및 MVC를 비롯한 BlazorRazor 모든 UI 프레임워크에서 작동하도록 설계되었습니다. 일반적으로 UseStaticFiles에 대한 드롭인 대체입니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

+app.MapStaticAssets();
-app.UseStaticFiles();
app.MapRazorPages();

app.Run();

MapStaticAssets 는 빌드 및 게시 시간 프로세스를 결합하여 앱의 모든 정적 리소스에 대한 정보를 수집하여 작동합니다. 그런 다음 런타임 라이브러리에서 이 정보를 활용하여 이러한 파일을 브라우저에 효율적으로 제공합니다.

MapStaticAssets 는 대부분의 상황에서 대체할 UseStaticFiles 수 있지만, 앱이 빌드 및 게시 시간에 대한 지식을 가지고 있는 자산을 제공하는 데 최적화되어 있습니다. 앱이 디스크 또는 포함된 리소스 UseStaticFiles 와 같은 다른 위치에서 자산을 제공하는 경우 사용해야 합니다.

MapStaticAssets에서는 다음과 같은 이점을 찾을 수 없습니다.UseStaticFiles

  • 앱의 모든 자산에 대한 빌드 시간 압축:
    • gzip 개발 중 및 gzip + brotli 게시하는 동안
    • 모든 자산은 자산의 크기를 최소로 줄이기 위해 압축됩니다.
  • 콘텐츠 기반ETags: 각 리소스Etags 콘텐츠의 SHA-256 해시의 Base64로 인코딩된 문자열입니다. 이렇게 하면 콘텐츠가 변경된 경우에만 브라우저에서 파일을 다시 다운로드합니다.

다음 표에서는 기본 Razor Pages 템플릿에 있는 CSS 및 파일의 원래 크기 및 JS 압축된 크기를 보여 줍니다.

파일 Original 압축됨 % 감소
bootstrap.min.css 163 17.5 89.26%
jquery.js 89.6 28 68.75%
bootstrap.min.js 78.5 20 74.52%
합계 331.1 65.5 80.20%

다음 표에서는 Fluent UI Blazor 구성 요소 라이브러리사용하여 원래 크기 및 압축된 크기를 보여 줍니다.

파일 Original 압축됨 % 감소
유창한.js 384 73 80.99%
fluent.css 94 11 88.30%
합계 478 84 82.43%

압축되지 않은 총 478KB의 경우 84KB로 압축됩니다.

다음 표에서는 MudBlazorBlazor 구성 요소 라이브러리를 사용하는 원래 및 압축된 크기를 보여 줍니다.

파일 Original 압축됨 축소
MudBlazor.min.css 5:41 37.5 93.07%
MudBlazor.min.js 47.4 9.2 80.59%
합계 588.4 46.7 92.07%

를 사용할 MapStaticAssets때 최적화가 자동으로 수행됩니다. 예를 들어 새 JavaScript 또는 CSS를 사용하여 라이브러리를 추가하거나 업데이트하면 자산이 빌드의 일부로 최적화됩니다. 최적화는 대역폭이 낮거나 신뢰할 수 없는 연결을 가질 수 있는 모바일 환경에 특히 유용합니다.

새 파일 배달 기능에 대한 자세한 내용은 다음 리소스를 참조하세요.

서버에서 동적 압축 사용 및 사용 MapStaticAssets

MapStaticAssets 에는 서버의 동적 압축에 비해 다음과 같은 이점이 있습니다.

  • 서버별 구성이 없으므로 더 간단합니다.
  • 자산이 빌드 시 압축되므로 성능이 더 높습니다.
  • 개발자가 빌드 프로세스 중에 추가 시간을 사용하여 자산이 최소 크기인지 확인할 수 있습니다.

다음 표에서는 MudBlazor 압축을 IIS 동적 압축과 비교하고 MapStaticAssets다음 표를 참조하세요.

IIS gzip MapStaticAssets MapStaticAssets 감소
≅ 90 37.5 59%

ASP0026: [권한 부여]가 [AllowAnonymous]에 의해 "멀리"에서 재정의될 때 경고하는 분석기

특성이 특성을 재정 [AllowAnonymous][Authorize] 하고 권한 부여를 강제하는 것보다 [AllowAnonymous] MVC 작업에 "더 가깝게" 배치된 특성이 직관적으로 보입니다. 그러나 반드시 그렇지는 않습니다. 중요한 것은 특성의 상대적 순서입니다.

다음 코드에서는 멀리 떨어진 특성에 의해 더 가까운 [Authorize] 특성이 재정의 [AllowAnonymous] 되는 예제를 보여 줍니다.

[AllowAnonymous]
public class MyController
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on the class
    public IActionResult Private() => null;
}
[AllowAnonymous]
public class MyControllerAnon : ControllerBase
{
}

[Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
public class MyControllerInherited : MyControllerAnon
{
}

public class MyControllerInherited2 : MyControllerAnon
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
    public IActionResult Private() => null;
}
[AllowAnonymous]
[Authorize] // Overridden by the preceding [AllowAnonymous]
public class MyControllerMultiple : ControllerBase
{
}

.NET 9 Preview 6에서는 MVC 작업에서 멀리 떨어진 특성에 의해 [AllowAnonymous] 더 가까운 [Authorize] 특성이 재정의되는 인스턴스를 강조 표시하는 분석기를 도입했습니다. 경고는 다음 메시지를 사용하여 재정의된 [Authorize] 특성을 가리킵니다.

ASP0026 [Authorize] overridden by [AllowAnonymous] from farther away

이 경고가 표시되면 수행할 올바른 작업은 특성 뒤에 있는 의도에 따라 달라집니다. 의도치 않게 엔드포인트를 익명 사용자에게 노출하는 경우 멀리 떨어진 [AllowAnonymous] 특성을 제거해야 합니다. 특성이 더 가까운 [Authorize] 특성을 재정의 [AllowAnonymous] 하도록 의도된 경우 특성 뒤 [Authorize][AllowAnonymous] 특성을 반복하여 의도를 명확히 할 수 있습니다.

[AllowAnonymous]
public class MyController
{
    // This produces no warning because the second, "closer" [AllowAnonymous]
    // clarifies that [Authorize] is intentionally overridden.
    // Specifying AuthenticationSchemes can still be useful
    // for endpoints that allow but don't require authenticated users.
    [Authorize(AuthenticationSchemes = "Cookies")]
    [AllowAnonymous]
    public IActionResult Privacy() => null;
}

Kestrel 향상된 연결 메트릭

연결이 실패한 이유에 대한 메타데이터를 포함하여 연결 메트릭을 크게 개선 Kestrel했습니다. 이제 메트릭에 kestrel.connection.duration 특성에 연결 닫기 error.type 이유가 포함됩니다.

다음은 값의 작은 샘플입니다.error.type

  • tls_handshake_failed - 연결에 TLS가 필요하며 TLS 핸드셰이크가 실패했습니다.
  • connection_reset - 요청이 진행되는 동안 클라이언트가 예기치 않게 연결을 닫습니다.
  • request_headers_timeout - Kestrel 시간에 따라 요청 헤더를 수신하지 않았기 때문에 연결을 닫습니다.
  • max_request_body_size_exceeded - Kestrel 업로드된 데이터가 최대 크기를 초과하여 연결을 닫습니다.

이전에는 연결 문제를 진단하기 Kestrel 위해 서버에서 자세한 하위 수준 로깅을 기록해야 했습니다. 그러나 로그를 생성하고 저장하는 데 비용이 많이 들 수 있으며 노이즈 중에서 올바른 정보를 찾기가 어려울 수 있습니다.

메트릭은 최소한의 영향으로 프로덕션 환경에 남아 있을 수 있는 훨씬 저렴한 대안입니다. 수집된 메트릭은 대시보드 및 경고를 구동할 수 있습니다. 메트릭을 사용하여 개략적으로 문제가 확인되면 로깅 및 기타 도구를 사용하여 추가 조사를 시작할 수 있습니다.

다음과 같은 여러 시나리오에서 향상된 연결 메트릭이 유용할 것으로 예상됩니다.

  • 짧은 연결 수명으로 인한 성능 문제 조사
  • 성능 및 안정성에 Kestrel 영향을 주는 지속적인 외부 공격을 관찰합니다.
  • '기본 제공 보안 강화에 대한 Kestrel Kestrel외부 공격 시도를 기록하면 방지됩니다.

자세한 내용은 ASP.NET Core 메트릭을 참조 하세요.

명명된 Kestrel 파이프 엔드포인트 사용자 지정

Kestrel'의 명명된 파이프 지원은 고급 사용자 지정 옵션으로 개선되었습니다. 명명된 파이프 옵션의 새 CreateNamedPipeServerStream 메서드를 사용하면 엔드포인트당 파이프를 사용자 지정할 수 있습니다.

이것이 유용한 예는 Kestrel 액세스 보안이 다른 두 개의 파이프 엔드포인트가 필요한 앱입니다. 이 CreateNamedPipeServerStream 옵션은 파이프 이름에 따라 사용자 지정 보안 설정을 사용하여 파이프를 만드는 데 사용할 수 있습니다.

var builder = WebApplication.CreateBuilder();

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenNamedPipe("pipe1");
    options.ListenNamedPipe("pipe2");
});

builder.WebHost.UseNamedPipes(options =>
{
    options.CreateNamedPipeServerStream = (context) =>
    {
        var pipeSecurity = CreatePipeSecurity(context.NamedPipeEndpoint.PipeName);

        return NamedPipeServerStreamAcl.Create(context.NamedPipeEndPoint.PipeName, PipeDirection.InOut,
            NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte,
            context.PipeOptions, inBufferSize: 0, outBufferSize: 0, pipeSecurity);
    };
});

ExceptionHandlerMiddleware 예외 유형에 따라 상태 코드를 선택하는 옵션

앱 개발자는 구성 시 ExceptionHandlerMiddleware 새 옵션을 사용하여 요청 처리 중에 예외가 발생할 때 반환할 상태 코드를 선택할 수 있습니다. 새 옵션은 응답에서 설정 ProblemDetails 되는 상태 코드를 변경합니다 ExceptionHandlerMiddleware.

app.UseExceptionHandler(new ExceptionHandlerOptions
{
    StatusCodeSelector = ex => ex is TimeoutException
        ? StatusCodes.Status503ServiceUnavailable
        : StatusCodes.Status500InternalServerError,
});

특정 엔드포인트 및 요청에서 HTTP 메트릭 옵트아웃

.NET 9에는 특정 엔드포인트 및 요청에 대한 HTTP 메트릭을 옵트아웃하는 기능이 도입되었습니다. 메트릭 기록 옵트아웃은 상태 검사와 같은 자동화된 시스템에서 자주 호출하는 엔드포인트에 유용합니다. 이러한 요청에 대한 메트릭을 기록하는 것은 일반적으로 필요하지 않습니다.

메타데이터를 추가하여 엔드포인트에 대한 HTTP 요청을 메트릭에서 제외할 수 있습니다. 다음 중 하나

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();

var app = builder.Build();
app.MapHealthChecks("/healthz").DisableHttpMetrics();
app.Run();

속성이 MetricsDisabled 추가된 대상은 다음과 같습니다 IHttpMetricsTagsFeature .

  • 요청이 엔드포인트에 매핑되지 않는 고급 시나리오입니다.
  • 특정 HTTP 요청에 대한 메트릭 컬렉션을 동적으로 사용하지 않도록 설정
// Middleware that conditionally opts-out HTTP requests.
app.Use(async (context, next) =>
{
    var metricsFeature = context.Features.Get<IHttpMetricsTagsFeature>();
    if (metricsFeature != null &&
        context.Request.Headers.ContainsKey("x-disable-metrics"))
    {
        metricsFeature.MetricsDisabled = true;
    }

    await next(context);
});

키 삭제를 위한 데이터 보호 지원

.NET 9 이전에는 데이터 손실을 방지하기 위해 데이터 보호 키를 의도적으로 삭제할 수 없었습니다. 키를 삭제하면 보호된 데이터를 복구할 수 없게 됩니다. 크기가 작을 경우 이러한 키의 누적은 일반적으로 최소한의 영향을 주었습니다. 그러나 매우 오래 실행되는 서비스를 수용하기 위해 키를 삭제하는 옵션이 도입되었습니다. 일반적으로 이전 키만 삭제해야 합니다. 스토리지를 절약하는 대가로 데이터 손실 위험을 허용할 수 있는 경우에만 키를 삭제합니다. 데이터 보호 키를 삭제하지 않는 것이 좋습니다.

using Microsoft.AspNetCore.DataProtection.KeyManagement;

var services = new ServiceCollection();
services.AddDataProtection();

var serviceProvider = services.BuildServiceProvider();

var keyManager = serviceProvider.GetService<IKeyManager>();

if (keyManager is IDeletableKeyManager deletableKeyManager)
{
    var utcNow = DateTimeOffset.UtcNow;
    var yearAgo = utcNow.AddYears(-1);

    if (!deletableKeyManager.DeleteKeys(key => key.ExpirationDate < yearAgo))
    {
        Console.WriteLine("Failed to delete keys.");
    }
    else
    {
        Console.WriteLine("Old keys deleted successfully.");
    }
}
else
{
    Console.WriteLine("Key manager does not support deletion.");
}

미들웨어에서 Keyed DI를 지원합니다.

이제 미들웨어는 생성자와 메서드 모두에서 Keyed DIInvoke/InvokeAsync 지원합니다.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<MySingletonClass>("test");
builder.Services.AddKeyedScoped<MyScopedClass>("test2");

var app = builder.Build();
app.UseMiddleware<MyMiddleware>();
app.Run();

internal class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next,
        [FromKeyedServices("test")] MySingletonClass service)
    {
        _next = next;
    }

    public Task Invoke(HttpContext context,
        [FromKeyedServices("test2")]
            MyScopedClass scopedService) => _next(context);
}

Linux에서 ASP.NET Core HTTPS 개발 인증서 신뢰

이제 Ubuntu 및 Fedora 기반 Linux 배포판 dotnet dev-certs https --trust 에서 ASP.NET Core HTTPS 개발 인증서를 신뢰할 수 있는 인증서로 구성합니다.

  • 크롬 브라우저(예: Google Chrome, Microsoft Edge 및 Chromium).
  • Mozilla Firefox 및 Mozilla 파생 브라우저.
  • .NET API(예: HttpClient)

--trust 이전에는 Windows 및 macOS에서만 작업했습니다. 인증서 신뢰는 사용자별로 적용됩니다.

OpenSSL에서 신뢰를 설정하려면 다음 도구를 사용합니다 dev-certs .

  • 인증서를 에 넣습니다. ~/.aspnet/dev-certs/trust
  • 디렉터리에서 OpenSSL의 c_rehash 도구간소화된 버전을 실행합니다.
  • 사용자에게 환경 변수를 업데이트 SSL_CERT_DIR 하도록 요청합니다.

dotnet에 대한 신뢰를 설정하기 위해 도구는 인증서 저장소에 인증서를 My/Root 배치합니다.

NSS 데이터베이스에 대한 신뢰를 설정하기 위해 도구 home 는 디렉터리에서 Firefox 프로필 ~/.pki/nssdb~/snap/chromium/current/.pki/nssdb. 찾은 각 디렉터리에 대해 도구는 항목을 nssdb추가합니다.