다음을 통해 공유


ASP.NET MVC 애플리케이션에서 리포지토리 및 작업 단위 패턴 구현(10개 중 9개)

작성자: Tom Dykstra

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

참고

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

이전 자습서에서는 상속을 사용하여 및 Instructor 엔터티 클래스의 Student 중복 코드를 줄입니다. 이 자습서에서는 CRUD 작업에 리포지토리 및 작업 패턴 단위를 사용하는 몇 가지 방법을 살펴보겠습니다. 이전 자습서와 마찬가지로 이 자습서에서는 새 페이지를 만드는 대신 이미 만든 페이지에서 코드가 작동하는 방식을 변경합니다.

리포지토리 및 작업 단위 패턴

리포지토리 및 작업 패턴 단위는 데이터 액세스 계층과 애플리케이션의 비즈니스 논리 계층 간에 추상화 계층을 만들기 위한 것입니다. 이러한 패턴을 구현하면 데이터 저장소의 변경 내용으로부터 애플리케이션을 격리할 수 있으며 자동화된 단위 테스트 또는 TDD(테스트 중심 개발)를 용이하게 수행할 수 있습니다.

이 자습서에서는 각 엔터티 형식에 대해 리포지토리 클래스를 구현합니다. 엔터티 형식의 Student 경우 리포지토리 인터페이스 및 리포지토리 클래스를 만듭니다. 컨트롤러에서 리포지토리를 인스턴스화할 때 컨트롤러가 리포지토리 인터페이스를 구현하는 모든 개체에 대한 참조를 수락하도록 인터페이스를 사용합니다. 컨트롤러가 웹 서버에서 실행되면 Entity Framework에서 작동하는 리포지토리를 받습니다. 컨트롤러가 단위 테스트 클래스에서 실행되면 메모리 내 컬렉션과 같이 테스트를 위해 쉽게 조작할 수 있는 방식으로 저장된 데이터와 함께 작동하는 리포지토리를 받습니다.

자습서의 뒷부분에서는 컨트롤러의 및 엔터티 형식에 대해 Course 여러 리포지토리 및 Department 작업 클래스 단위를 Course 사용합니다. 작업 클래스 단위는 모든 사용자가 공유하는 단일 데이터베이스 컨텍스트 클래스를 만들어 여러 리포지토리의 작업을 조정합니다. 자동화된 단위 테스트를 수행하려면 리포지토리에 대해 수행한 것과 동일한 방식으로 이러한 클래스에 대한 인터페이스를 Student 만들고 사용합니다. 그러나 자습서를 간단하게 유지하려면 인터페이스 없이 이러한 클래스를 만들고 사용합니다.

다음 그림에서는 리포지토리 또는 작업 단위 패턴을 전혀 사용하지 않는 것과 비교하여 컨트롤러와 컨텍스트 클래스 간의 관계를 개념화하는 한 가지 방법을 보여 줍니다.

Repository_pattern_diagram

이 자습서 시리즈에서는 단위 테스트를 만들지 않습니다. 리포지토리 패턴을 사용하는 MVC 애플리케이션을 사용하는 TDD에 대한 소개는 연습: ASP.NET MVC와 함께 TDD 사용을 참조하세요. 리포지토리 패턴에 대한 자세한 내용은 다음 리소스를 참조하세요.

참고

리포지토리 및 작업 패턴 단위를 구현하는 방법에는 여러 가지가 있습니다. 작업 클래스 단위를 사용하거나 사용하지 않고 리포지토리 클래스를 사용할 수 있습니다. 모든 엔터티 형식에 대해 단일 리포지토리를 구현하거나 각 형식에 대해 하나의 리포지토리를 구현할 수 있습니다. 각 형식에 대해 하나를 구현하는 경우 별도의 클래스, 제네릭 기본 클래스 및 파생 클래스 또는 추상 기본 클래스 및 파생 클래스를 사용할 수 있습니다. 리포지토리에 비즈니스 논리를 포함하거나 데이터 액세스 논리로 제한할 수 있습니다. 엔터티 집합에 대한 DbSet 형식 대신 IDbSet 인터페이스를 사용하여 데이터베이스 컨텍스트 클래스에 추상화 계층을 빌드할 수도 있습니다. 이 자습서에 표시된 추상화 계층을 구현하는 방법은 모든 시나리오 및 환경에 대한 권장 사항이 아니라 고려해야 할 한 가지 옵션입니다.

학생 리포지토리 클래스 만들기

DAL 폴더에서 IStudentRepository.cs라는 클래스 파일을 만들고 기존 코드를 다음 코드로 바꿉니다.

using System;
using System.Collections.Generic;
using ContosoUniversity.Models;

namespace ContosoUniversity.DAL
{
    public interface IStudentRepository : IDisposable
    {
        IEnumerable<Student> GetStudents();
        Student GetStudentByID(int studentId);
        void InsertStudent(Student student);
        void DeleteStudent(int studentID);
        void UpdateStudent(Student student);
        void Save();
    }
}

이 코드는 모든 Student 엔터티를 반환하는 두 개의 읽기 메서드와 ID로 단일 Student 엔터티를 찾는 메서드를 포함하여 일반적인 CRUD 메서드 집합을 선언합니다.

DAL 폴더에서 StudentRepository.cs 파일이라는 클래스 파일을 만듭니다. 기존 코드를 인터페이스를 구현하는 다음 코드로 바꿉니다.IStudentRepository

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using ContosoUniversity.Models;

namespace ContosoUniversity.DAL
{
    public class StudentRepository : IStudentRepository, IDisposable
    {
        private SchoolContext context;

        public StudentRepository(SchoolContext context)
        {
            this.context = context;
        }

        public IEnumerable<Student> GetStudents()
        {
            return context.Students.ToList();
        }

        public Student GetStudentByID(int id)
        {
            return context.Students.Find(id);
        }

        public void InsertStudent(Student student)
        {
            context.Students.Add(student);
        }

        public void DeleteStudent(int studentID)
        {
            Student student = context.Students.Find(studentID);
            context.Students.Remove(student);
        }

        public void UpdateStudent(Student student)
        {
            context.Entry(student).State = EntityState.Modified;
        }

        public void Save()
        {
            context.SaveChanges();
        }

        private bool disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    context.Dispose();
                }
            }
            this.disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}

데이터베이스 컨텍스트는 클래스 변수에 정의되며 생성자는 호출 개체가 컨텍스트의 instance 전달해야 합니다.

private SchoolContext context;

public StudentRepository(SchoolContext context)
{
    this.context = context;
}

리포지토리에서 새 컨텍스트를 인스턴스화할 수 있지만 한 컨트롤러에서 여러 리포지토리를 사용하는 경우 각각은 별도의 컨텍스트로 끝납니다. 나중에 컨트롤러에서 여러 리포지토리를 Course 사용하며, 작업 클래스 단위가 모든 리포지토리에서 동일한 컨텍스트를 사용하도록 하는 방법을 확인할 수 있습니다.

리포지토리는 IDisposable 을 구현하고 컨트롤러의 앞부분에서 보았듯이 데이터베이스 컨텍스트를 삭제하며, CRUD 메서드는 앞에서 본 것과 동일한 방식으로 데이터베이스 컨텍스트를 호출합니다.

리포지토리를 사용하도록 학생 컨트롤러 변경

StudentController.cs에서 현재 클래스에 있는 코드를 다음 코드로 바꿉니다. 변경 내용은 강조 표시되어 있습니다.

using System;
using System.Data;
using System.Linq;
using System.Web.Mvc;
using ContosoUniversity.Models;
using ContosoUniversity.DAL;
using PagedList;

namespace ContosoUniversity.Controllers
{
   public class StudentController : Controller
   {
      private IStudentRepository studentRepository;

      public StudentController()
      {
         this.studentRepository = new StudentRepository(new SchoolContext());
      }

      public StudentController(IStudentRepository studentRepository)
      {
         this.studentRepository = studentRepository;
      }

      //
      // GET: /Student/

      public ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)
      {
         ViewBag.CurrentSort = sortOrder;
         ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
         ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";

         if (searchString != null)
         {
            page = 1;
         }
         else
         {
            searchString = currentFilter;
         }
         ViewBag.CurrentFilter = searchString;

         var students = from s in studentRepository.GetStudents()
                        select s;
         if (!String.IsNullOrEmpty(searchString))
         {
            students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                                   || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
         }
         switch (sortOrder)
         {
            case "name_desc":
               students = students.OrderByDescending(s => s.LastName);
               break;
            case "Date":
               students = students.OrderBy(s => s.EnrollmentDate);
               break;
            case "date_desc":
               students = students.OrderByDescending(s => s.EnrollmentDate);
               break;
            default:  // Name ascending 
               students = students.OrderBy(s => s.LastName);
               break;
         }

         int pageSize = 3;
         int pageNumber = (page ?? 1);
         return View(students.ToPagedList(pageNumber, pageSize));
      }

      //
      // GET: /Student/Details/5

      public ViewResult Details(int id)
      {
         Student student = studentRepository.GetStudentByID(id);
         return View(student);
      }

      //
      // GET: /Student/Create

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

      //
      // POST: /Student/Create

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Create(
         [Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
           Student student)
      {
         try
         {
            if (ModelState.IsValid)
            {
               studentRepository.InsertStudent(student);
               studentRepository.Save();
               return RedirectToAction("Index");
            }
         }
         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.");
         }
         return View(student);
      }

      //
      // GET: /Student/Edit/5

      public ActionResult Edit(int id)
      {
         Student student = studentRepository.GetStudentByID(id);
         return View(student);
      }

      //
      // POST: /Student/Edit/5

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Edit(
         [Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
         Student student)
      {
         try
         {
            if (ModelState.IsValid)
            {
               studentRepository.UpdateStudent(student);
               studentRepository.Save();
               return RedirectToAction("Index");
            }
         }
         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.");
         }
         return View(student);
      }

      //
      // GET: /Student/Delete/5

      public ActionResult Delete(bool? saveChangesError = false, int id = 0)
      {
         if (saveChangesError.GetValueOrDefault())
         {
            ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
         }
         Student student = studentRepository.GetStudentByID(id);
         return View(student);
      }

      //
      // POST: /Student/Delete/5

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Delete(int id)
      {
         try
         {
            Student student = studentRepository.GetStudentByID(id);
            studentRepository.DeleteStudent(id);
            studentRepository.Save();
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
            return RedirectToAction("Delete", new { id = id, saveChangesError = true });
         }
         return RedirectToAction("Index");
      }

      protected override void Dispose(bool disposing)
      {
         studentRepository.Dispose();
         base.Dispose(disposing);
      }
   }
}

이제 컨트롤러는 컨텍스트 클래스 대신 인터페이스를 구현하는 개체에 IStudentRepository 대한 클래스 변수를 선언합니다.

private IStudentRepository studentRepository;

기본(매개 변수 없는) 생성자는 새 컨텍스트 instance 만들고 선택적 생성자를 사용하면 호출자가 컨텍스트 instance 전달할 수 있습니다.

public StudentController()
{
    this.studentRepository = new StudentRepository(new SchoolContext());
}

public StudentController(IStudentRepository studentRepository)
{
    this.studentRepository = studentRepository;
}

( 종속성 주입 또는 DI를 사용하는 경우 DI 소프트웨어가 올바른 리포지토리 개체가 항상 제공되도록 하기 때문에 기본 생성자가 필요하지 않습니다.)

CRUD 메서드에서 리포지토리는 이제 컨텍스트 대신 호출됩니다.

var students = from s in studentRepository.GetStudents()
               select s;
Student student = studentRepository.GetStudentByID(id);
studentRepository.InsertStudent(student);
studentRepository.Save();
studentRepository.UpdateStudent(student);
studentRepository.Save();
studentRepository.DeleteStudent(id);
studentRepository.Save();

이제 메서드는 Dispose 컨텍스트 대신 리포지토리를 삭제합니다.

studentRepository.Dispose();

사이트를 실행하고 학생 탭을 클릭합니다.

Students_Index_page

페이지는 리포지토리를 사용하도록 코드를 변경하기 전과 동일하게 표시되고 작동하며 다른 학생 페이지도 동일하게 작동합니다. 그러나 컨트롤러의 메서드가 필터링 및 순서 지정을 Index 수행하는 방식에는 중요한 차이가 있습니다. 이 메서드의 원래 버전에는 다음 코드가 포함되어 있습니다.

var students = from s in context.Students
               select s;
if (!String.IsNullOrEmpty(searchString))
{
    students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                           || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}

업데이트 Index 된 메서드에는 다음 코드가 포함됩니다.

var students = from s in studentRepository.GetStudents()
                select s;
if (!String.IsNullOrEmpty(searchString))
{
    students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                        || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}

강조 표시된 코드만 변경되었습니다.

코드 students 의 원래 버전에서 은 개체로 IQueryable 입력됩니다. 쿼리는 인덱스 보기가 학생 모델에 액세스할 때까지 발생하지 않는 와 같은 메서드를 사용하여 컬렉션으로 ToList변환될 때까지 데이터베이스로 전송되지 않습니다. 위의 원래 코드에서 메서드는 Where 데이터베이스로 WHERE 전송되는 SQL 쿼리의 절이 됩니다. 즉, 선택한 엔터티만 데이터베이스에서 반환됩니다. 그러나 를 로 변경 context.StudentsstudentRepository.GetStudents()결과 이 문 뒤의 students 변수는 데이터베이스의 모든 학생을 IEnumerable 포함하는 컬렉션입니다. 메서드를 적용한 Where 최종 결과는 동일하지만 이제 데이터베이스가 아닌 웹 서버의 메모리에서 작업이 수행됩니다. 대량의 데이터를 반환하는 쿼리의 경우 비효율적일 수 있습니다.

IQueryable 및 IEnumerable

여기에 표시된 대로 리포지토리를 구현한 후 검색 상자에 항목을 입력하더라도 SQL Server 전송된 쿼리는 검색 조건을 포함하지 않으므로 모든 Student 행을 반환합니다.

구현되고 강조 표시된 새 학생 리포지토리를 보여 주는 코드의 스크린샷

SELECT 
'0X0X' AS [C1], 
[Extent1].[PersonID] AS [PersonID], 
[Extent1].[LastName] AS [LastName], 
[Extent1].[FirstName] AS [FirstName], 
[Extent1].[EnrollmentDate] AS [EnrollmentDate]
FROM [dbo].[Person] AS [Extent1]
WHERE [Extent1].[Discriminator] = N'Student'

이 쿼리는 리포지토리가 검색 조건에 대해 알지 못하고 쿼리를 실행했기 때문에 모든 학생 데이터를 반환합니다. 페이징을 위해 데이터의 하위 집합을 정렬, 적용 및 선택하는 프로세스(이 경우 3개 행만 표시)는 나중에 컬렉션에서 메서드를 호출할 때 ToPagedList 메모리에서 IEnumerable 수행됩니다.

이전 버전의 코드에서(리포지토리를 구현하기 전에) 쿼리는 개체에서 가 호출될 때 ToPagedList 검색 조건을 적용할 때까지 데이터베이스로 IQueryable 전송되지 않습니다.

학생 컨트롤러 코드를 보여 주는 스크린샷 코드의 검색 문자열 행과 코드의 페이지로 이동 목록 행이 강조 표시됩니다.

개체에서 IQueryable ToPagedList를 호출하면 SQL Server 전송된 쿼리는 검색 문자열을 지정하며, 결과적으로 검색 조건을 충족하는 행만 반환되며 메모리에서 필터링을 수행할 필요가 없습니다.

exec sp_executesql N'SELECT TOP (3) 
[Project1].[StudentID] AS [StudentID], 
[Project1].[LastName] AS [LastName], 
[Project1].[FirstName] AS [FirstName], 
[Project1].[EnrollmentDate] AS [EnrollmentDate]
FROM ( SELECT [Project1].[StudentID] AS [StudentID], [Project1].[LastName] AS [LastName], [Project1].[FirstName] AS [FirstName], [Project1].[EnrollmentDate] AS [EnrollmentDate], row_number() OVER (ORDER BY [Project1].[LastName] ASC) AS [row_number]
FROM ( SELECT 
    [Extent1].[StudentID] AS [StudentID], 
    [Extent1].[LastName] AS [LastName], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[EnrollmentDate] AS [EnrollmentDate]
    FROM [dbo].[Student] AS [Extent1]
    WHERE (( CAST(CHARINDEX(UPPER(@p__linq__0), UPPER([Extent1].[LastName])) AS int)) > 0) OR (( CAST(CHARINDEX(UPPER(@p__linq__1), UPPER([Extent1].[FirstName])) AS int)) > 0)
)  AS [Project1]
)  AS [Project1]
WHERE [Project1].[row_number] > 0
ORDER BY [Project1].[LastName] ASC',N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)',@p__linq__0=N'Alex',@p__linq__1=N'Alex'

(다음 자습서에서는 SQL Server 전송된 쿼리를 검사하는 방법을 설명합니다.)

다음 섹션에서는 데이터베이스에서 이 작업을 수행하도록 지정할 수 있는 리포지토리 메서드를 구현하는 방법을 보여 줍니다.

이제 컨트롤러와 Entity Framework 데이터베이스 컨텍스트 간에 추상화 계층을 만들었습니다. 이 애플리케이션을 사용하여 자동화된 단위 테스트를 수행하려는 경우 를 구현하는 단위 테스트 프로젝트에서 대체 리포지토리 클래스를 만들 수 있습니다 IStudentRepository. 이 모의 리포지토리 클래스는 컨텍스트를 호출하여 데이터를 읽고 쓰는 대신 컨트롤러 함수를 테스트하기 위해 메모리 내 컬렉션을 조작할 수 있습니다.

제네릭 리포지토리 및 작업 단위 클래스 구현

각 엔터티 형식에 대한 리포지토리 클래스를 만들면 많은 중복 코드가 발생할 수 있으며 부분 업데이트가 발생할 수 있습니다. 예를 들어 동일한 트랜잭션의 일부로 두 개의 서로 다른 엔터티 형식을 업데이트해야 하는 경우를 가정해 보겠습니다. 각각이 별도의 데이터베이스 컨텍스트 instance 사용하는 경우 하나는 성공하고 다른 하나는 실패할 수 있습니다. 중복 코드를 최소화하는 한 가지 방법은 제네릭 리포지토리를 사용하는 것이며, 모든 리포지토리가 동일한 데이터베이스 컨텍스트(따라서 모든 업데이트를 조정)를 사용하도록 하는 한 가지 방법은 작업 클래스 단위를 사용하는 것입니다.

자습서의 이 섹션에서는 클래스와 클래스를 GenericRepositoryUnitOfWork 만들고 컨트롤러에서 Course 사용하여 및 엔터티 집합 모두에 DepartmentCourse 액세스합니다. 앞에서 설명한 것처럼 자습서의 이 부분을 간단하게 유지하기 위해 이러한 클래스에 대한 인터페이스를 만들지 않습니다. 그러나 TDD를 용이하게 하는 데 사용하려는 경우 일반적으로 리포지토리와 동일한 방식으로 Student 인터페이스를 사용하여 구현합니다.

제네릭 리포지토리 만들기

DAL 폴더에서 GenericRepository.cs를 만들고 기존 코드를 다음 코드로 바꿉니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.Data.Entity;
using ContosoUniversity.Models;
using System.Linq.Expressions;

namespace ContosoUniversity.DAL
{
    public class GenericRepository<TEntity> where TEntity : class
    {
        internal SchoolContext context;
        internal DbSet<TEntity> dbSet;

        public GenericRepository(SchoolContext context)
        {
            this.context = context;
            this.dbSet = context.Set<TEntity>();
        }

        public virtual IEnumerable<TEntity> Get(
            Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            string includeProperties = "")
        {
            IQueryable<TEntity> query = dbSet;

            if (filter != null)
            {
                query = query.Where(filter);
            }

            foreach (var includeProperty in includeProperties.Split
                (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
            {
                query = query.Include(includeProperty);
            }

            if (orderBy != null)
            {
                return orderBy(query).ToList();
            }
            else
            {
                return query.ToList();
            }
        }

        public virtual TEntity GetByID(object id)
        {
            return dbSet.Find(id);
        }

        public virtual void Insert(TEntity entity)
        {
            dbSet.Add(entity);
        }

        public virtual void Delete(object id)
        {
            TEntity entityToDelete = dbSet.Find(id);
            Delete(entityToDelete);
        }

        public virtual void Delete(TEntity entityToDelete)
        {
            if (context.Entry(entityToDelete).State == EntityState.Detached)
            {
                dbSet.Attach(entityToDelete);
            }
            dbSet.Remove(entityToDelete);
        }

        public virtual void Update(TEntity entityToUpdate)
        {
            dbSet.Attach(entityToUpdate);
            context.Entry(entityToUpdate).State = EntityState.Modified;
        }
    }
}

클래스 변수는 데이터베이스 컨텍스트 및 리포지토리가 인스턴스화되는 엔터티 집합에 대해 선언됩니다.

internal SchoolContext context;
internal DbSet dbSet;

생성자는 데이터베이스 컨텍스트 instance 수락하고 엔터티 집합 변수를 초기화합니다.

public GenericRepository(SchoolContext context)
{
    this.context = context;
    this.dbSet = context.Set<TEntity>();
}

메서드는 Get 람다 식을 사용하여 호출 코드가 필터 조건과 결과를 정렬할 열을 지정할 수 있도록 하고 문자열 매개 변수를 사용하면 호출자가 즉시 로드할 쉼표로 구분된 탐색 속성 목록을 제공할 수 있습니다.

public virtual IEnumerable<TEntity> Get(
    Expression<Func<TEntity, bool>> filter = null,
    Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
    string includeProperties = "")

이 코드 Expression<Func<TEntity, bool>> filter 는 호출자가 형식을 기반으로 TEntity 람다 식을 제공하고 이 식이 부울 값을 반환한다는 것을 의미합니다. 예를 들어 엔터티 형식에 대해 Student 리포지토리가 인스턴스화되는 경우 호출 메서드의 코드는 매개 변수에 대해 filter "를 지정할 student => student.LastName == "Smith수 있습니다.

또한 이 코드 Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy 는 호출자가 람다 식을 제공한다는 것을 의미합니다. 그러나 이 경우 식에 대한 입력은 형식의 TEntity 개체입니다IQueryable. 식은 해당 IQueryable 개체의 정렬된 버전을 반환합니다. 예를 들어 엔터티 형식에 대해 Student 리포지토리가 인스턴스화되면 호출 메서드의 코드가 매개 변수에 대해 orderBy 를 지정할 q => q.OrderBy(s => s.LastName) 수 있습니다.

메서드의 Get 코드는 개체를 IQueryable 만든 다음 필터 식이 있는 경우 적용합니다.

IQueryable<TEntity> query = dbSet;

if (filter != null)
{
    query = query.Where(filter);
}

다음으로 쉼표로 구분된 목록을 구문 분석한 후 즉시 로드 식을 적용합니다.

foreach (var includeProperty in includeProperties.Split
    (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) 
{ 
    query = query.Include(includeProperty); 
}

마지막으로 식이 있으면 식을 적용 orderBy 하고 결과를 반환합니다. 그렇지 않으면 순서가 지정되지 않은 쿼리의 결과를 반환합니다.

if (orderBy != null)
{
    return orderBy(query).ToList();
}
else
{
    return query.ToList();
}

메서드를 Get 호출할 때 이러한 함수에 대한 매개 변수를 제공하는 대신 메서드에서 반환된 컬렉션에 IEnumerable 대한 필터링 및 정렬을 수행할 수 있습니다. 그러나 정렬 및 필터링 작업은 웹 서버의 메모리에서 수행됩니다. 이러한 매개 변수를 사용하여 작업이 웹 서버가 아닌 데이터베이스에서 수행되도록 합니다. 다른 방법은 특정 엔터티 형식에 대한 파생 클래스를 만들고 또는 GetStudentsByName와 같은 GetStudentsInNameOrder 특수 메서드를 Get 추가하는 것입니다. 그러나 복잡한 애플리케이션에서는 이러한 파생 클래스와 특수 메서드가 많이 생성될 수 있으므로 유지 관리하는 데 더 많은 작업이 될 수 있습니다.

, Insert및 메서드의 GetByID코드는 제네릭이 아닌 리포지토리에서 본 코드와 Update 비슷합니다. (메서드를 사용하여 즉시 로드할 수 없으므로 서명에 GetByID 즉시 로드 매개 변수를 Find 제공하지 않습니다.)

메서드에 대해 두 개의 오버로드가 Delete 제공됩니다.

public virtual void Delete(object id)
{
    TEntity entityToDelete = dbSet.Find(id);
    dbSet.Remove(entityToDelete);
}

public virtual void Delete(TEntity entityToDelete)
{
    if (context.Entry(entityToDelete).State == EntityState.Detached)
    {
        dbSet.Attach(entityToDelete);
    }
    dbSet.Remove(entityToDelete);
}

이 중 하나를 사용하면 삭제할 엔터티의 ID만 전달하고 엔터티 instance 사용합니다. 동시성 처리 자습서에서 보았듯이 동시성 처리의 경우 추적 속성의 원래 값을 포함하는 엔터티 instance 사용하는 메서드가 필요합니다Delete.

이 일반 리포지토리는 일반적인 CRUD 요구 사항을 처리합니다. 특정 엔터티 형식에 더 복잡한 필터링 또는 순서 지정과 같은 특별한 요구 사항이 있는 경우 해당 형식에 대한 추가 메서드가 있는 파생 클래스를 만들 수 있습니다.

작업 단위 클래스 만들기

작업 클래스 단위는 여러 리포지토리를 사용할 때 단일 데이터베이스 컨텍스트를 공유하도록 하는 한 가지 용도로 사용됩니다. 이렇게 하면 작업 단위가 완료되면 컨텍스트의 해당 instance 메서드를 호출 SaveChanges 하고 모든 관련 변경 내용이 조정될 것을 확신할 수 있습니다. 클래스에 필요한 것은 각 리포지토리에 대한 메서드 및 속성입니다 Save . 각 리포지토리 속성은 다른 리포지토리 인스턴스와 동일한 데이터베이스 컨텍스트 instance 사용하여 인스턴스화된 리포지토리 instance 반환합니다.

DAL 폴더에서 UnitOfWork.cs라는 클래스 파일을 만들고 템플릿 코드를 다음 코드로 바꿉니다.

using System;
using ContosoUniversity.Models;

namespace ContosoUniversity.DAL
{
    public class UnitOfWork : IDisposable
    {
        private SchoolContext context = new SchoolContext();
        private GenericRepository<Department> departmentRepository;
        private GenericRepository<Course> courseRepository;

        public GenericRepository<Department> DepartmentRepository
        {
            get
            {

                if (this.departmentRepository == null)
                {
                    this.departmentRepository = new GenericRepository<Department>(context);
                }
                return departmentRepository;
            }
        }

        public GenericRepository<Course> CourseRepository
        {
            get
            {

                if (this.courseRepository == null)
                {
                    this.courseRepository = new GenericRepository<Course>(context);
                }
                return courseRepository;
            }
        }

        public void Save()
        {
            context.SaveChanges();
        }

        private bool disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    context.Dispose();
                }
            }
            this.disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}

이 코드는 데이터베이스 컨텍스트 및 각 리포지토리에 대한 클래스 변수를 만듭니다. 변수의 context 경우 새 컨텍스트가 인스턴스화됩니다.

private SchoolContext context = new SchoolContext();
private GenericRepository<Department> departmentRepository;
private GenericRepository<Course> courseRepository;

각 리포지토리 속성은 리포지토리가 이미 있는지 여부를 확인합니다. 그렇지 않은 경우 컨텍스트 instance 전달하여 리포지토리를 인스턴스화합니다. 따라서 모든 리포지토리는 동일한 컨텍스트 instance 공유합니다.

public GenericRepository<Department> DepartmentRepository
{
    get
    {

        if (this.departmentRepository == null)
        {
            this.departmentRepository = new GenericRepository<Department>(context);
        }
        return departmentRepository;
    }
}

메서드는 Save 데이터베이스 컨텍스트에서 를 호출 SaveChanges 합니다.

클래스 변수에서 데이터베이스 컨텍스트를 인스턴스화하는 클래스와 마찬가지로 클래스는 UnitOfWork 컨텍스트를 IDisposable 구현하고 삭제합니다.

UnitOfWork 클래스 및 리포지토리를 사용하도록 과정 컨트롤러 변경

CourseController.cs에 현재 있는 코드를 다음 코드로 바꿉니다.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using ContosoUniversity.Models;
using ContosoUniversity.DAL;

namespace ContosoUniversity.Controllers
{
   public class CourseController : Controller
   {
      private UnitOfWork unitOfWork = new UnitOfWork();

      //
      // GET: /Course/

      public ViewResult Index()
      {
         var courses = unitOfWork.CourseRepository.Get(includeProperties: "Department");
         return View(courses.ToList());
      }

      //
      // GET: /Course/Details/5

      public ViewResult Details(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         return View(course);
      }

      //
      // GET: /Course/Create

      public ActionResult Create()
      {
         PopulateDepartmentsDropDownList();
         return View();
      }

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Create(
          [Bind(Include = "CourseID,Title,Credits,DepartmentID")]
         Course course)
      {
         try
         {
            if (ModelState.IsValid)
            {
               unitOfWork.CourseRepository.Insert(course);
               unitOfWork.Save();
               return RedirectToAction("Index");
            }
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
         }
         PopulateDepartmentsDropDownList(course.DepartmentID);
         return View(course);
      }

      public ActionResult Edit(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         PopulateDepartmentsDropDownList(course.DepartmentID);
         return View(course);
      }

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Edit(
           [Bind(Include = "CourseID,Title,Credits,DepartmentID")]
         Course course)
      {
         try
         {
            if (ModelState.IsValid)
            {
               unitOfWork.CourseRepository.Update(course);
               unitOfWork.Save();
               return RedirectToAction("Index");
            }
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
         }
         PopulateDepartmentsDropDownList(course.DepartmentID);
         return View(course);
      }

      private void PopulateDepartmentsDropDownList(object selectedDepartment = null)
      {
         var departmentsQuery = unitOfWork.DepartmentRepository.Get(
             orderBy: q => q.OrderBy(d => d.Name));
         ViewBag.DepartmentID = new SelectList(departmentsQuery, "DepartmentID", "Name", selectedDepartment);
      }

      //
      // GET: /Course/Delete/5

      public ActionResult Delete(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         return View(course);
      }

      //
      // POST: /Course/Delete/5

      [HttpPost, ActionName("Delete")]
      [ValidateAntiForgeryToken]
      public ActionResult DeleteConfirmed(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         unitOfWork.CourseRepository.Delete(id);
         unitOfWork.Save();
         return RedirectToAction("Index");
      }

      protected override void Dispose(bool disposing)
      {
         unitOfWork.Dispose();
         base.Dispose(disposing);
      }
   }
}

이 코드는 클래스에 대한 클래스 변수를 추가합니다 UnitOfWork . 여기서 인터페이스를 사용하는 경우 여기에서 변수를 초기화하지 않고 대신 리포지토리에 대해 Student 했던 것처럼 두 생성자의 패턴을 구현합니다.

private UnitOfWork unitOfWork = new UnitOfWork();

나머지 클래스에서 데이터베이스 컨텍스트에 대한 모든 참조는 속성을 사용하여 UnitOfWork 리포지토리에 액세스하는 적절한 리포지토리에 대한 참조로 대체됩니다. 메서드는 Dispose instance 삭제합니다 UnitOfWork .

var courses = unitOfWork.CourseRepository.Get(includeProperties: "Department");
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Insert(course);
unitOfWork.Save();
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Update(course);
unitOfWork.Save();
// ...
var departmentsQuery = unitOfWork.DepartmentRepository.Get(
    orderBy: q => q.OrderBy(d => d.Name));
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Delete(id);
unitOfWork.Save();
// ...
unitOfWork.Dispose();

사이트를 실행하고 과정 탭을 클릭합니다.

Courses_Index_page

페이지가 변경되기 전과 동일하게 표시되고 작동하며 다른 과정 페이지도 동일하게 작동합니다.

요약

이제 리포지토리 및 작업 패턴 단위를 모두 구현했습니다. 제네릭 리포지토리에서 람다 식을 메서드 매개 변수로 사용했습니다. 이러한 식을 개체와 함께 IQueryable 사용하는 방법에 대한 자세한 내용은 MSDN 라이브러리의 IQueryable(T) 인터페이스(System.Linq) 를 참조하세요. 다음 자습서에서는 몇 가지 고급 시나리오를 처리하는 방법을 알아봅니다.

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