이 글 에서는 Blazor 서버앱에서 Entity Framework Core(EF Core)를 사용하는 방법을 설명합니다. DB를 연결하고 사용자 Add, Edit, Delete 하는 화면까지 Step by Step 으로 진행합니다.
소개
Blazor는 .NET 웹 시스템으로, C# 및 Razor에 의해 제어됩니다. Entity Framework Core와 함께 Blazor 서버 앱에서 CRUD(Create, Read, Update, Delete) 작업을 수행하는 방법을 살펴보겠습니다.
이글에서 사용될 DB 는 MSSQL 이고 예제 DB 는 다음 에서 발췌하여 테스트를 진행 했습니다.
https://en.wikiversity.org/wiki/Database_Examples/Northwind/SQL_Server
Database First 로 DB 먼저 작성후 모델을 생성하도록 하겠습니다.
프로젝트 생성
먼저 가장 간단한 프로젝트를 생성해 보도록 하겠습니다.
이글에서는 Visual Studio 2022, .Net 7.0 으로 프로젝트를 생성했습니다.
첫번째로 프로젝트 새로만들기를 클릭하여 블레이저 서버 앱을 선택하고 다음을 누릅니다.
프로젝트 이름을 BlazorCRUD 로 정하고 다음을 누릅니다.
화면 그대로 .Net7.0 으로 프로젝트를 생성합니다.
위의 구조와 같은 프로젝트가 생성되었습니다.
데이터베이스 연결
EntityFramework 를 사용하고 모델을 자동 생성하기 위하여 다음 두가지 패키지를 설치합니다.
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
MSSQL 서버에 접속하여 모델을 생성하도록 하겠습니다.
DB 접속
DB 접속 정보를 확인하기 위하여 SQL Server 개체 탐색기를 엽니다.
개체탐색기에서 접속 정보가 없다면 SQL Server 추가 버튼을 누릅니다.
이미 접속 정보가 있다면 이번 그림과 다음 그림은 건너띄기 하시면 됩니다.
서버 접속 정보를 입력 하시고 연결을 누릅니다.
미리 구성해 두었던 Northwind DB 가 보이며 선택하면 속성창에 연결 문자열이 보입니다. 그 연결 문자열을 복사합니다.
appsetting.json 파일에 접속 정보를 붙여넣기 합니다.
"ConnectionStrings": {
"DefaultConnection": "Data Source=127.0.0.1,1433;Initial Catalog=Northwind;User ID=XXXXXXX;Password=XXXXX;Connect Timeout=30;Encrypt=False;Trust Server Certificate=False;Application Intent=ReadWrite;Multi Subnet Failover=False"
}
위의 구문을 추가 하면 됩니다.
모델 자동 생성
우리는 Northwind 데이터베이스에서 Employees 테이블만 사용하도록 하겠습니다.
Scaffold-DbContext 명령어는 기존 데이터베이스를 기반으로 모델을 만드는 데 사용합니다. 패키지 관리자 콘솔에서 Scaffold-DbContext를 사용하여 지정할 수 있는 매개 변수는 다음과 같습니다.
Scaffold-DbContext [-Connection] [-Provider] [-OutputDir]
[-Context] [-Schemas>] [-Tables>]
[-DataAnnotations] [-Force] [-Project]
[-StartupProject] [<CommonParameters>]
위의 규칙을 읽어보시고 제 DB 에서 모델을 생성하도록 하겠습니다. 패키지 관리자 콘솔을 열어줍니다.
다음과 같이 타이핑 합니다.
Scaffold-DbContext "Name=ConnectionStrings:DefaultConnection" Microsoft.EntityFrameWorkCore.SqlServer -outputdir Data -context NorthwindContext -contextdir Data -DataAnnotations -Force -Tables Employees
그러면 솔루션 탐색기의 Data 폴더에 Employee.cs, NorthwindContxt.cs 파일이 자동 생깁니다.
Emplyee 모델과 DbContext 클래스 파일입니다.
appsetting.json 의 연결 문자열을 적용하기 위해서 NorthwindContxt.cs 에서 OnConfiguring 함수를 삭제합니다.
/*
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer("Name=ConnectionStrings:DefaultConnection");
*/
program.cs 에 다음을 추가합니다.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContextFactory<NorthwindContext>(item => item.UseSqlServer(connectionString));
이제 Navimenu.razor 파일에 다음을 추가합니다. 메뉴를 추가 하였습니다.
<div class="nav-item px-3">
<NavLink class="nav-link" href="Employees">
<span class="oi oi-people" aria-hidden="true"></span> Employees
</NavLink>
</div>
이제 CRUD 를 구성할 수 있는 파일 Razor 구성요소들을 추가하도록 하겠습니다.
- CRUD 의 Create를 담당하게될 AddEmployees.razor
- CRUD 의 Read를 담당하게될 Employees.razor
- CRUD 의 Update 를 담당하게될 EditEmployee.razor
- CRUD 의 Delete 를 담당헤게될 Delete.razor
AddEmployees.razor
@page "/AddEmployee"
@using BlazorCRUD.Data
@using Microsoft.EntityFrameworkCore;
@inject IDbContextFactory<NorthwindContext> DbFactory
@inject NavigationManager NavigationManager
<h2>Add Employee</h2>
<hr />
<form>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label for="Name" class="control-label">LastName</label>
<input form="Name" class="form-control" @bind="@obj.LastName" />
</div>
<div class="form-group">
<label for="Name" class="control-label">FirstName</label>
<input form="Name" class="form-control" @bind="@obj.FirstName" />
</div>
<div class="form-group">
<label for="BirthDay" class="control-label">BirthDay</label>
<input form="BirthDay" class="form-control" @bind="@obj.BirthDate" />
</div>
<div class="form-group">
<label for="Photo" class="control-label">Photo</label>
<input form="Photo" class="form-control" @bind="@obj.Photo" />
</div>
<div class="form-group">
<label for="Notes" class="control-label">Notes</label>
<input form="Notes" class="form-control" @bind="@obj.Notes" />
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<input type="button" class="btn btn-primary" @onclick="@CreateEmployee" value="Save" />
<input type="button" class="btn btn-primary" @onclick="@Cancel" value="Cancel" />
</div>
</div>
</div>
</form>
@code {
Employee obj = new Employee();
protected async void CreateEmployee()
{
using var context = DbFactory.CreateDbContext();
if (obj is not null)
{
context.Employees?.Add(obj);
}
try
{
await context.SaveChangesAsync();
}
catch (Exception ex)
{
}
NavigationManager.NavigateTo("Employees");
}
void Cancel()
{
NavigationManager.NavigateTo("Employees");
}
}
Employees.razor
@page "/Employees"
@using BlazorCRUD.Data
@using Microsoft.EntityFrameworkCore;
@inject IDbContextFactory<NorthwindContext> DbFactory
<NavLink class="nav-link" href="AddEmployee">
<span class="oi oi-plus" aria-hidden="true">Add New</span>
</NavLink>
<h1>Employee Info</h1>
@if (EmpObj is null)
{
<p><em>Loading... !</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>EmployeeId</th>
<th>LastName</th>
<th>FirstName</th>
<th>BirthDate</th>
<th>Photo</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
@foreach (var emp in EmpObj)
{
<tr>
<td>@emp.EmployeeId</td>
<td>@emp.LastName</td>
<td>@emp.FirstName</td>
<td>@emp.BirthDate</td>
<td>@emp.Photo</td>
<td>@emp.Notes</td>
<td>
<a class="nav-link" href="EditEmployee/@emp.EmployeeId">
<span class="oi oi-pencil" aria-hidden="true">Edit</span>
</a>
<a class="nav-link" href="Delete/@emp.EmployeeId">
<span class="oi oi-trash" aria-hidden="true">Delete</span>
</a>
</td>
</tr>
}
</tbody>
</table>
}
@code {
List<Employee>? EmpObj;
protected override async Task OnInitializedAsync()
{
using var context = DbFactory.CreateDbContext();
EmpObj = await Task.Run(() => context.Employees.ToList<Employee>());
}
}
EditEmployee.razor
@page "/EditEmployee/{Id}"
@using BlazorCRUD.Data
@using Microsoft.EntityFrameworkCore;
@implements IDisposable
@inject IDbContextFactory<NorthwindContext> DbFactory
@inject NavigationManager NavigationManager
<h2>Edit Employee</h2>
<hr />
<form>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<input form="Name" class="form-control" @bind="@obj.EmployeeId" />
</div>
<div class="form-group">
<label for="Name" class="control-label">LastName</label>
<input form="Name" class="form-control" @bind="@obj.LastName" />
</div>
<div class="form-group">
<label for="Name" class="control-label">FirstName</label>
<input form="Name" class="form-control" @bind="@obj.FirstName" />
</div>
<div class="form-group">
<label for="BirthDay" class="control-label">BirthDay</label>
<input form="BirthDay" class="form-control" @bind="@obj.BirthDate" />
</div>
<div class="form-group">
<label for="Photo" class="control-label">Photo</label>
<input form="Photo" class="form-control" @bind="@obj.Photo" />
</div>
<div class="form-group">
<label for="Notes" class="control-label">Notes</label>
<input form="Notes" class="form-control" @bind="@obj.Notes" />
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<input type="button" class="btn btn-primary" @onclick="@UpdateEmployee" value="Update" />
<input type="button" class="btn btn-primary" @onclick="@Cancel" value="Cancel" />
</div>
</div>
</div>
</form>
@code {
[Parameter]
public string Id { get; set; }
Employee obj = new Employee();
private NorthwindContext? Context { get; set; }
protected override async Task OnInitializedAsync()
{
Context = DbFactory.CreateDbContext();
var employee = await Context.Employees.SingleOrDefaultAsync(c => c.EmployeeId == Convert.ToInt32(Id));
if (employee is not null)
{
obj = employee;
}
}
protected async void UpdateEmployee()
{
if (Context is not null)
{
await Context.SaveChangesAsync();
}
NavigationManager.NavigateTo("Employees");
}
void Cancel()
{
NavigationManager.NavigateTo("Employees");
}
public void Dispose()
{
Context?.Dispose();
}
}
Delete.razor
@page "/Delete/{Id}"
@using BlazorCRUD.Data
@using Microsoft.EntityFrameworkCore;
@implements IDisposable
@inject IDbContextFactory<NorthwindContext> DbFactory
@inject NavigationManager NavigationManager
<h2>Delete Employee</h2>
<hr />
<h3>Are you sure want to delete this?</h3>
<form>
<div class="row">
<div class=" col-md-8">
<div class="form-group">
<label>Employee Id:</label>
<label>@obj.EmployeeId</label>
</div>
<div class="form-group">
<label>LastName:</label>
<label>@obj.LastName</label>
</div>
<div class="form-group">
<label>FirstName:</label>
<label>@obj.FirstName</label>
</div>
<div class="form-group">
<label>BirthDate:</label>
<label>@obj.BirthDate</label>
</div>
<div class="form-group">
<label>Photo:</label>
<label>@obj.Photo</label>
</div>
<div class="form-group">
<label>Notes:</label>
<label>@obj.Notes</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<input type="button" class="btn btn-danger" @onclick="@DeleteEmployee" value="Delete" />
<input type="button" class="btn btn-primary" @onclick="@Cancel" value="Cancel" />
</div>
</div>
</div>
</form>
@code {
[Parameter]
public string Id { get; set; }
Employee obj = new Employee();
private NorthwindContext? Context { get; set; }
protected override async Task OnInitializedAsync()
{
Context = DbFactory.CreateDbContext();
var employee = await Context.Employees.SingleOrDefaultAsync(c => c.EmployeeId == Convert.ToInt32(Id));
if (employee is not null)
{
obj = employee;
}
}
protected async void DeleteEmployee()
{
if (Context is not null)
{
Context.Employees?.Remove(obj);
await Context.SaveChangesAsync();
}
NavigationManager.NavigateTo("Employees");
}
void Cancel()
{
NavigationManager.NavigateTo("Employees");
}
public void Dispose()
{
Context?.Dispose();
}
}
응용 프로그램 실행 및 테스트
Employee 탭 클릭하면 그리드에 모든 기존 세부 정보가 표시됩니다.
DB Northwind의 Employee 정보가 자세히 표시됩니다.
Add 버튼을 클릭하여 추가하는 화면이 구성되었습니다.
Employee 정보를 수정할 수 있습니다.
Employee 삭제도 가능합니다.
결론
이 글에서는 Blazor 앱에서 EntityFramework를 통한 DB 접근 CRUD 를 알아보았습니다.