다음을 통해 공유


ASP.NET MVC 애플리케이션에서 Entity Framework와의 동시성 처리(10개 중 7개)

작성자 : Tom Dykstra

Contoso University 샘플 웹 애플리케이션은 Entity Framework 5 Code First 및 Visual Studio 2012를 사용하여 ASP.NET MVC 4 애플리케이션을 만드는 방법을 보여 줍니다. 자습서 시리즈에 대한 정보는 시리즈의 첫 번째 자습서를 참조하세요.

참고

resolve 수 없는 문제가 발생하면 완료된 장을 다운로드하고 문제를 재현해 보세요. 일반적으로 코드를 완료된 코드와 비교하여 문제에 대한 해결 방법을 찾을 수 있습니다. 몇 가지 일반적인 오류 및 해결 방법은 오류 및 해결 방법을 참조하세요.

이전 두 자습서에서는 관련 데이터를 사용했습니다. 이 자습서에서는 동시성을 처리하는 방법을 보여줍니다. 엔터티와 함께 Department 작동하는 웹 페이지를 만들고 엔터티를 편집하고 삭제 Department 하는 페이지에서 동시성 오류를 처리합니다. 다음 그림에서는 동시성 충돌이 발생할 경우 표시되는 일부 메시지를 포함하여 인덱스 및 삭제 페이지를 보여 줍니다.

스크린샷은 편집하기 전에 Contoso University Departments 페이지를 보여줍니다.

스크린샷은 다른 사용자가 값을 변경했기 때문에 작업이 취소되었음을 설명하는 메시지가 포함된 University 페이지를 보여줍니다.

동시성 충돌

한 명의 사용자가 편집하기 위해 엔터티의 데이터를 표시한 다음, 다른 사용자가 첫 번째 사용자의 변경 내용이 데이터베이스에 기록되기 전에 동일한 엔터티의 데이터를 업데이트하는 경우 동시성 충돌이 발생합니다. 이러한 충돌의 감지를 활성화하지 않는 경우 누구든지 데이터베이스를 마지막으로 업데이트하면 다른 사용자의 변경 내용을 덮어씁니다. 많은 애플리케이션에서 이 위험은 허용 가능합니다. 적은 수의 사용자 또는 적은 업데이트가 있거나 일부 변경 내용이 덮어쓰여지는지 여부가 실제로 중요하지 않은 경우 동시성에 대한 프로그래밍의 비용은 이점보다 클 수 있습니다. 이 경우 동시성 충돌을 처리하도록 애플리케이션을 구성할 필요가 없습니다.

비관적 동시성(잠금)

애플리케이션에서 동시성 시나리오에서 실수로 인한 데이터 손실을 방지할 필요가 있는 경우 해당 작업을 수행하는 한 가지 방법은 데이터베이스 잠금을 사용하는 것입니다. 이를 비관적 동시성이라고 합니다. 예를 들어 데이터베이스에서 행을 읽기 전에 읽기 전용 또는 업데이트 액세스에 대한 잠금을 요청합니다. 업데이트 액세스에 대한 행을 잠그는 경우 변경 중인 데이터의 복사본을 가져오기 때문에 다른 사용자는 읽기 전용 또는 업데이트 액세스에 대한 행을 잠그도록 허용되지 않습니다. 읽기 전용 액세스에 대한 행을 잠그는 경우 다른 사용자도 읽기 전용에 대해 잠글 수 있지만 업데이트에 대해서는 잠글 수 없습니다.

잠금 관리에는 단점이 있습니다. 프로그램을 설정하는 데 복잡할 수 있습니다. 상당한 데이터베이스 관리 리소스가 필요하며 애플리케이션의 사용자 수가 증가함에 따라 성능 문제가 발생할 수 있습니다(즉, 잘 확장되지 않음). 이러한 이유로 모든 데이터베이스 관리 시스템은 비관적 동시성을 지원하지 않습니다. Entity Framework는 기본 제공 지원을 제공하지 않으며 이 자습서에서는 구현 방법을 보여 주지 않습니다.

낙관적 동시성

비관적 동시성에 대한 대안은 낙관적 동시성입니다. 낙관적 동시성은 동시성 충돌 발생을 허용한 다음, 그럴 경우 적절하게 반응하는 것을 의미합니다. 예를 들어 John은 부서 편집 페이지를 실행하고 영어 부서의 예산 금액을 $350,000.00에서 $0.00로 변경합니다.

Changing_English_dept_budget_to_100000

John이 저장을 클릭하기 전에 Jane은 동일한 페이지를 실행하고 시작 날짜 필드를 2007년 9월 1일에서 2013년 8월 8일로 변경합니다.

Changing_English_dept_start_date_to_1999

John은 저장 을 먼저 클릭하고 브라우저가 인덱스 페이지로 돌아오면 변경 내용을 확인하고 Jane은 저장을 클릭합니다. 다음 작업은 동시성 충돌을 처리하는 방법에 따라 결정됩니다. 몇 가지 옵션에는 다음이 포함됩니다.

  • 사용자가 수정한 속성의 추적을 유지하고 데이터베이스에서 해당하는 열만 업데이트할 수 있습니다. 예제 시나리오에서 서로 다른 속성이 두 사용자에 의해 업데이트되었기 때문에 데이터가 손실되지 않습니다. 다음에 누군가가 영어 부서를 찾아볼 때 John과 Jane의 변경 내용(2013년 8월 8일의 시작 날짜 및 0달러 예산)을 모두 볼 수 있습니다.

    이 업데이트의 메서드는 데이터 손실이 발생할 수 있는 충돌 수를 줄일 수 있지만 경쟁하는 변경 내용이 동일한 엔터티의 속성에 만들어지는 경우 데이터 손실을 방지할 수 없습니다. Entity Framework가 이 방식으로 작동하는지 여부는 업데이트 코드를 구현하는 방법에 따라 달라집니다. 엔터티에 대한 모든 기존 속성 값 뿐만 아니라 새 값의 추적을 유지하기 위해 많은 양의 상태를 유지 관리해야 하므로 웹 애플리케이션에서는 종종 실용적이지 않습니다. 많은 양의 상태를 유지 관리하면 서버 리소스가 필요하거나 웹 페이지 자체(예: 숨겨진 필드)에 포함되어야 하므로 애플리케이션 성능에 영향을 줄 수 있습니다.

  • 제인의 변화가 존의 변화를 덮어쓰도록 할 수 있습니다. 다음에 누군가가 영어 부서를 찾아볼 때 2013년 8월 8일과 복원된 $350,000.00 값이 표시됩니다. 이를 클라이언트 우선 또는 최종 우선 시나리오라고 합니다. (클라이언트의 값이 데이터 저장소에 있는 값보다 우선합니다.) 이 섹션 소개에서 설명한 대로 동시성 처리를 위해 코딩을 수행하지 않으면 자동으로 발생합니다.

  • Jane의 변경 내용이 데이터베이스에서 업데이트되지 않도록 방지할 수 있습니다. 일반적으로 오류 메시지를 표시하고, 데이터의 현재 상태를 표시하고, 변경 내용을 계속 적용하려는 경우 변경 내용을 다시 적용할 수 있습니다. 이를 저장소 우선 시나리오라고 합니다. (데이터 저장소 값은 클라이언트에서 전송한 값에 우선합니다.) 이 자습서에서는 저장소 우선 시나리오를 구현합니다. 이 메서드는 상황에 대한 경고를 받는 사용자 없이 변경 내용을 덮어쓰지 않도록 합니다.

동시성 충돌 검색

Entity Framework에서 throw하는 OptimisticConcurrencyException 예외를 처리하여 충돌을 resolve 수 있습니다. 이러한 예외를 throw하는 시기를 확인하기 위해 Entity Framework에서 충돌을 검색할 수 있어야 합니다. 따라서 데이터베이스와 데이터 모델을 적절하게 구성해야 합니다. 충돌 검색을 활성화하기 위한 몇 가지 옵션은 다음과 같습니다.

  • 데이터베이스 테이블에서 행이 변경된 시기를 확인하는 데 사용될 수 있는 추적 열을 포함합니다. 그런 다음 SQL Update 또는 Delete 명령의 절에 Where 해당 열을 포함하도록 Entity Framework를 구성할 수 있습니다.

    추적 열의 데이터 형식은 일반적으로 rowversion입니다. rowversion 값은 행이 업데이트될 때마다 증가하는 순차적 숫자입니다. Update 또는 Delete 명령에서 절에는 Where 추적 열의 원래 값(원래 행 버전)이 포함됩니다. 업데이트되는 행이 다른 사용자에 의해 변경된 경우 열의 rowversion 값이 원래 값과 다르므로 Update 또는 Delete 문은 절로 인해 업데이트할 행을 Where 찾을 수 없습니다. Entity Framework가 또는 Delete 명령에 의해 Update 업데이트된 행이 없음을 발견하면(즉, 영향을 받는 행 수가 0인 경우) 동시성 충돌로 해석됩니다.

  • Delete 명령의 절 Update 에 테이블에 Where 있는 모든 열의 원래 값을 포함하도록 Entity Framework를 구성합니다.

    첫 번째 옵션에서와 같이 행을 처음 읽 Where 은 이후 행의 내용이 변경된 경우 절은 엔터티 프레임워크가 동시성 충돌로 해석하는 업데이트할 행을 반환하지 않습니다. 열이 많은 데이터베이스 테이블의 경우 이 방법을 사용하면 절이 매우 커질 Where 수 있으며 많은 양의 상태를 유지 관리해야 할 수 있습니다. 앞에서 설명한 대로 많은 양의 상태를 유지 관리하면 서버 리소스가 필요하거나 웹 페이지 자체에 포함되어야 하므로 애플리케이션 성능에 영향을 줄 수 있습니다. 따라서 이 방법은 일반적으로 권장되지 않으며 이 자습서에서 사용되는 메서드가 아닙니다.

    동시성에 대한 이 방법을 구현하려면 ConcurrencyCheck 특성을 추가하여 동시성을 추적하려는 엔터티의 모든 기본 키가 아닌 속성을 표시해야 합니다. 이러한 변경을 통해 Entity Framework는 문의 SQL WHEREUPDATE 에 모든 열을 포함할 수 있습니다.

이 자습서의 나머지 부분에서는 엔터티에 rowversion 추적 속성을 Department 추가하고, 컨트롤러 및 뷰를 만들고, 모든 항목이 올바르게 작동하는지 테스트합니다.

부서 엔터티에 낙관적 동시성 속성 추가

Models\Department.cs에서 라는 RowVersion추적 속성을 추가합니다.

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)]
    public DateTime StartDate { get; set; }

    [Display(Name = "Administrator")]
    public int? InstructorID { get; set; }

    [Timestamp]
    public byte[] RowVersion { get; set; }

    public virtual Instructor Administrator { get; set; }
    public virtual ICollection<Course> Courses { get; set; }
}

Timestamp 특성은 이 열이 데이터베이스로 Where 전송된 및 Delete 명령의 Update 절에 포함되도록 지정합니다. 특성은 이전 버전의 SQL Server SQL rowversion이 바꾸기 전에 SQL 타임스탬프 데이터 형식을 사용했기 때문에 타임스탬프라고 합니다. 에 대한 rowversion .Net 형식은 바이트 배열입니다. 흐름 API를 사용하려는 경우 다음 예제와 같이 IsConcurrencyToken 메서드를 사용하여 추적 속성을 지정할 수 있습니다.

modelBuilder.Entity<Department>()
    .Property(p => p.RowVersion).IsConcurrencyToken();

GitHub 문제 IsRowVersion으로 IsConcurrencyToken 바꾸기를 참조하세요.

속성을 추가하여 데이터베이스 모델을 변경했으므로 다른 마이그레이션을 수행해야 합니다. PMC(패키지 관리자 콘솔)에서 다음 명령을 입력합니다.

Add-Migration RowVersion
Update-Database

부서 컨트롤러 만들기

다음 설정을 사용하여 컨트롤러를 Department 만들고 다른 컨트롤러와 동일한 방식으로 볼 수 있습니다.

Add_Controller_dialog_box_for_Department_controller

Controllers\DepartmentController.cs에서 문을 추가 using 합니다.

using System.Data.Entity.Infrastructure;

부서 관리자 드롭다운 목록에 성이 아닌 강사의 전체 이름이 포함되도록 이 파일의 모든 위치에서 "LastName"을 "FullName"으로 변경합니다(네 번 발생).

ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName");

메서드의 기존 코드를 HttpPostEdit 다음 코드로 바꿉 있습니다.

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
   [Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, InstructorID")]
    Department department)
{
   try
   {
      if (ModelState.IsValid)
      {
         db.Entry(department).State = EntityState.Modified;
         db.SaveChanges();
         return RedirectToAction("Index");
      }
   }
   catch (DbUpdateConcurrencyException ex)
   {
      var entry = ex.Entries.Single();
      var clientValues = (Department)entry.Entity;
      var databaseValues = (Department)entry.GetDatabaseValues().ToObject();

      if (databaseValues.Name != clientValues.Name)
         ModelState.AddModelError("Name", "Current value: "
             + databaseValues.Name);
      if (databaseValues.Budget != clientValues.Budget)
         ModelState.AddModelError("Budget", "Current value: "
             + String.Format("{0:c}", databaseValues.Budget));
      if (databaseValues.StartDate != clientValues.StartDate)
         ModelState.AddModelError("StartDate", "Current value: "
             + String.Format("{0:d}", databaseValues.StartDate));
      if (databaseValues.InstructorID != clientValues.InstructorID)
         ModelState.AddModelError("InstructorID", "Current value: "
             + db.Instructors.Find(databaseValues.InstructorID).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.");
      department.RowVersion = databaseValues.RowVersion;
   }
   catch (DataException /* dex */)
   {
      //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
      ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
   }

   ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName", department.InstructorID);
   return View(department);
}

뷰는 숨겨진 필드에 원래 RowVersion 값을 저장합니다. 모델 바인더가 instance 만들 때 해당 개체는 편집 페이지에서 사용자가 입력한 대로 원래 RowVersion 속성 값과 다른 속성에 대한 새 값을 갖습니다 department . 그런 다음 Entity Framework에서 SQL UPDATE 명령을 만들 때 해당 명령에는 원래 RowVersion 값이 있는 행을 찾는 절이 포함 WHERE 됩니다.

명령의 UPDATE 영향을 받는 행이 없는 경우(원래 RowVersion 값이 있는 행이 없음) Entity Framework에서 예외를 throw DbUpdateConcurrencyException 하고 블록의 catch 코드는 예외 개체에서 영향을 받는 Department 엔터티를 가져옵니다. 이 엔터티에는 데이터베이스에서 읽은 값과 사용자가 입력한 새 값이 모두 있습니다.

var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseValues = (Department)entry.GetDatabaseValues().ToObject();

다음으로, 코드는 사용자가 편집 페이지에 입력한 것과 다른 데이터베이스 값이 있는 각 열에 대한 사용자 지정 오류 메시지를 추가합니다.

if (databaseValues.Name != currentValues.Name)
    ModelState.AddModelError("Name", "Current value: " + databaseValues.Name);
    // ...

더 긴 오류 메시지는 발생한 작업과 이에 대해 수행할 작업을 설명합니다.

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.");

마지막으로, 코드는 개체의 Department 값을 데이터베이스에서 검색된 새 값으로 설정합니다RowVersion. 이 새로운 RowVersion 값은 편집 페이지가 다시 표시되고, 다음 번에 사용자가 저장을 클릭할 때 숨겨진 필드에 저장되고, 편집 페이지의 다시 표시로 인해 발생하는 동시성 오류만 catch됩니다.

Views\Department\Edit.cshtml에서 속성의 숨겨진 필드 바로 다음에 속성 값을 저장할 RowVersion 숨겨진 필드를 DepartmentID 추가합니다.

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Department</legend>

        @Html.HiddenFor(model => model.DepartmentID)
        @Html.HiddenFor(model => model.RowVersion)

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>

Views\Department\Index.cshtml에서 기존 코드를 다음 코드로 바꿔 행 링크를 왼쪽으로 이동하고 관리자 열 대신 LastName 페이지 제목과 열 머리글을 표시 FullName 하도록 변경합니다.

@model IEnumerable<ContosoUniversity.Models.Department>

@{
    ViewBag.Title = "Departments";
}

<h2>Departments</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th></th>
        <th>Name</th>
        <th>Budget</th>
        <th>Start Date</th>
        <th>Administrator</th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.DepartmentID }) |
            @Html.ActionLink("Details", "Details", new { id=item.DepartmentID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.DepartmentID })
        </td>
        <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.Administrator.FullName)
        </td>
    </tr>
}

</table>

낙관적 동시성 처리 테스트

사이트를 실행하고 부서를 클릭합니다.

스크린샷은 Contoso University Departments 페이지를 보여줍니다.

Kim Abercrombie에 대한 편집 하이퍼링크를 마우스 오른쪽 단추로 클릭하고 새 탭에서 열기 를 선택한 다음, Kim Abercrombie에 대한 편집 하이퍼링크를 클릭합니다. 두 창에 동일한 정보가 표시됩니다.

Department_Edit_page_before_changes

첫 번째 브라우저 창에서 필드를 변경하고 저장을 클릭합니다.

Department_Edit_page_1_after_change

브라우저에 변경된 값과 인덱스 페이지가 표시됩니다.

Departments_Index_page_after_first_budget_edit

두 번째 브라우저 창에서 필드를 변경하고 저장을 클릭합니다.

Department_Edit_page_2_after_change

두 번째 브라우저 창에서 저장 을 클릭합니다. 오류 메시지가 표시됩니다.

스크린샷은 사용자가 저장을 다시 선택할 준비가 된 오류 메시지가 있는 University 페이지를 보여줍니다.

저장을 다시 클릭합니다. 두 번째 브라우저에 입력한 값은 첫 번째 브라우저에서 변경한 데이터의 원래 값과 함께 저장됩니다. 인덱스 페이지가 나타날 때 저장된 값이 표시됩니다.

Department_Index_page_with_change_from_second_browser

삭제 페이지 업데이트

삭제 페이지의 경우 Entity Framework는 비슷한 방식으로 부서를 편집하는 사용자에 의해 발생한 동시성 충돌을 감지합니다. 메서드가 HttpGetDelete 확인 보기를 표시하면 뷰에 숨겨진 필드에 원래 RowVersion 값이 포함됩니다. 그런 다음 사용자가 삭제를 HttpPostDelete 확인할 때 호출되는 메서드에서 해당 값을 사용할 수 있습니다. Entity Framework에서 SQL DELETE 명령을 만들 때 원래 RowVersion 값이 있는 WHERE 절이 포함됩니다. 명령에서 영향을 받는 행이 0개인 경우(즉, 삭제 확인 페이지가 표시된 후 행이 변경됨) 동시성 예외가 throw되고 HttpGet Delete 오류 플래그가 로 설정된 true 상태에서 메서드가 호출되어 확인 페이지를 오류 메시지와 함께 다시 표시합니다. 다른 사용자가 행을 삭제했기 때문에 행 0개가 영향을 받을 수도 있으므로 이 경우 다른 오류 메시지가 표시됩니다.

DepartmentController.cs에서 메서드를 HttpGetDelete 다음 코드로 바꿉니다.

public ActionResult Delete(int id, bool? concurrencyError)
{
    Department department = db.Departments.Find(id);

    if (concurrencyError.GetValueOrDefault())
    {
        if (department == null)
        {
            ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
                + "was deleted by another user after you got the original values. "
                + "Click the Back to List hyperlink.";
        }
        else
        {
            ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
                + "was modified by another user after you got the original values. "
                + "The delete operation was canceled and the current values in the "
                + "database have been displayed. If you still want to delete this "
                + "record, click the Delete button again. Otherwise "
                + "click the Back to List hyperlink.";
        }
    }

    return View(department);
}

메서드는 동시성 오류가 발생한 후 페이지가 다시 표시되고 있는지 여부를 나타내는 선택적 매개 변수를 허용합니다. 이 플래그가 이 true면 속성을 사용하여 오류 메시지가 보기로 ViewBag 전송됩니다.

메서드(라는 DeleteConfirmed)의 HttpPostDelete 코드를 다음 코드로 바꿉니다.

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(Department department)
{
    try
    {
        db.Entry(department).State = EntityState.Deleted;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    catch (DbUpdateConcurrencyException)
    {
        return RedirectToAction("Delete", new { concurrencyError=true } );
    }
    catch (DataException /* dex */)
    {
        //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
        ModelState.AddModelError(string.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator.");
        return View(department);
    }
}

방금 바꾼 스캐폴드된 코드에서 이 메서드는 레코드 ID만 허용했습니다.

public ActionResult DeleteConfirmed(int id)

이 매개 변수를 모델 바인더로 만든 Department 엔터티 인스턴스로 변경했습니다. 이렇게 하면 레코드 키 외에 RowVersion 속성 값에 액세스할 수 있습니다.

public ActionResult Delete(Department department)

또한 DeleteConfirmed에서 Delete로 작업 메서드 이름을 변경했습니다. 메서드라는 스캐폴드된 코드는 HttpPostDelete 메서드 DeleteConfirmedHttpPost 고유한 서명을 제공합니다. CLR을 사용하려면 오버로드된 메서드에 다른 메서드 매개 변수가 있어야 합니다. 이제 서명이 고유하므로 MVC 규칙을 고수하고 및 HttpGet 삭제 메서드에 HttpPost 동일한 이름을 사용할 수 있습니다.

동시성 오류가 catch되는 경우 코드는 삭제 확인 페이지를 다시 표시하고 동시성 오류 메시지를 표시해야 함을 나타내는 플래그를 제공합니다.

Views\Department\Delete.cshtml에서 스캐폴드된 코드를 서식을 변경하고 오류 메시지 필드를 추가하는 다음 코드로 바꿉니다. 변경 내용은 강조 표시되어 있습니다.

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Delete";
}

<h2>Delete</h2>

<p class="error">@ViewBag.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<fieldset>
    <legend>Department</legend>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Name)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Name)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Budget)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Budget)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.StartDate)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.StartDate)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Administrator.FullName)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Administrator.FullName)
    </div>
</fieldset>
@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
   @Html.HiddenFor(model => model.DepartmentID)
    @Html.HiddenFor(model => model.RowVersion)
    <p>
        <input type="submit" value="Delete" /> |
        @Html.ActionLink("Back to List", "Index")
    </p>
}

이 코드는 및 h3 제목 사이에 h2 오류 메시지를 추가합니다.

<p class="error">@ViewBag.ConcurrencyErrorMessage</p>

은 필드에서 로 FullNameAdministrator 바뀝니다.LastName

<div class="display-label">
    @Html.LabelFor(model => model.InstructorID)
</div>
<div class="display-field">
    @Html.DisplayFor(model => model.Administrator.FullName)
</div>

마지막으로 문 다음에 Html.BeginFormRowVersion 속성에 DepartmentID 대한 숨겨진 필드를 추가합니다.

@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)

부서 인덱스 페이지를 실행합니다. 영어 부서의 삭제 하이퍼링크를 마우스 오른쪽 단추로 클릭하고 새 창에서 열기 를 선택한 다음, 첫 번째 창에서 영어 부서의 편집 하이퍼링크를 클릭합니다.

첫 번째 창에서 값 중 하나를 변경하고 저장 을 클릭합니다.

Department_Edit_page_after_change_before_delete

인덱스 페이지에서 변경 사항을 확인합니다.

Departments_Index_page_after_budget_edit_before_delete

두 번째 창에서 삭제를 클릭합니다.

Department_Delete_confirmation_page_before_concurrency_error

동시성 오류 메시지가 표시되며 부서 값은 데이터베이스의 현재 값으로 새로 고쳐집니다.

Department_Delete_confirmation_page_with_concurrency_error

삭제를 다시 클릭하면 부서가 삭제되었음을 보여 주는 인덱스 페이지로 리디렉션됩니다.

요약

동시성 충돌 처리에 대한 소개를 완료합니다. 다양한 동시성 시나리오를 처리하는 다른 방법에 대한 자세한 내용은 Entity Framework 팀 블로그에서 낙관적 동시성 패턴속성 값 작업을 참조하세요. 다음 자습서에서는 및 Student 엔터티에 대한 계층별 테이블 상속을 구현하는 Instructor 방법을 보여 있습니다.

다른 Entity Framework 리소스에 대한 링크는 ASP.NET 데이터 액세스 콘텐츠 맵에서 찾을 수 있습니다.