Реализация шаблонов репозитория и единиц работы в приложении ASP.NET MVC (9 из 10)

от Tom Dykstra)

Пример веб-приложения Contoso университета демонстрирует создание приложений ASP.NET MVC 4 с помощью Entity Framework 5 Code First и Visual Studio 2012. Сведения о серии руководств см. в первом руководстве серии.

Note

Если проблема не устранена, Скачайте готовую главу и попытайтесь воспроизвести проблему. Как правило, решение проблемы можно найти, сравнив код с завершенным кодом. Некоторые распространенные ошибки и способы их устранения см. в разделе ошибки и обходные пути.

В предыдущем учебном курсе было использовано наследование для сокращения избыточного кода в Student Instructor классах сущностей и. В этом учебнике вы увидите несколько способов использования шаблонов репозитория и единиц работы для операций CRUD. Как и в предыдущем учебнике, в этом случае вы измените способ работы кода с уже созданными страницами, а не создавайте новые страницы.

Шаблоны репозитория и единиц работы

Шаблоны репозитория и единиц работы предназначены для создания слоя абстракции между уровнем доступа к данным и уровнем бизнес-логики приложения. Реализация таких шаблонов позволяет изолировать приложение от изменений в хранилище данных и упрощает автоматическое модульное тестирование или разработку на основе тестирования.

В этом учебнике вы реализуете класс репозитория для каждого типа сущности. Для Student типа сущности вы создадите интерфейс репозитория и класс репозитория. При создании экземпляра репозитория в контроллере будет использоваться интерфейс, чтобы контроллер принял ссылку на любой объект, реализующий интерфейс репозитория. Когда контроллер запускается на веб-сервере, он получает репозиторий, который работает с Entity Framework. Когда контроллер запускается в классе модульного теста, он получает репозиторий, который работает с данными, сохраненными таким образом, что можно легко управлять для тестирования, например в коллекции в памяти.

Далее в этом руководстве вы будете использовать несколько репозиториев и класс единиц работы для Course Department типов сущностей и в Course контроллере. Класс единиц работы координирует работу нескольких репозиториев, создавая один класс контекста базы данных, общий для всех. Если вы хотите иметь возможность выполнять автоматическое модульное тестирование, вы создадите и используете интерфейсы для этих классов так же, как и для Student репозитория. Тем не менее, чтобы упростить учебник, вы создадите и используете эти классы без интерфейсов.

На следующем рисунке показан один из способов концептуализировать связей между классами контроллера и контекста по сравнению с тем, чтобы не использовать шаблон репозитория или единицы работы вообще.

Repository_pattern_diagram

В этой серии руководств не создаются модульные тесты. Общие сведения о TDD с приложением MVC, которое использует шаблон репозитория, см. в разделе Пошаговое руководство. Использование TDD с ASP.NET MVC. Дополнительные сведения о шаблоне репозитория см. в следующих ресурсах:

Note

Существует множество способов реализации шаблонов репозитория и единиц работы. Классы репозитория можно использовать с классом единиц работы или без него. Можно реализовать один репозиторий для всех типов сущностей или по одному для каждого типа. При реализации по одному для каждого типа можно использовать отдельные классы, универсальный базовый класс и производные классы, а также абстрактный базовый класс и производные классы. Вы можете включить бизнес-логику в репозиторий или ограничить ее логикой доступа к данным. Также можно создать слой абстракции в классе контекста базы данных, используя интерфейсы идбсет вместо типов DbSet для наборов сущностей. Подход к реализации слоя абстракции, приведенный в этом учебнике, является одним из вариантов, которые следует учитывать, а не рекомендациям для всех сценариев и сред.

Создание класса репозитория учащихся

В папке 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();
    }
}

Этот код объявляет типичный набор методов CRUD, включая два метода чтения — один, который возвращает все Student сущности, и один объект, который находит одну Student сущность по идентификатору.

В папке DAL создайте файл класса с именем StudentRepository.CS File. Замените существующий код следующим кодом, который реализует 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);
        }
    }
}

Контекст базы данных определен в переменной класса, и конструктору требуется передать вызывающий объект в экземпляр контекста:

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;

Конструктор по умолчанию (без параметров) создает новый экземпляр контекста, а необязательный конструктор позволяет вызывающему объекту передать экземпляр контекста.

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 (учащиеся ).

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.Students на studentRepository.GetStudents() students переменную после этой инструкции будет IEnumerable коллекция, включающая в себя всех учащихся в базе данных. Конечный результат применения Where метода тот же, но теперь работа выполняется в памяти на веб-сервере, а не в базе данных. Для запросов, возвращающих большие объемы данных, это может оказаться неэффективным.

Tip

IQueryable и IEnumerable

После реализации репозитория, как показано здесь, даже при вводе чего-либо в поле поиска запрос, отправленный SQL Server возвращает все строки учащихся, так как не включает условия поиска:

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 объекта запрос, отправленный в 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 . Вместо того чтобы вызывать контекст для чтения и записи данных, этот макет класса репозитория может управлять коллекциями в памяти для тестирования функций контроллера.

Реализация универсального репозитория и класса единиц работы

Создание класса репозитория для каждого типа сущности может привести к созданию большого объема избыточного кода, что может привести к частичному обновлению. Например, предположим, что необходимо обновить два разных типа сущностей в рамках одной транзакции. Если каждый из них использует отдельный экземпляр контекста базы данных, один может завершиться успешно, а другой может произойти сбой. Одним из способов сворачивания избыточного кода является использование универсального репозитория и один из способов убедиться, что все репозитории используют один и тот же контекст базы данных (и, таким образом, согласовывая все обновления), — использовать класс единиц работы.

В этом разделе руководства вы создадите GenericRepository класс и UnitOfWork класс и используете их в Course контроллере для доступа к Department Course наборам сущностей и. Как было сказано ранее, для упрощения этой части учебника не создаются интерфейсы для этих классов. Но если вы собираетесь использовать их для упрощения 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;

Конструктор принимает экземпляр контекста базы данных и инициализирует переменную набора сущностей:

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 типа сущности, то код в вызывающем методе может указать student => student.LastName == "Smith " для filter параметра.

Код Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy также означает, что вызывающий объект предоставит лямбда-выражение. Но в этом случае входные данные для выражения являются IQueryable объектом для TEntity типа. Выражение возвратит упорядоченную версию этого IQueryable объекта. Например, если создается экземпляр репозитория для Student типа сущности, то код в вызывающем методе может указать q => q.OrderBy(s => s.LastName) для orderBy параметра.

Код в 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 коллекции, возвращаемой методом, вместо предоставления параметров для этих функций. Но после этого работа по сортировке и фильтрации будет осуществляться в памяти на веб-сервере. Используя эти параметры, вы гарантируете, что работа будет выполняться базой данных, а не веб-сервером. Альтернативой является создание производных классов для конкретных типов сущностей и добавление специализированных Get методов, таких как GetStudentsInNameOrder или GetStudentsByName . Однако в сложном приложении это может привести к созданию большого количества таких производных классов и специализированных методов, которые могут оказаться более сложными для обслуживания.

Код в GetByID Insert 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);
}

Один из них позволяет передать только идентификатор удаляемой сущности, а другой — экземпляр сущности. Как было показано в учебнике Обработка параллелизма , для обработки параллелизма необходим Delete метод, принимающий экземпляр сущности, включающий исходное значение свойства отслеживания.

Этот универсальный репозиторий будет выполнять стандартные требования CRUD. Если определенный тип сущности имеет особые требования, например более сложную фильтрацию или упорядочение, можно создать производный класс, содержащий дополнительные методы для этого типа.

Создание класса единиц работы

Класс единиц работы играет одну цель: чтобы убедиться, что при использовании нескольких репозиториев они совместно используют один контекст базы данных. Таким образом, при завершении единицы работы можно вызвать SaveChanges метод для этого экземпляра контекста и гарантировать, что все связанные изменения будут скоординированы. Все, что требуется класс — это Save метод и свойство для каждого репозитория. Каждое свойство репозитория возвращает экземпляр репозитория, экземпляр которого был создан с использованием того же экземпляра контекста базы данных, что и другие экземпляры репозитория.

В папке 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;

Каждое свойство репозитория проверяет, существует ли уже репозиторий. В противном случае он создает экземпляр репозитория, передавая ему экземпляр контекста. В результате все репозитории совместно используют один и тот же экземпляр контекста.

public GenericRepository<Department> DepartmentRepository
{
    get
    {

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

SaveМетод вызывает SaveChanges в контексте базы данных.

Как и любой класс, создающий экземпляр контекста базы данных в переменной класса, UnitOfWork класс реализует IDisposable и удаляет контекст.

Изменение контроллера курса для использования класса и репозиториев Унитофворк

Замените код, который в данный момент находится в 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Метод уничтожает 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 объектом см. в разделе интерфейс IQueryable (T) (System. LINQ) в библиотеке MSDN. В следующем учебном курсе вы узнаете, как работать с некоторыми расширенными сценариями.

Ссылки на другие ресурсы Entity Framework можно найти в карте содержимого ASP.NET Data Access.