본문 바로가기
Entity Framework Core

.NET Core MVC] EF Core - 2.CRUD

by Fastlane 2023. 7. 31.
728x90
반응형

상세화면

    public async Task<IActionResult> Details(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }
        
        // null-forgiving operator(!)를 사용해서 compiler에러를 없앤다.
        // !를 너무 자주 사용해야 한다면, non-nullable로 바꾸고 Fluent API 또는 Data Annotation을 통해서 optional 구성할 수 있다.
        var student = await _context.Students
            .Include(s => s.Enrollments!)
                .ThenInclude(e => e.Course)
            .AsNoTracking()
            .FirstOrDefaultAsync(m => m.ID == id);

        if (student == null)
        {
            return NotFound();
        }
        return View(student);
    }

 Detail.cshtml

@model WEB.Models.Student

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.ID">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

Create


    public IActionResult Create()
    {
        return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create(
        [Bind("EnrollmentDate,FirstMidName,LastName")] Student student) //use Bind to prevent overposting
    {
        try
        {
            //throw new DbUpdateException();

            if (ModelState.IsValid)
            {
                _context.Add(student);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists " +
                "see your system administrator.");
        }
        return View(student);
    }

ASP.NET Core MVC model binder로부터 생성된 Student entity를 Students entity set에 추가한 다음, database에 저장하는 코드이다. model binder는 posted form values를 CLR type으로 변환해 action 함수에 parameter로 전달한다. 이 경우에는, model binder가 Form collection의 property values를 사용하여 Student entity를 초기화하였다. 

 

ID는 auto increment 컬럼이므로 Bind에서 제외하였다. DbUpdateException 에러가 발생하면 에러메시지를 보여준다. DbUpdateException은 종종 앱 외부원인으로 발생하기 때문에 사용자에게 재시도하도록 권할 수 있다. 프로덕션 앱에서는 에러 로깅할 수 있다. 

 

ValidationAntiForgeryToken 은 cross-site request forgery 공격을 방지한다. token은 자동으로 form 전송 시, 포함되며 유효성을 체크한다. 

 

overposting 방지

Bind attribute는 Student instance 생성 시, model binder가 사용할 fields를 제한한다. hacker가 추가 항목을 지정한다 하더라도 DB에 저장되지 않는다. 이 방법은 추천되지 않는데, Include parameter 리스트에 없는 fields는 값을 지워버리기 때문이다. 

또는 먼저 db에서 entity를 읽어온다음,  TryUpdateModel을 호출하고 수정할 properties만 넘겨준다. 

대체 방법으로, 많은 개발자가 선호하는 방법은 model binding된 entity class를 사용하기보다 view model을 사용한다. 

AutoMapper를 선택적으로 사용할 수 있다. 

 

 Create.cshtml

@model WEB.Models.Student
@{
    ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="LastName" class="control-label"></label>
                <input asp-for="LastName" class="form-control" />
                <span asp-validation-for="LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="FirstMidName" class="control-label"></label>
                <input asp-for="FirstMidName" class="form-control" />
                <span asp-validation-for="FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="EnrollmentDate" class="control-label"></label>
                <input asp-for="EnrollmentDate" class="form-control" />
                <span asp-validation-for="EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

UPDATE

    // GET: Students/Edit/5
    public async Task<IActionResult> Edit(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }
        var student = await _context.Students.FindAsync(id);
        if (student == null)
        {
            return NotFound();
        }
        return View(student);
    }
    // POST: Students/Edit/5
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
    [HttpPost, ActionName("Edit")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> EditPost(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }
        var studentToUpdate = await _context.Students.FirstOrDefaultAsync(s => s.ID == id);
        if (await TryUpdateModelAsync<Student>(
            studentToUpdate,
            "",
            s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
        {
            try
            {
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }
            catch (DbUpdateException /* ex */)
            {
                //Log the error (uncomment ex variable name and write a log.)
                ModelState.AddModelError("", "Unable to save changes. " +
                    "Try again, and if the problem persists, " +
                    "see your system administrator.");
            }
        }
        return View(studentToUpdate);
    }

 

Entity States

  • Added
  • Unchanged
  • Modified
  • Deleted
  • Detached - entity가 database context에 의해 track되지 않는 상태

Edit.cshtml

@model WEB.Models.Student

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Student</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="ID" />
            <div class="form-group">
                <label asp-for="LastName" class="control-label"></label>
                <input asp-for="LastName" class="form-control" />
                <span asp-validation-for="LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="FirstMidName" class="control-label"></label>
                <input asp-for="FirstMidName" class="form-control" />
                <span asp-validation-for="FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="EnrollmentDate" class="control-label"></label>
                <input asp-for="EnrollmentDate" class="form-control" />
                <span asp-validation-for="EnrollmentDate" 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");}
}

DELETE

    public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
    {
        if (id == null)
        {
            return NotFound();
        }

        var student = await _context.Students
            .AsNoTracking()
            .FirstOrDefaultAsync(m => m.ID == id);
        if (student == null)
        {
            return NotFound();
        }

        if (saveChangesError.GetValueOrDefault())
        {
            ViewData["ErrorMessage"] =
                "Delete failed. Try again, and if the problem persists, see your system administrator.";
        }

        return View(student);
    }
   
    /*
    // read-first approach
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteConfirmed(int id)
    {
        var student = await _context.Students.FindAsync(id);
        if (student == null)
        {
            return RedirectToAction(nameof(StudentsController.Index));
        }

        try
        {
            _context.Students.Remove(student); //set the entity's status to Deleted
            await _context.SaveChangesAsync(); // SQL DELETE command 실행
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateException)
        {
            return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
        }
    }
    */

    // create-and-attach approach
    /*
    performance 개선이 더 중요한 경우, 불필요한 SQL query를 피할 수 있다.
    PK 값만 초기화해서 Student Entity를 만든다음, entity state를 Deleted로 설정한다.
    EF Core에서 entity 삭제를 위해 필요한 것은 이게 전부다.
    */
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteConfirmed(int id)
    {
        try
        {
            Student studentToDelete = new Student() { ID = id };
            _context.Entry(studentToDelete).State = EntityState.Deleted;
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateException)
        {
            return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
        }
    }

Delete.cshtml

@model WEB.Models.Student
@{
    ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<p class="text-danger">@ViewData["ErrorMessage"]</p>
<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.LastName)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.LastName)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.FirstMidName)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.FirstMidName)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.EnrollmentDate)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.EnrollmentDate)
        </dd>
    </dl>
   
    <form asp-action="Delete">
        <input type="hidden" asp-for="ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-action="Index">Back to List</a>
    </form>
</div>

database connection lifetime

database connection이 잡고 있는 resource를 해제하기 위해, context instance는 작업 후, 가능한 빨리 disposed되어야 한다. ASP.NET Core 내장 DI가 해당 일을 처리한다. 

 

아래 함수는 service lifetime을 기본으로 Scoped로 한다. Scoped는 web request와 동시에 lifetime이 시작되어 web request가 끝나면 자동으로 disposed된다. 

        public static IServiceCollection AddDbContext<TContext>(
            [NotNull] this IServiceCollection serviceCollection,
            [CanBeNull] Action<DbContextOptionsBuilder> optionsAction = null,
            ServiceLifetime contextLifetime = ServiceLifetime.Scoped)
            where TContext : DbContext
            => AddDbContext<TContext>(serviceCollection, (p, b) => optionsAction?.Invoke(b), contextLifetime);

Handle transactions

EF는 기본적으로 transactions 처리를 한다. SaveChanges를 호출하여 changes 중간에 error가 발생하면, changes가 자동으로 롤백된다. 수동으로 transactions 세팅을 할 수도 있다. 

 

 

728x90
반응형

댓글