Entity Framework Core(EF Core)를 사용한 ASP.NET Core Blazor

참고 항목

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

Important

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

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

이 문서에서는 서버 쪽 Blazor 앱에서 Entity Framework Core(EF Core)를 사용하는 방법을 설명합니다.

서버 쪽 Blazor 은 상태 저장 앱 프레임워크입니다. 앱은 서버에 대한 지속적인 연결을 유지하고, 사용자 상태는 ‘회로’의 서버 메모리에 저장됩니다. 사용자 상태의 한 예는 회로로 범위가 지정된 DI(종속성 주입) 서비스 인스턴스에 저장된 데이터입니다. Blazor에서 제공하는 고유한 애플리케이션 모델을 사용하려면 Entity Framework Core를 사용하는 특별한 방법이 필요합니다.

참고 항목

이 문서에서는 서버 쪽 Blazor 앱에 대해 설명합니다EF Core. Blazor WebAssembly 앱은 대부분 직접 데이터베이스 연결을 방지하는 WebAssembly 샌드박스에서 실행됩니다. Blazor WebAssembly에서 EF Core를 실행하는 것은 이 문서에서 다루지 않습니다.

이 지침은 웹앱에서 대화형 SSR(대화형 서버 쪽 렌더링)을 채택하는 구성 요소에 Blazor 적용됩니다.

이 지침은 호스트 Blazor WebAssembly 된 Server 솔루션 또는 Blazor Server 앱의 프로젝트에 적용됩니다.

샘플 앱

샘플 앱은 사용하는 EF Core서버 쪽 Blazor 앱에 대한 참조로 빌드되었습니다. 샘플 앱에는 정렬 및 필터링, 삭제, 추가 및 업데이트 작업을 포함하는 표가 포함되어 있습니다. 이 샘플에서는 EF Core를 사용하여 낙관적 동시성을 처리하는 방법을 보여줍니다.

샘플 코드 보기 또는 다운로드(다운로드 방법): 채택 중인 .NET 버전과 일치하는 폴더를 선택합니다. 버전 폴더 내에서 이름이 인 BlazorWebAppEFCore샘플에 액세스합니다.

샘플 코드 보기 또는 다운로드(다운로드 방법): 채택 중인 .NET 버전과 일치하는 폴더를 선택합니다. 버전 폴더 내에서 이름이 인 BlazorServerEFCoreSample샘플에 액세스합니다.

이 샘플에서는 모든 플랫폼에서 사용할 수 있도록 로컬 SQLite 데이터베이스를 사용합니다. 또한 생성되는 SQL 쿼리를 표시하도록 데이터베이스 로깅을 구성합니다. 이는 appsettings.Development.json에서 구성됩니다.

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

표, 추가 및 보기 구성 요소에서는 작업별로 하나의 컨텍스트를 생성하는 “context-per-operation” 패턴을 사용합니다. 편집 구성 요소에서는 구성 요소별로 하나의 컨텍스트를 생성하는 “context-per-component” 패턴을 사용합니다.

참고 항목

이 항목의 일부 코드 예제에는 표시되지 않은 네임스페이스와 서비스가 필요합니다. Razor 예제에 대한 필수 @using@inject 지시문을 포함하여 완전히 작동하는 코드를 살펴보려면 샘플 앱을 참조하세요.

데이터베이스 액세스

EF Core는 데이터베이스 액세스를 구성하고 작업 단위 역할을 하는 수단으로 사용합니다.DbContext EF Core는 기본적으로 컨텍스트를 AddDbContext 범위가 지정된 서비스로 등록하는 ASP.NET Core 앱에 대한 확장을 제공합니다. 서버 쪽 Blazor 앱에서 범위가 지정된 서비스 등록은 인스턴스가 사용자의 회로 내의 구성 요소 간에 공유되기 때문에 문제가 될 수 있습니다. DbContext는 스레드로부터 안전하지 않고 동시 사용을 위해 설계되지 않았습니다. 기존 수명은 다음과 같은 이유로 적합하지 않습니다.

  • Singleton은 앱의 모든 사용자에 대한 상태를 공유하고 부적절한 동시 사용을 초래합니다.
  • 범위 지정(기본값)은 동일한 사용자에 대한 구성 요소 간에 유사한 문제를 초래합니다.
  • 임시는 요청별로 새 인스턴스를 생성하지만 구성 요소가 오래 지속될 수 있으므로 의도한 것보다 수명이 긴 컨텍스트가 생성됩니다.

다음 권장 사항은 서버 쪽 Blazor 앱에서 사용하기 EF Core 위한 일관된 접근 방식을 제공하도록 설계되었습니다.

  • 기본적으로 작업당 하나의 컨텍스트를 사용하는 것이 좋습니다. 이 컨텍스트는 빠르고 낮은 오버헤드 인스턴스화를 위해 설계되었습니다.

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • 플래그를 사용하여 여러 동시 작업을 방지합니다.

    if (Loading)
    {
        return;
    }
    
    try
    {
        Loading = true;
    
        ...
    }
    finally
    {
        Loading = false;
    }
    

    try 블록의 Loading = true; 줄 뒤에 작업을 추가합니다.

    스레드 보안은 문제가 되지 않으므로 논리를 로드해도 데이터베이스 레코드를 잠글 필요가 없습니다. 논리 로드는 데이터를 가져오는 동안 사용자가 실수로 단추를 선택하거나 필드를 업데이트하지 않도록 UI 컨트롤을 사용하지 않도록 설정하는 데 사용됩니다.

  • 여러 스레드가 동일한 코드 블록에 액세스할 가능성이 있는 경우 센터를 삽입하고 작업당 새 인스턴스를 만듭니다. 그렇지 않으면 일반적으로 컨텍스트를 삽입하고 사용하는 것으로 충분합니다.

  • EF Core의 변경 내용 추적 또는 동시성 제어를 활용하는 장기 작업의 경우, 컨텍스트의 범위를 구성 요소의 수명으로 지정합니다.

DbContext 인스턴스

DbContext 인스턴스를 만드는 가장 빠른 방법은 new를 사용하여 새 인스턴스를 만드는 것입니다. 하지만 추가 종속성을 확인해야 하는 시나리오가 있습니다.

종속성을 사용하여 새 DbContext를 만드는 권장 접근법은 팩터리를 사용하는 것입니다. EF Core 5.0 이상에서는 새 컨텍스트를 만들기 위한 기본 제공 팩터리를 제공합니다.

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorServerDbContextExample.Data
{
    public class DbContextFactory<TContext> 
        : IDbContextFactory<TContext> where TContext : DbContext
    {
        private readonly IServiceProvider provider;

        public DbContextFactory(IServiceProvider provider)
        {
            this.provider = provider ?? throw new ArgumentNullException(
                $"{nameof(provider)}: You must configure an instance of " +
                "IServiceProvider");
        }

        public TContext CreateDbContext() => 
            ActivatorUtilities.CreateInstance<TContext>(provider);
    }
}

이전 팩터리에서

다음 예에서는 SQLite를 구성하고 데이터 로깅을 사용하도록 설정합니다. 이 코드는 확장 메서드(AddDbContextFactory)를 사용하여 DI용 데이터베이스 팩터리를 구성하고 기본 옵션을 제공합니다.

builder.Services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
builder.Services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
builder.Services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

팩터리는 구성 요소에 삽입되어 새 DbContext 인스턴스를 만드는 데 사용됩니다.

샘플 앱 IDbContextFactory<ContactContext> 의 홈페이지에서 구성 요소에 삽입됩니다.

@inject IDbContextFactory<ContactContext> DbFactory

팩터리(DbFactory)를 사용하여 DeleteContactAsync 메서드에서 연락처를 삭제하는 DbContext가 만들어집니다.

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    Filters.Loading = true;

    if (Wrapper is not null && context.Contacts is not null)
    {
        var contact = await context.Contacts
            .FirstAsync(c => c.Id == Wrapper.DeleteRequestId);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    Filters.Loading = true;

    if (Wrapper is not null && context.Contacts is not null)
    {
        var contact = await context.Contacts
            .FirstAsync(c => c.Id == Wrapper.DeleteRequestId);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    Filters.Loading = true;

    if (Wrapper is not null && context.Contacts is not null)
    {
        var contact = await context.Contacts
            .FirstAsync(c => c.Id == Wrapper.DeleteRequestId);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    {
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    {
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    }

    Filters.Loading = false;

    await ReloadAsync();
}

참고 항목

Filters는 삽입된 IContactFilters이며, WrapperGridWrapper 구성 요소에 대한 구성 요소 참조입니다. 샘플 앱에서 Home 구성 요소(Components/Pages/Home.razor)를 참조하세요.

참고 항목

Filters는 삽입된 IContactFilters이며, WrapperGridWrapper 구성 요소에 대한 구성 요소 참조입니다. 샘플 앱에서 Index 구성 요소(Pages/Index.razor)를 참조하세요.

구성 요소 수명으로 범위 지정

구성 요소의 수명 동안 존재하는 DbContext를 만들 수 있습니다. 그러면 해당 인스턴스를 작업 단위로 사용하고 변경 내용 추적, 동시성 확인과 같은 기본 제공 기능을 활용할 수 있습니다.

팩터리를 사용하여 컨텍스트를 만든 후 구성 요소의 수명 동안 추적할 수 있습니다. 먼저 구성 요소Components/Pages/EditContact.razor()에 표시된 EditContact 대로 팩터리를 구현 IDisposable 하고 삽입합니다.

팩터리를 사용하여 컨텍스트를 만든 후 구성 요소의 수명 동안 추적할 수 있습니다. 먼저 구성 요소Pages/EditContact.razor()에 표시된 EditContact 대로 팩터리를 구현 IDisposable 하고 삽입합니다.

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

샘플 앱이 구성 요소가 삭제될 때 컨텍스트가 삭제되는지 확인합니다.

public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}

마지막으로 OnInitializedAsync를 재정의하여 새 컨텍스트를 만듭니다. 샘플 앱에서 OnInitializedAsync는 동일한 방법으로 연락처를 로드합니다.

protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();

        if (Context is not null && Context.Contacts is not null)
        {
            var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);

            if (contact is not null)
            {
                Contact = contact;
            }
        }
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();

        if (Context is not null && Context.Contacts is not null)
        {
            var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);

            if (contact is not null)
            {
                Contact = contact;
            }
        }
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();

        if (Context is not null && Context.Contacts is not null)
        {
            var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);

            if (contact is not null)
            {
                Contact = contact;
            }
        }
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();
        Contact = await Context.Contacts
            .SingleOrDefaultAsync(c => c.Id == ContactId);
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();
        Contact = await Context.Contacts
            .SingleOrDefaultAsync(c => c.Id == ContactId);
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}

앞의 예에서:

  • Busytrue로 설정한 경우 비동기 작업이 시작될 수 있습니다. Busyfalse로 다시 설정할 경우 비동기 작업을 완료해야 합니다.
  • catch 블록에 추가 오류 처리 논리를 삽입합니다.

중요한 데이터 로깅 사용

EnableSensitiveDataLogging에는 예외 메시지 및 프레임워크 로깅의 애플리케이션 데이터가 포함됩니다. 로깅된 데이터에는 엔터티 인스턴스 속성에 할당된 값과 데이터베이스로 전송된 명령의 매개 변수 값이 포함될 수 있습니다. 데이터 EnableSensitiveDataLogging 로깅은 데이터베이스에 대해 실행된 SQL 문을 기록할 때 암호 및 기타 PII(개인 식별 정보)를 노출할 수 있으므로 보안 위험입니다.

EnableSensitiveDataLogging은 개발 및 테스트 용도로만 사용하는 것이 좋습니다.

#if DEBUG
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
        .EnableSensitiveDataLogging());
#else
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
#endif

추가 리소스