ASP.NET Core Blazor 종속성 주입

참고 항목

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

작성자: Rainer StropekMike Rousos

이 문서에서는 Blazor 앱이 서비스를 구성 요소에 삽입하는 방법을 설명합니다.

DI(종속성 주입)는 중앙 위치에 구성된 서비스에 액세스하기 위한 기술입니다.

  • 프레임워크 등록 서비스를 구성 요소에 Razor 직접 삽입할 수 있습니다.
  • Blazor 앱은 사용자 지정 서비스를 정의 및 등록하고 DI를 통해 앱 전체에서 사용하도록 할 수 있습니다.

참고 항목

이 항목을 읽기 전에 ASP.NET Core에서 종속성 주입을 읽는 것이 좋습니다.

이 문서 전체에서 서버 서버/쪽 및 클라이언트/클라이언트 쪽이라는 용어는 앱 코드가 실행되는 위치를 구분하는 데 사용됩니다.

  • 서버/서버 쪽: 웹앱의 Blazor 대화형 서버 쪽 렌더링(대화형 SSR)
  • 클라이언트/클라이언트 쪽
    • 웹앱의 CSR(클라이언트 쪽 렌더링)입니다 Blazor .
    • 앱입니다 Blazor WebAssembly .

설명서 구성 요소 예제는 일반적으로 구성 요소의 정의 파일(.razor)에 지시문을 사용하여 @rendermode 대화형 렌더링 모드를 구성하지 않습니다.

  • Blazor 웹앱에서 구성 요소는 구성 요소의 정의 파일에 적용되거나 부모 구성 요소에서 상속된 대화형 렌더링 모드가 있어야 합니다. 자세한 내용은 ASP.NET Core Blazor 렌더링 모드를 참조하세요.

  • 독립 실행형 Blazor WebAssembly 앱에서 구성 요소는 표시된 대로 작동하며 구성 요소가 항상 앱의 WebAssembly에서 대화형으로 실행되므로 렌더링 모드가 Blazor WebAssembly 필요하지 않습니다.

Interactive WebAssembly 또는 대화형 자동 렌더링 모드를 사용하는 경우 클라이언트로 전송된 구성 요소 코드를 디컴파일하고 검사할 수 있습니다. 프라이빗 코드, 앱 비밀 또는 기타 중요한 정보를 클라이언트 렌더링 구성 요소에 배치하지 마세요.

  • 서버/서버 쪽
    • Server 호스트 Blazor WebAssembly 된 앱의 프로젝트입니다.
    • 앱입니다 Blazor Server .
  • 클라이언트/클라이언트 쪽
    • Client 호스트 Blazor WebAssembly 된 앱의 프로젝트입니다.
    • 앱입니다 Blazor WebAssembly .

파일 및 폴더의 용도 및 위치에 대한 지침은 ASP.NET Core Blazor 프로젝트 구조를 참조하세요. 이 구조에서는 시작 스크립트의 Blazor 위치와 콘텐츠<body><head> 위치도 설명합니다.

데모 코드를 실행하는 가장 좋은 방법은 대상으로 하는 .NET 버전과 일치하는 샘플 GitHub 리포지토리에서Blazor샘플 앱을 다운로드 BlazorSample_{PROJECT TYPE} 하는 것입니다. 현재 모든 설명서 예제가 샘플 앱에 있는 것은 아니지만 대부분의 .NET 8 문서 예제를 .NET 8 샘플 앱으로 이동하기 위한 노력이 현재 진행 중입니다. 이 작업은 2024년 1분기에 완료될 예정입니다.

기본 서비스

다음 표에 표시된 서비스는 일반적으로 Blazor 앱에서 사용됩니다.

서비스 수명(lifetime) 설명
HttpClient Scoped

URI로 식별되는 리소스에서 HTTP 요청을 보내고 HTTP 응답을 받기 위한 메서드를 제공합니다.

클라이언트 쪽에서 인스턴스는 파일의 HttpClient 앱에 Program 의해 등록되고 브라우저를 사용하여 백그라운드에서 HTTP 트래픽을 처리합니다.

서버 쪽은 HttpClient 기본적으로 서비스로 구성되지 않습니다. 서버 쪽 코드에서 .HttpClient

자세한 내용은 ASP.NET Core Blazor 앱에서 웹 API 호출을 참조하세요.

HttpClient가 싱글톤이 아닌 범위가 지정된 서비스로 등록됩니다. 자세한 내용은 서비스 수명 섹션을 참조하세요.

IJSRuntime

클라이언트 쪽: 싱글톤

서버 쪽: 범위 지정

Blazor 프레임워크는 앱의 서비스 컨테이너에 IJSRuntime를 등록합니다.

JavaScript 호출이 디스패치되는 JavaScript 런타임의 인스턴스를 나타냅니다. 자세한 내용은 ASP.NET Core Blazor의 .NET 메서드에서 JavaScript 함수 호출을 참조하세요.

서버의 싱글톤 서비스에 서비스를 삽입하려는 경우 다음 방법 중 하나를 수행합니다.

  • 서비스 등록을 IJSRuntime의 등록과 일치하는 범위로 변경합니다. 이는 서비스가 사용자별 상태를 처리하는 경우에 적합합니다.
  • IJSRuntime을 싱글톤에 삽입하는 대신 싱글톤 서비스의 구현에 메서드 호출의 인수로 전달합니다.
NavigationManager

클라이언트 쪽: 싱글톤

서버 쪽: 범위 지정

Blazor 프레임워크는 앱의 서비스 컨테이너에 NavigationManager를 등록합니다.

URI 및 탐색 상태를 사용하기 위한 도우미를 포함합니다. 자세한 내용은 URI 및 탐색 상태 도우미를 참조하세요.

Blazor 프레임워크에 의해 등록된 추가 서비스는 구성 및 로깅과 같은 Blazor 기능을 설명하는 데 사용되는 설명서에 설명되어 있습니다.

사용자 지정 서비스 공급자는 테이블에 나열된 기본 서비스를 자동으로 제공하지 않습니다. 사용자 지정 서비스 공급자를 사용하고 표에 표시된 서비스가 필요한 경우 새 서비스 공급자에 필요한 서비스를 추가합니다.

클라이언트 쪽 서비스 추가

파일에서 앱의 서비스 컬렉션에 대한 서비스를 구성합니다 Program . 다음 예제에서는 ExampleDependency 구현을 IExampleDependency에 등록합니다.

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
...

await builder.Build().RunAsync();

호스트를 빌드한 후 구성 요소를 렌더링하기 전에 루트 DI 범위에서 서비스를 사용할 수 있습니다. 이러한 기능은 콘텐츠를 렌더링하기 전에 초기화 논리를 실행하는 데 유용할 수 있습니다.

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...

var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync();

await host.RunAsync();

호스트는 앱의 중앙 구성 인스턴스를 제공합니다. 이전 예제를 기준으로 작성한 날씨 서비스 URL은 기본 구성 원본(예: appsettings.json)에서 InitializeWeatherAsync로 전달됩니다.

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...

var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync(
    host.Configuration["WeatherServiceUrl"]);

await host.RunAsync();

서버 쪽 서비스 추가

새 앱을 만든 후 Program 파일의 일부를 검사합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();

builder 변수는 서비스 설명자 개체 목록인 IServiceCollection이 있는 WebApplicationBuilder를 나타냅니다. 서비스 설명자를 서비스 컬렉션에 제공하여 서비스를 추가합니다. 다음 예제에서는 IDataAccess 인터페이스와 해당 구체적 구현 DataAccess를 통해 이러한 개념을 보여 줍니다.

builder.Services.AddSingleton<IDataAccess, DataAccess>();

새 앱을 만든 후 Startup.ConfigureServices에서 Startup.cs 메서드를 검사합니다.

using Microsoft.Extensions.DependencyInjection;

...

public void ConfigureServices(IServiceCollection services)
{
    ...
}

ConfigureServices 메서드는 서비스 설명자 개체 목록에 해당하는 IServiceCollection에 전달됩니다. 서비스 설명자를 서비스 컬렉션에 제공함으로써 서비스가 ConfigureServices 메서드에 추가됩니다. 다음 예제에서는 IDataAccess 인터페이스와 해당 구체적 구현 DataAccess를 통해 이러한 개념을 보여 줍니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDataAccess, DataAccess>();
}

일반 서비스 등록

하나 이상의 일반적인 서비스가 클라이언트 및 서버 쪽에 필요한 경우 메서드 클라이언트 쪽에 공통 서비스 등록을 배치하고 메서드를 호출하여 두 프로젝트에 서비스를 등록할 수 있습니다.

먼저 공통 서비스 등록을 별도의 메서드로 팩터링합니다. 예를 들어 메서드 클라이언트 쪽을 ConfigureCommonServices 만듭니다.

public static void ConfigureCommonServices(IServiceCollection services)
{
    services.Add...;
}

클라이언트 쪽 Program 파일의 경우 공통 서비스를 등록하기 위해 호출 ConfigureCommonServices 합니다.

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

서버 쪽 Program 파일에서 공통 서비스를 등록하기 위해 호출 ConfigureCommonServices 합니다.

var builder = WebApplication.CreateBuilder(args);

...

Client.Program.ConfigureCommonServices(builder.Services);

이 방법의 예제는 ASP.NET Core Blazor WebAssembly 추가 보안 시나리오를 참조하세요.

미리 렌더링하는 동안 실패하는 클라이언트 쪽 서비스

이 섹션은 Web Apps의 WebAssembly 구성 요소에 Blazor 만 적용됩니다.

Blazor Web Apps는 일반적으로 클라이언트 쪽 WebAssembly 구성 요소를 미리 렌더링합니다. 프로젝트에 등록된 .Client 필수 서비스만 사용하여 앱을 실행하는 경우 구성 요소가 미리 렌더링하는 동안 필요한 서비스를 사용하려고 할 때 다음과 유사한 런타임 오류가 발생합니다.

InvalidOperationException: '{ASSEMBLY}} 형식에서 {PROPERTY}에 대한 값을 제공할 수 없습니다. Client.Pages. {COMPONENT NAME}'. '{SERVICE}' 형식의 등록된 서비스가 없습니다.

다음 방법 중 하나를 사용하여 이 문제를 해결합니다.

  • 구성 요소를 미리 렌더링하는 동안 사용할 수 있도록 기본 프로젝트에 서비스를 등록합니다.
  • 구성 요소에 사전 렌더링이 필요하지 않은 경우 ASP.NET Core Blazor 렌더링 모드의 지침에 따라 미리 렌더링을 사용하지 않도록 설정합니다. 이 방법을 채택하는 경우 기본 프로젝트에 서비스를 등록할 필요가 없습니다.

자세한 내용은 미리 렌더링하는 동안 클라이언트 쪽 서비스가 확인되지 않습니다.

서비스 수명

다음 표에 표시된 수명으로 서비스를 구성할 수 있습니다.

수명(lifetime) 설명
Scoped

클라이언트 쪽에는 현재 DI 범위의 개념이 없습니다. Scoped 등록 서비스는 Singleton 서비스처럼 동작합니다.

서버 쪽 개발은 Scoped HTTP 요청에서 수명을 지원하지만 클라이언트에 로드된 구성 요소 간의 연결/회로 메시지에서는 SignalR 지원되지 않습니다. Razor 앱의 Pages 또는 MVC 부분은 범위가 지정된 서비스를 정상적으로 처리하고 페이지 또는 뷰 간 또는 페이지 또는 보기에서 구성 요소로 이동할 때 각 HTTP 요청에서 서비스를 다시 만듭니다. 클라이언트의 구성 요소 사이를 이동할 때는 범위가 지정된 서비스가 다시 구성되지 않습니다. 이 경우 서버와의 통신은 HTTP 요청을 통하지 않고 사용자 회로의 SignalR 연결을 통해 이루어집니다. 클라이언트에서 다음과 같은 구성 요소 시나리오에서는 사용자를 위해 새 회로가 만들어지므로 범위가 지정된 서비스가 다시 구성됩니다.

  • 사용자가 브라우저의 창을 닫습니다. 사용자가 새 창을 열고 앱으로 다시 이동합니다.
  • 사용자가 브라우저 창에서 앱의 탭을 닫습니다. 사용자가 새 탭을 열고 앱으로 다시 이동합니다.
  • 사용자가 브라우저의 다시 로드/새로 고침 단추를 선택합니다.

서버 쪽 앱에서 사용자 상태를 유지하는 방법에 대한 자세한 내용은 ASP.NET Core Blazor 상태 관리를 참조하세요.

Singleton DI는 서비스의 단일 인스턴스를 만듭니다. Singleton 서비스가 필요한 모든 구성 요소는 서비스의 같은 인스턴스를 수신합니다.
Transient 구성 요소는 서비스 컨테이너에서 Transient 서비스의 인스턴스를 가져올 때마다 서비스의 새 인스턴스을 받습니다.

DI 시스템은 ASP.NET Core에서 DI 시스템을 기준으로 합니다. 자세한 내용은 ASP.NET Core에서 종속성 주입을 참조하세요.

구성 요소에서 서비스 요청

서비스가 서비스 컬렉션에 추가된 후 다음 두 매개 변수가 있는 지시문을 사용하여 @injectRazor 구성 요소에 서비스를 삽입합니다.

  • 형식: 삽입할 서비스의 형식입니다.
  • 속성: 삽입된 앱 서비스를 수신하는 속성의 이름입니다. 이 속성은 수동으로 만들 필요가 없습니다. 컴파일러에서 속성을 만들기 때문입니다.

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

여러 @inject 문을 사용하여 여러 서비스를 주입합니다.

다음 예제에서는 @inject를 사용하는 방법을 보여 줍니다. Services.IDataAccess를 구현하는 서비스는 구성 요소의 속성 DataRepository에 주입됩니다. 코드가 IDataAccess 추상화만 사용하는 방식에 유의하세요.

@page "/the-sunmakers"
@inject IDataAccess DataRepository

<PageTitle>The Sunmakers</PageTitle>

<h1>Doctor Who®: The Sunmakers Actors (Villains)</h1>

@if (actors != null)
{
    <ul>
        @foreach (var actor in actors)
        {
            <li>@actor.FirstName @actor.LastName</li>
        }
    </ul>
}

<a href="https://www.doctorwho.tv">Doctor Who</a>  is a
    registered trademark of the <a href="https://www.bbc.com/">BBC</a>. 
<a href="https://www.doctorwho.tv/stories/the-sunmakers">The Sunmakers</a>

@code {
    private IReadOnlyList<Actor>? actors;

    protected override async Task OnInitializedAsync()
    {
        actors = await DataRepository.GetAllActorsAsync();
    }

    public class Actor
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }

    public interface IDataAccess
    {
        public Task<IReadOnlyList<Actor>> GetAllActorsAsync();
    }

    public class DataAccess : IDataAccess
    {
        public Task<IReadOnlyList<Actor>> GetAllActorsAsync() => 
            Task.FromResult(GetActors());
    }

    /*
     * Register the service in Program.cs:
     * using static BlazorSample.Components.Pages.TheSunmakers;
     * builder.Services.AddScoped<IDataAccess, DataAccess>();
    */

    public static IReadOnlyList<Actor> GetActors()
    {
        return new Actor[]
        {
           new() { FirstName = "Henry", LastName = "Woolf" },
           new() { FirstName = "Jonina", LastName = "Scott" },
           new() { FirstName = "Richard", LastName = "Leech" }
        };
    }
}
@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)
{
    <ul>
        @foreach (var customer in customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Customer>? customers;

    protected override async Task OnInitializedAsync()
    {
        customers = await DataRepository.GetAllCustomersAsync();
    }

    private class Customer
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }

    private interface IDataAccess
    {
        public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
    }
}
@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)
{
    <ul>
        @foreach (var customer in customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Customer>? customers;

    protected override async Task OnInitializedAsync()
    {
        customers = await DataRepository.GetAllCustomersAsync();
    }

    private class Customer
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }

    private interface IDataAccess
    {
        public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
    }
}
@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)
{
    <ul>
        @foreach (var customer in customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Customer> customers;

    protected override async Task OnInitializedAsync()
    {
        customers = await DataRepository.GetAllCustomersAsync();
    }

    private class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    private interface IDataAccess
    {
        public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
    }
}
@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)
{
    <ul>
        @foreach (var customer in customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Customer> customers;

    protected override async Task OnInitializedAsync()
    {
        customers = await DataRepository.GetAllCustomersAsync();
    }

    private class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    private interface IDataAccess
    {
        public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
    }
}

내부적으로 생성된 속성(DataRepository)은 [Inject] 특성을 사용합니다. 일반적으로 이 특성은 직접 사용되지 않습니다. 구성 요소에 기본 클래스가 필요하고 기본 클래스에 주입된 속성도 필요하면 [Inject] 특성을 수동으로 추가합니다.

using Microsoft.AspNetCore.Components;

public class ComponentBase : IComponent
{
    [Inject]
    protected IDataAccess DataRepository { get; set; } = default!;

    ...
}

참고 항목

삽입된 서비스를 사용할 수 있어야 하므로 null-forgiving 연산자(default!)가 있는 기본 리터럴이 .NET 6 이상에 할당됩니다. 자세한 내용은 NRT(Nullable 참조 형식) 및 .NET 컴파일러 null 상태 정적 분석을 참조하세요.

기본 클래스에서 파생된 구성 요소에서는 @inject 지시문이 필요하지 않습니다. InjectAttribute 기본 클래스로 충분하며 필요한 모든 구성 요소는 지시문입니다@inherits.

@page "/demo"
@inherits ComponentBase

<h1>Demo Component</h1>

서비스에서 DI 사용

복잡한 서비스에는 추가 서비스가 필요할 수 있습니다. 다음 예제에서는 DataAccessHttpClient 기본 서비스가 필요합니다. @inject(또는 [Inject] 특성)는 서비스에서 사용할 수 없습니다. 대신 생성자 주입을 사용해야 합니다. 서비스의 생성자에 매개 변수를 추가하여 필요한 서비스를 추가합니다. DI는 서비스를 만들 때 생성자에 필요한 서비스를 인식하고 적절히 제공합니다. 다음 예제에서 생성자는 DI를 통해 HttpClient를 받습니다. HttpClient는 기본 서비스입니다.

using System.Net.Http;

public class DataAccess : IDataAccess
{
    public DataAccess(HttpClient http)
    {
        ...
    }
}

생성자 주입의 필수 조건:

  • 모든 인수를 DI에서 처리할 수 있는 생성자가 하나 있어야 합니다. DI에서 다루지 않는 추가 매개 변수는 기본값이 지정되면 허용됩니다.
  • 적용 가능한 생성자는 public이어야 합니다.
  • 적용 가능한 생성자가 하나 있어야 합니다. 모호한 경우 시 DI는 예외를 throw합니다.

구성 요소에 키 입력 서비스 삽입

Blazor 는 특성을 사용하여 키 지정된 서비스 삽입을 [Inject] 지원합니다. 키는 종속성 주입을 사용할 때 등록 및 서비스 사용 범위를 허용합니다. 속성을 InjectAttribute.Key 사용하여 삽입할 서비스에 대한 키를 지정합니다.

[Inject(Key = "my-service")]
public IMyService MyService { get; set; }

DI 범위를 관리 하는 유틸리티 기본 구성 요소 클래스

비 ASP.NETBlazor Core 앱에서 범위 및 임시 서비스는 일반적으로 현재 요청으로 범위가 지정됩니다. 요청이 완료되면 DI 시스템에서 범위 및 임시 서비스를 삭제합니다.

대화형 서버 쪽 Blazor 앱에서 DI 범위는 회로 기간( SignalR 클라이언트와 서버 간의 연결)동안 지속되며, 이로 인해 범위가 지정되고 일회용 임시 서비스가 단일 구성 요소의 수명보다 훨씬 오래 지속될 수 있습니다. 따라서 서비스 수명이 구성 요소의 수명과 일치하도록 하려는 경우 범위가 지정된 서비스를 구성 요소에 직접 삽입하지 마세요. 구현 IDisposable 하지 않는 구성 요소에 삽입된 임시 서비스는 구성 요소가 삭제될 때 가비지 수집됩니다. 그러나 구현 IDisposable 하는 주입된 일시적 서비스는 회로의 수명 동안 DI 컨테이너에 의해 기본 유지되므로 구성 요소가 삭제될 때 서비스 가비지 수집을 방지하고 메모리 누수를 발생합니다. 형식을 기반으로 OwningComponentBase 하는 범위가 지정된 서비스에 대한 대체 방법은 이 섹션의 뒷부분에서 설명하며, 삭제 가능한 일시적 서비스는 전혀 사용하지 않아야 합니다. 자세한 내용은 임시 일회용 Blazor Serverdotnet/aspnetcore (#26676)을 해결하기 위한 디자인을 참조하세요.

회로를 통해 작동하지 않는 클라이언트 쪽 Blazor 앱에서도 범위가 지정된 수명에 등록된 서비스는 싱글톤으로 처리되므로 일반적인 ASP.NET Core 앱에서 범위가 지정된 서비스보다 더 오래 살고 있습니다. 또한 클라이언트 쪽 일회용 임시 서비스는 삭제 가능한 서비스에 대한 참조를 보유하는 DI 컨테이너가 앱의 수명 동안 유지되어 서비스에서 가비지 수집을 방지하기 때문에 삽입되는 구성 요소보다 더 오래 유지됩니다. 수명이 긴 일회용 임시 서비스는 서버에서 더 큰 관심사이지만 클라이언트 서비스 등록으로도 피해야 합니다. 서비스 수 OwningComponentBase 명을 제어하기 위해 클라이언트 쪽 범위의 서비스에도 이 형식을 사용하는 것이 좋으며 일회용 임시 서비스를 전혀 사용하지 않아야 합니다.

서비스 수명을 제한하는 방법은 형식을 사용하는 것입니다 OwningComponentBase . OwningComponentBase는 구성 요소의 수명에 해당하는 DI 범위를 만드는 추 ComponentBase 상 형식입니다. 이 범위를 사용하여 구성 요소는 범위가 지정된 수명을 가진 서비스를 삽입하고 구성 요소만큼 오래 사용할 수 있도록 할 수 있습니다. 구성 요소가 제거되면 구성 요소 범위 지정 서비스 공급자의 서비스도 삭제됩니다. 이는 구성 요소 내에서 다시 사용되지만 구성 요소 간에 공유되지 않는 서비스에 유용할 수 있습니다.

두 버전의 OwningComponentBase 형식을 사용할 수 있으며 다음 두 섹션에 설명되어 있습니다.

OwningComponentBase

OwningComponentBaseIServiceProvider 형식의 보호된 ScopedServices 속성을 사용하여 ComponentBase 형식의 삭제 가능한 추상 자식입니다. 이 공급자는 구성 요소의 수명으로 범위가 지정된 서비스를 확인하는 데 사용할 수 있습니다.

@inject 또는 [Inject] 특성을 사용하여 구성 요소에 주입된 DI 서비스는 구성 요소의 범위에 만들어지지 않습니다. 구성 요소의 범위를 사용하려면 ScopedServicesGetRequiredService 또는 GetService과 사용하여 서비스를 확인해야 합니다. ScopedServices 공급자를 사용하여 확인된 모든 서비스에는 구성 요소의 범위에서 종속성이 제공됩니다.

다음 예제에서는 범위가 지정된 서비스를 직접 삽입하는 것과 서버에서 사용하는 ScopedServices 서비스 확인 간의 차이점을 보여 줍니다. 시간 이동 클래스에 대한 다음 인터페이스 및 구현에는 DateTime 값을 보유하는 DT 속성이 포함됩니다. 구현은 DateTime.Now를 호출하여 TimeTravel 클래스가 인스턴스화될 때 DT를 설정합니다.

ITimeTravel.cs:

public interface ITimeTravel
{
    public DateTime DT { get; set; }
}

TimeTravel.cs:

public class TimeTravel : ITimeTravel
{
    public DateTime DT { get; set; } = DateTime.Now;
}

서비스는 서버 쪽 Program 파일에서 범위로 등록됩니다. 범위가 지정된 서버 쪽 서비스는 회로 기간과 동일한 수명을 갖습니다.

Program 파일에서:

builder.Services.AddScoped<ITimeTravel, TimeTravel>();

다음은 TimeTravel 구성 요소에 대한 설명입니다.

  • 시간 이동 서비스는 @inject과 함께 TimeTravel1로 직접 삽입됩니다.
  • 또한 서비스는 ScopedServicesGetRequiredService을 사용하여 TimeTravel2과 별도로 확인됩니다.

TimeTravel.razor:

@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase

<h1><code>OwningComponentBase</code> Example</h1>

<ul>
    <li>TimeTravel1.DT: @TimeTravel1?.DT</li>
    <li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>

@code {
    private ITimeTravel TimeTravel2 { get; set; } = default!;

    protected override void OnInitialized()
    {
        TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
    }
}
@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase

<h1><code>OwningComponentBase</code> Example</h1>

<ul>
    <li>TimeTravel1.DT: @TimeTravel1?.DT</li>
    <li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>

@code {
    private ITimeTravel TimeTravel2 { get; set; } = default!;

    protected override void OnInitialized()
    {
        TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
    }
}

처음에 TimeTravel 구성 요소로 이동하며, 구성 요소가 로드될 때 시간 이동 서비스가 두 번 인스턴스화되고, TimeTravel1TimeTravel2이 동일한 초기 값을 갖습니다.

TimeTravel1.DT: 8/31/2022 2:54:45 PM
TimeTravel2.DT: 8/31/2022 2:54:45 PM

TimeTravel 구성 요소에서 다른 구성 요소로 이동하고 TimeTravel 구성 요소로 다시 이동하는 경우:

  • TimeTravel1은 구성 요소가 처음 로드될 때 생성된 동일한 서비스 인스턴스가 제공되므로 DT 값이 동일하게 유지됩니다.
  • TimeTravel2는 새 ITimeTravel 서비스 인스턴스를 TimeTravel2에 새 DT 값으로 가져옵니다.

TimeTravel1.DT: 8/31/2022 2:54:45 PM
TimeTravel2.DT: 8/31/2022 2:54:48 PM

TimeTravel1은 사용자의 회로에 연결됩니다. 이는 그대로 유지되며 기본 회로가 분해될 때까지 삭제되지 않습니다. 예를 들어 끊어진 회로 보존 기간동안 회로 연결이 끊어진 경우 서비스가 삭제됩니다.

파일의 범위가 지정된 서비스 등록 Program 및 사용자 회로 TimeTravel2 의 수명에도 불구하고 구성 요소가 초기화될 때마다 새 ITimeTravel 서비스 인스턴스를 받습니다.

OwningComponentBase<TService>

OwningComponentBase<TService>OwningComponentBase에서 파생되고 범위가 지정된 DI 공급자에서 T의 인스턴스를 반환하는 Service 속성을 추가합니다. 이 형식은 구성 요소의 범위를 사용하여 DI 컨테이너에서 앱에 필요한 기본 서비스가 하나 있는 경우 IServiceProvider 인스턴스를 사용하지 않고 범위 지정 서비스에 액세스할 수 있는 편리한 방법입니다. ScopedServices 속성을 사용할 수 있으므로 필요한 경우 앱에서 다른 형식의 서비스를 가져올 수 있습니다.

@page "/users"
@attribute [Authorize]
@inherits OwningComponentBase<AppDbContext>

<h1>Users (@Service.Users.Count())</h1>

<ul>
    @foreach (var user in Service.Users)
    {
        <li>@user.UserName</li>
    }
</ul>

클라이언트 쪽 임시 삭제 가능 항목 검색

사용자 지정 코드를 클라이언트 쪽 Blazor 앱에 추가하여 사용해야 OwningComponentBase하는 앱에서 삭제 가능한 일시적 서비스를 검색할 수 있습니다. 이 방법은 나중에 앱에 추가된 코드가 라이브러리에서 추가된 서비스를 포함하여 하나 이상의 일시적인 삭제 가능한 서비스를 사용하는 것을 우려하는 경우에 유용합니다. 데모 코드는 샘플 GitHub 리포지토리에서 Blazor 사용할 수 있습니다.

샘플의 .NET 6 이상 버전에서 다음을 BlazorSample_WebAssembly 검사합니다.

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransientDisposableService.cs
  • In Program.cs:
    • 앱의 Services 네임스페이스는 파일(using BlazorSample.Services;)의 맨 위에 제공됩니다.
    • DetectIncorrectUsageOfTransients는 .에서 WebAssemblyHostBuilder.CreateDefault할당된 builder 직후에 호출됩니다.
    • 등록 TransientDisposableService 됨(builder.Services.AddTransient<TransientDisposableService>();)입니다.
    • EnableTransientDisposableDetection 는 앱(host.EnableTransientDisposableDetection();)의 처리 파이프라인에서 빌드된 호스트에서 호출됩니다.
  • 앱은 예외를 TransientDisposableService throw하지 않고 서비스를 등록합니다. 그러나 서비스를 해결하려고 하면 프레임워크가 TransientService.razor 인스턴스TransientDisposableService를 생성하려고 할 때 throw InvalidOperationException 됩니다.

서버 쪽 일시적 삭제 가능 항목 검색

사용자 지정 코드를 서버 쪽 Blazor 앱에 추가하여 사용해야 OwningComponentBase하는 앱에서 서버 쪽 일회용 임시 서비스를 검색할 수 있습니다. 이 방법은 나중에 앱에 추가된 코드가 라이브러리에서 추가된 서비스를 포함하여 하나 이상의 일시적인 삭제 가능한 서비스를 사용하는 것을 우려하는 경우에 유용합니다. 데모 코드는 샘플 GitHub 리포지토리에서 Blazor 사용할 수 있습니다.

샘플의 .NET 8 이상 버전에서 다음을 BlazorSample_BlazorWebApp 검사합니다.

샘플의 .NET 6 또는 .NET 7 버전에서 다음을 검사합니다 BlazorSample_Server .

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransitiveTransientDisposableDependency.cs:
  • In Program.cs:
    • 앱의 Services 네임스페이스는 파일(using BlazorSample.Services;)의 맨 위에 제공됩니다.
    • DetectIncorrectUsageOfTransients 는 호스트 작성기(builder.DetectIncorrectUsageOfTransients();)에서 호출됩니다.
    • TransientDependency 서비스가 등록됩니다(builder.Services.AddTransient<TransientDependency>();).
    • (TransitiveTransientDisposableDependency)에 대해 ITransitiveTransientDisposableDependencybuilder.Services.AddTransient<ITransitiveTransientDisposableDependency, TransitiveTransientDisposableDependency>();등록됩니다.
  • 앱은 예외를 TransientDependency throw하지 않고 서비스를 등록합니다. 그러나 서비스를 해결하려고 하면 프레임워크가 TransientService.razor 인스턴스TransientDependency를 생성하려고 할 때 throw InvalidOperationException 됩니다.

처리기에 대한 IHttpClientFactory/HttpClient 임시 서비스 등록

IHttpClientFactory/HttpClient 처리기에 대한 임시 서비스 등록을 권장합니다. 앱에 처리기가 포함되어 IHttpClientFactory/HttpClient 있고 인증에 대한 지원을 추가하는 데 사용하는 IRemoteAuthenticationBuilder<TRemoteAuthenticationState,TAccount> 경우 클라이언트 쪽 인증에 대한 다음과 같은 임시 삭제 가능 항목도 검색되며 이는 예상되며 무시될 수 있습니다.

다른 인스턴스 IHttpClientFactory/HttpClient 도 검색됩니다. 이러한 인스턴스는 무시될 수도 있습니다.

샘플 GitHub 리포지토리의Blazor샘플 앱은 Blazor 일시적인 삭제 가능 개체를 검색하는 코드를 보여 줍니다. 그러나 샘플 앱에 처리기가 포함되어 IHttpClientFactory/HttpClient 있으므로 코드가 비활성화됩니다.

데모 코드를 활성화하고 해당 작업을 감시하려면 다음을 수행합니다.

  • 에서 임시 일회용 줄의 주석 처리를 제거합니다 Program.cs.

  • 구성 요소가 앱의 탐색 사이드바에 NavLink.razor 표시되지 않도록 TransientService 하는 조건부 검사 제거합니다.

    - else if (name != "TransientService")
    + else
    
  • 샘플 앱을 실행하고 .에서 /transient-service구성 요소로 이동합니다TransientService.

DI에서 EF Core(Entity Framework Core) DbContext 사용

자세한 내용은 EF Core(Entity Framework Core)를 사용한 ASP.NET Core Blazor를 참조하세요.

다른 DI 범위에서 서버 쪽 Blazor 서비스에 액세스

회로 작업 처리기는 사용 하 여 IHttpClientFactory만든 범위와 같은 다른 비 종Blazor속성 주입 (DI) 범위에서 범위 Blazor 서비스에 액세스 하는 방법을 제공 합니다.

.NET 8에서 ASP.NET Core를 릴리스하기 전에 사용자 지정 기본 구성 요소 유형을 사용하는 데 필요한 다른 종속성 주입 범위에서 회로 범위 서비스에 액세스합니다. 회로 작업 처리기를 사용하는 경우 다음 예제와 같이 사용자 지정 기본 구성 요소 형식이 필요하지 않습니다.

public class CircuitServicesAccessor
{
    static readonly AsyncLocal<IServiceProvider> blazorServices = new();

    public IServiceProvider? Services
    {
        get => blazorServices.Value;
        set => blazorServices.Value = value;
    }
}

public class ServicesAccessorCircuitHandler : CircuitHandler
{
    readonly IServiceProvider services;
    readonly CircuitServicesAccessor circuitServicesAccessor;

    public ServicesAccessorCircuitHandler(IServiceProvider services, 
        CircuitServicesAccessor servicesAccessor)
    {
        this.services = services;
        this.circuitServicesAccessor = servicesAccessor;
    }

    public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
        Func<CircuitInboundActivityContext, Task> next)
    {
        return async context =>
        {
            circuitServicesAccessor.Services = services;
            await next(context);
            circuitServicesAccessor.Services = null;
        };
    }
}

public static class CircuitServicesServiceCollectionExtensions
{
    public static IServiceCollection AddCircuitServicesAccessor(
        this IServiceCollection services)
    {
        services.AddScoped<CircuitServicesAccessor>();
        services.AddScoped<CircuitHandler, ServicesAccessorCircuitHandler>();

        return services;
    }
}

필요한 위치를 주입하여 CircuitServicesAccessor 회로 범위 서비스에 액세스합니다.

설치를 사용하여 IHttpClientFactory설정에서 DelegatingHandler 액세스 AuthenticationStateProvider 하는 방법을 보여 주는 예제는 서버 쪽 ASP.NET Core Blazor 추가 보안 시나리오를 참조하세요.

Razor 구성 요소가 다른 DI 범위에서 코드를 실행하는 비동기 메서드를 호출하는 경우가 있을 수 있습니다. 올바른 접근 방식이 없으면, 이러한 DI 범위는 IJSRuntimeMicrosoft.AspNetCore.Components.Server.ProtectedBrowserStorage와 같은 Blazor의 서비스에 액세스할 수 없습니다.

예를 들어, IHttpClientFactory를 사용하여 만든 HttpClient 인스턴스에는 자체 DI 서비스 범위가 있습니다. 따라서, HttpClient에 구성된 HttpMessageHandler 인스턴스는 Blazor 서비스를 직접 삽입할 수 없습니다.

현재 비동기 컨텍스트에 대한 BlazorIServiceProvider를 저장하는 AsyncLocal를 정의하는 클래스 BlazorServiceAccessor를 만듭니다. 다른 DI 서비스 범위 내에서 BlazorServiceAcccessor 인스턴스를 획득하여 Blazor 서비스에 액세스할 수 있습니다.

BlazorServiceAccessor.cs:

internal sealed class BlazorServiceAccessor
{
    private static readonly AsyncLocal<BlazorServiceHolder> s_currentServiceHolder = new();

    public IServiceProvider? Services
    {
        get => s_currentServiceHolder.Value?.Services;
        set
        {
            if (s_currentServiceHolder.Value is { } holder)
            {
                // Clear the current IServiceProvider trapped in the AsyncLocal.
                holder.Services = null;
            }

            if (value is not null)
            {
                // Use object indirection to hold the IServiceProvider in an AsyncLocal
                // so it can be cleared in all ExecutionContexts when it's cleared.
                s_currentServiceHolder.Value = new() { Services = value };
            }
        }
    }

    private sealed class BlazorServiceHolder
    {
        public IServiceProvider? Services { get; set; }
    }
}

async 구성 요소 메서드가 호출될 때 BlazorServiceAccessor.Services의 값을 자동으로 설정하려면, 세 가지 기본 비동기 진입점을 Razor 구성 요소 코드로 다시 구현하는 사용자 지정 기본 구성 요소를 만듭니다.

다음 클래스는 기본 구성 요소에 대한 구현을 보여줍니다.

CustomComponentBase.cs:

using Microsoft.AspNetCore.Components;

public class CustomComponentBase : ComponentBase, IHandleEvent, IHandleAfterRender
{
    private bool hasCalledOnAfterRender;

    [Inject]
    private IServiceProvider Services { get; set; } = default!;

    [Inject]
    private BlazorServiceAccessor BlazorServiceAccessor { get; set; } = default!;

    public override Task SetParametersAsync(ParameterView parameters)
        => InvokeWithBlazorServiceContext(() => base.SetParametersAsync(parameters));

    Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
        => InvokeWithBlazorServiceContext(() =>
        {
            var task = callback.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                task.Status != TaskStatus.Canceled;

            StateHasChanged();

            return shouldAwaitTask ?
                CallStateHasChangedOnAsyncCompletion(task) :
                Task.CompletedTask;
        });

    Task IHandleAfterRender.OnAfterRenderAsync()
        => InvokeWithBlazorServiceContext(() =>
        {
            var firstRender = !hasCalledOnAfterRender;
            hasCalledOnAfterRender |= true;

            OnAfterRender(firstRender);

            return OnAfterRenderAsync(firstRender);
        });

    private async Task CallStateHasChangedOnAsyncCompletion(Task task)
    {
        try
        {
            await task;
        }
        catch
        {
            if (task.IsCanceled)
            {
                return;
            }

            throw;
        }

        StateHasChanged();
    }

    private async Task InvokeWithBlazorServiceContext(Func<Task> func)
    {
        try
        {
            BlazorServiceAccessor.Services = Services;
            await func();
        }
        finally
        {
            BlazorServiceAccessor.Services = null;
        }
    }
}

CustomComponentBase을 자동으로 확장하는 모든 구성 요소는 현재 Blazor DI 범위의 BlazorServiceAccessor.ServicesIServiceProvider로 설정합니다.

마지막으로, 파일에서 Program 범위가 BlazorServiceAccessor 지정된 서비스로 추가합니다.

builder.Services.AddScoped<BlazorServiceAccessor>();

마지막으로 범위 Startup.ConfigureServicesStartup.cs가 지정된 서비스로 추가 BlazorServiceAccessor 합니다.

services.AddScoped<BlazorServiceAccessor>();

추가 리소스