Entity Framework Core(EF Core)를 사용한 ASP.NET Core Blazor
참고 항목
이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.
Important
이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.
현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.
이 문서에서는 서버 쪽 Blazor 앱에서 Entity Framework Core(EF Core)를 사용하는 방법을 설명합니다.
서버 쪽 Blazor 은 상태 저장 앱 프레임워크입니다. 앱은 서버에 대한 지속적인 연결을 유지하고, 사용자 상태는 ‘회로’의 서버 메모리에 저장됩니다. 사용자 상태의 한 예는 회로로 범위가 지정된 DI(종속성 주입) 서비스 인스턴스에 저장된 데이터입니다. Blazor에서 제공하는 고유한 애플리케이션 모델을 사용하려면 Entity Framework Core를 사용하는 특별한 방법이 필요합니다.
참고 항목
이 문서에서는 서버 쪽 Blazor 앱에 대해 설명합니다EF Core. Blazor WebAssembly 앱은 대부분 직접 데이터베이스 연결을 방지하는 WebAssembly 샌드박스에서 실행됩니다. Blazor WebAssembly에서 EF Core를 실행하는 것은 이 문서에서 다루지 않습니다.
이 지침은 .에서 대화형 서버 쪽 렌더링(대화형 SSR)을 채택하는 구성 요소에 Blazor Web App적용됩니다.
이 지침은 호스트 Blazor WebAssembly 된 Server
솔루션 또는 Blazor Server 앱의 프로젝트에 적용됩니다.
프로덕션 앱에 필요한 보안 인증 흐름
이 문서에서는 사용자 인증이 필요하지 않은 로컬 데이터베이스를 사용합니다. 프로덕션 앱은 사용 가능한 가장 안전한 인증 흐름을 사용해야 합니다. 배포된 테스트 및 프로덕션 Blazor 앱의 인증에 대한 자세한 내용은 보안 및 Identity 노드의 Blazor문서를 참조하세요.
Microsoft Azure 서비스의 경우 관리 ID를 사용하는 것이 좋습니다. 관리 ID는 앱 코드에 자격 증명을 저장하지 않고 Azure 서비스에 안전하게 인증합니다. 자세한 내용은 다음 리소스를 참조하세요.
- Azure 리소스에 대한 관리 ID란? (Microsoft Entra 설명서)
- Azure 서비스 설명서
샘플 앱
샘플 앱은 사용하는 EF Core서버 쪽 Blazor 앱에 대한 참조로 빌드되었습니다. 샘플 앱에는 정렬 및 필터링, 삭제, 추가 및 업데이트 작업을 포함하는 표가 포함되어 있습니다.
이 샘플에서는 EF Core를 사용하여 낙관적 동시성을 처리하는 방법을 보여줍니다. 그러나 네이 티브 데이터베이스 생성 동시성 토큰 은 샘플 앱의 데이터베이스 공급자인 SQLite 데이터베이스에 대해 지원되지 않습니다. 샘플 앱과의 동시성을 보여 주려면 데이터베이스 생성 동시성 토큰(예: SQL Server 공급자)을 지원하는 다른 데이터베이스 공급자를 채택합니다.
샘플 코드 보기 또는 다운로드(다운로드 방법): 채택 중인 .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.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
지시문을 포함하여 완전히 작동하는 코드를 살펴보려면 샘플 앱을 참조하세요.
Blazor 동영상 데이터베이스 앱 빌드 자습서
데이터베이스 작업에 사용하는 EF Core 앱을 빌드하는 자습서 환경은 영화 데이터베이스 앱 빌드Blazor(개요)를 참조하세요. 이 자습서에서는 영화 데이터베이스에서 Blazor Web App 영화를 표시하고 관리할 수 있는 동영상을 만드는 방법을 보여줍니다.
데이터베이스 액세스
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
를 사용하여 새 인스턴스를 만드는 것입니다. 하지만 추가 종속성을 확인해야 하는 시나리오가 있습니다.
DbContextOptions
를 사용하여 컨텍스트를 구성합니다.- ASP.NET Core의 Identity 모델을 사용하는 경우와 같이 DbContext당 연결 문자열을 사용합니다. 자세한 내용은 다중 테넌트(EF Core 문서)를 참조하세요.
Warning
앱 비밀, 연결 문자열, 자격 증명, 암호, PIN(개인 식별 번호), 개인 C#/.NET 코드 또는 프라이빗 키/토큰을 항상 안전하지 않은 클라이언트 쪽 코드에 저장하지 마세요. 테스트/스테이징 및 프로덕션 환경에서 서버 쪽 Blazor 코드 및 웹 API는 프로젝트 코드 또는 구성 파일 내에서 자격 증명을 유지 관리하지 않는 보안 인증 흐름을 사용해야 합니다. 로컬 개발 테스트 외에는 환경 변수가 가장 안전한 방법이 아니므로 환경 변수를 사용하여 중요한 데이터를 저장하는 것을 피하는 것이 좋습니다. 로컬 개발 테스트의 경우 중요한 데이터를 보호하기 위해 Secret Manager 도구를 사용하는 것이 좋습니다. 자세한 내용은 중요한 데이터 및 자격 증명을 안전하게 유지 관리하세요.
종속성을 사용하여 새 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);
}
}
이전 팩터리에서
- ActivatorUtilities.CreateInstance은 서비스 공급자를 통해 종속 항목을 충족합니다.
- IDbContextFactory<TContext>는 EF Core ASP.NET Core 5.0 이상에서 사용할 수 있으므로 ASP.NET Core 3.x에 대한 샘플 앱에서 인터페이스가 구현됩니다.
다음 예에서는 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"));
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
인스턴스를 만드는 데 사용됩니다.
home 샘플 앱 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;
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
이며, Wrapper
는 GridWrapper
구성 요소에 대한 구성 요소 참조입니다. 샘플 앱에서 Home
구성 요소(Components/Pages/Home.razor
)를 참조하세요.
참고 항목
Filters
는 삽입된 IContactFilters
이며, Wrapper
는 GridWrapper
구성 요소에 대한 구성 요소 참조입니다. 샘플 앱에서 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();
}
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;
}
}
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;
}
}
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();
}
앞의 예에서:
Busy
를true
로 설정한 경우 비동기 작업이 시작될 수 있습니다.Busy
를false
로 다시 설정할 경우 비동기 작업을 완료해야 합니다.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
추가 리소스
ASP.NET Core