728x90
반응형
Department.cs
namespace WEB.Models;
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public Instructor? Administrator { get; set; }
public ICollection<Course>? Courses { get; set; }
}
DepartmentsController.cs
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using WEB.Data;
using WEB.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace WEB.Controllers;
public class DepartmentsController : Controller
{
private readonly SchoolContext _context;
public DepartmentsController(SchoolContext context)
{
_context = context;
}
public async Task<IActionResult> Index()
{
var SchoolContext = _context.Departments.Include(d => d.Administrator);
return View(await SchoolContext.ToListAsync());
}
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
var department = await _context.Departments
.FromSqlRaw(query, id)
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync();
if (department == null)
{
return NotFound();
}
return View(department);
}
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var department = await _context.Departments
.Include(i => i.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (department == null)
{
return NotFound();
}
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
return View(department);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
{
if (id == null)
{
return NotFound();
}
var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
//다른 사용자에 의해 수정하려는 entity가 삭제된 경우, posted form values를 사용하여 entity를 생성하고,
//에러메시지와 함께 수정화면에 노출한다.
Department deletedDepartment = new Department();
await TryUpdateModelAsync(deletedDepartment);
ModelState.AddModelError(string.Empty, "Unable to save changes. The department was deleted by another user.");
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
return View(deletedDepartment);
}
//view는 hidden field에 original RowVersion 값을 저장하고, Edit 함수는 그 값을 rowVersion parameter로 받는다.
//original RowVersion 값을 entity의 OriginalValues collection에 넣는다.
_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
//UPDATE 구문에 original RowVersion row를 찾는 WHERE 조건이 추가된다.
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"",
s => s.Name, s=> s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
//영향받은 row가 없으면(original RowVersion값의 row가 없음), EF는 DbUpdateConcurrencyException exception을 발생시킨다.
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single(); //일반적으로 한개의 EntityEntry object를 갖는다.
var clientValues = (Department)exceptionEntry.Entity; //사용자가 입력한 값을 갖는 entity
var databaseEntry = exceptionEntry.GetDatabaseValues(); //현재 database 값을 갖는 entity
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save changes. The department was deleted by another user.");
}
else
{
var databaseValues = (Department)databaseEntry.ToObject();
if (databaseValues.Name != clientValues.Name)
{
ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
}
if (databaseValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
}
if (databaseValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
}
if (databaseValues.InstructorID != clientValues.InstructorID)
{
Instructor databaseInstructor = await _context.Instructors.FirstOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
//db에 저장된 new RowVersion값을 할당한다.
//hidden field에 저장되며, 다음 번 저장 시 redisplay 이후 발생한 동시성 에러만 잡는다.
departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
//ModelState가 old RowVersion값을 갖기 때문에 삭제한다.
//ModelState 값이 model property보다 우선순위를 갖는다.
ModelState.Remove("RowVersion");
}
}
}
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
return View(departmentToUpdate);
}
}
Index.cshtml
@model IEnumerable<WEB.Models.Department>
@{
ViewData["Title"] = "Departments";
}
<h1>Departments</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.RowVersion)
</th>
<th>
@Html.DisplayNameFor(model => model.Administrator)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Budget)
</td>
<td>
@Html.DisplayFor(modelItem => item.StartDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.RowVersion)
</td>
<td>
@Html.DisplayFor(modelItem => item.Administrator.FullName)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Edit.cshtml
@model WEB.Models.Department
@{
ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="DepartmentID" />
<input type="hidden" asp-for="RowVersion" />
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Budget" class="control-label"></label>
<input asp-for="Budget" class="form-control" />
<span asp-validation-for="Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<input asp-for="StartDate" class="form-control" />
<span asp-validation-for="StartDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="InstructorID" class="control-label"></label>
<select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
<option value="">-- Select Administrator --</option>
</select>
<span asp-validation-for="InstructorID" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
동시성 충돌 테스트
앱을 실행한 다음 Department Index page로 이동한다. English department 의 Edit page로 간 다음, 탭을 복제하여 동일한 화면 2개를 띄운다.
왼쪽 browser의 Budget을 50000.00으로 수정한 다음 Save를 클릭한다.
English department의 RowVersion이 update된 것을 확인할 수 있다.
오른쪽 browser의 Budget을 200000.00으로 수정한 다음 Save를 클릭한다.
에러메시지를 확인할 수 있다. Save 버튼을 다시 한번 클릭한다.
Budget, RowVersion이 수정된 것을 확인할 수 있다.
728x90
반응형
'Entity Framework Core' 카테고리의 다른 글
.NET Core MVC] EF Core - 9. raw SQL queries (0) | 2023.09.01 |
---|---|
.NET Core MVC] EF Core - 8. Entity 상속(table-per-hierachy inheritance) (0) | 2023.08.31 |
.NET Core MVC] EF Core - 7. DB 동시성 제어(1) tracking property (0) | 2023.08.29 |
.NET Core MVC] EF Core - 6.related data CRUD (0) | 2023.08.28 |
.NET Core MVC] EF Core - 5.복잡한 data model 생성 (0) | 2023.08.07 |
댓글