Почему Unit of Work
В проекте “Музей юмора” у меня используется достаточно больше количество репозиториев. В этой статье реализуем паттерн “Unit of Work”. Предвижу возможный вопрос: “для чего?”. На сайте asp.net о причине использования данного паттерна описано так:
The unit of work class serves one purpose: to make sure that when you use multiple repositories, they share a single database context.
Что, в вольном переводе автора статьи звучит как:
Класс “единица работы” служит одной цели: чтобы была уверенность в том, что при использовании множества репозиториев, которые работают с базой данных, работа осуществлялась с одним и тем же экземпляром DbContext.
В своем приложении я использовал контейнер UnityContainer для разрешения экземпляров классов (resolve), ну, и, соответственно, для инъекций вливания (Dependency Injection). На самом деле, существует огромное количество контейнеров, каждый из которых, в свою очередь, так или иначе, позволяет использовать экземпляры некоторых классов в режиме “единственный экземпляр” (Singleton). Не плохо с этим справляется и UnityContainer (использование PerThreadLifetimeManager или HierarchicalLifetimeManager), но в силу ограниченности серверных ресурсов (ограничения хост-провайдера, см. предыстории часть 1 и часть 2), пришлось искать альтернативное решение.
В результате большого количества тестов направленных в основном на использование памяти (private memory), выяснилось, что принцип управления жизненным циклом экземпляра DbContext лучше всего “взять в свои руки”, а не полагаться на универсальные механизмы примененные в UnityContainer. Таким образом, если говорить о концепции “один запрос – одни DbContext” (one DbContext per request), то использование “единицы работы” (Unit of Work) позволит несколько другим способом контролировать уникальность контекста базы данных (DbContext) на один запрос.
Unit of Work (UoW) — описание
О принципе UoW можно кратко сказать так, что UoW объединяет репозитории для работы с одним и тем же экземпляром базы данных. Если у вас более сложная структура данных, возможно потребуется более чем одни DbContext, и, соответственно, более чем один Unit of Work. В случае с проектом “Музей юмора”, имеет смысл один DbContext, и, соответственно, одни Unit of Work.
UoW имеет у себя практически единственный метод Commit, который в выполняет метод SaveChange у DbContext:
1: publicinterface IMuseumUoW { 2: 3: // Сохранение данных 4: // ожидающих сохранения 5: void Commit(); 6: 7: // Репозитории 8: IRepository<Exhibit> Exhibits { get; } 9: IRepository<Hall> Halls { get; } 10: IRepository<LinkItem> LinkItems { get; } 11: IRepository<LinkCatalog> LinkCatalogs { get; } 12: ISubscriberRepository Subscribers { get; } 13: ILentaRepository Lentas { get; } 14: ILogRepository Logs { get; } 15: ITagRepository Tags { get; } 16: }
Думаю, вы обратили внимание на некоторое количество репозиториев, которые перекочевали сюда из инициализации Bootstrapper’e (см. статью “Всё ради данных”).
Nuget-пакет Mvc.UnitOfWork
Небольшое лирическое отступление связано с ленью. Чтобы не писать один и тот же код много раз, я в очередной раз создал nuget-пакет. Пакет называется Mvc.UnitOfWork. В нем содержится некоторое количество классов и интерфейсов, использование которых существенно ускорят процесс внедрения паттерна Unit of Work в вашем ASP.NET MVC проекте.
Откуда пакеты? С Nuget’а вестимо!
Установим новый пакет:
PM> Install-Package Mvc.UnitOfWork
Attempting to resolve dependency ‘EntityFramework (≥ 4.3.0)’.
Successfully installed ‘Mvc.UnitOfWork 0.1.0’.
Successfully added ‘Mvc.UnitOfWork 0.1.0’ to Calabonga.Mvc.Humor.
PM>
Интерфейсы репозиториев и их реализации
Теперь придется немного переделать интерфейсы. Например, IExhibitRepository, которые был ранее в статьях “История одного проекта” описан как:
1: publicinterface IExhibitRepository { 2: IQueryable<Exhibit> All { get; } 3: IQueryable<Exhibit> AllIncluding(params Expression<Func<Exhibit, object>>[] includeProperties); 4: Exhibit Find(int id); 5: void Insert(Exhibit item); 6: void Update(Exhibit item); 7: void Delete(int id); 8: void Save(); 9: }
В силу того, что реализация интерфейсов убрана в обобщенные базовые классы (в nuget-пакете валяются):
1: publicinterface IRepository<T> where T : class { 2: IQueryable<T> All(); 3: IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties); 4: T GetById(int id); 5: void Add(T entity); 6: void Update(T entity); 7: void Delete(T entity); 8: void Delete(int id); 9: }
Теперь мой интерфейс будет выглядеть так:
1: publicinterface IExhibitRepository : IRepository<Exhibit> { 2: }
С реализацией базовых интерфейсов (interface) всё заметно упростилось, не правда ли? А теперь реализуем IExhibitRepository, описанный ранее.
1: publicclass ExhibitRepository : EntityRepository<Exhibit>, IExhibitRepository { 2: public ExhibitRepository(DbContext context) : base(context) { } 3: }
Этого достаточно, чтобы получит контроль надо CRUD-операциями этой сущности (Create/Read/Update/Delete = CRUD).
Но что делать, если нам потребуется немного больше свойств и методов, нежели уже описанных в базовых классах. Примером тому сущность “метка” (Tag). У меня в проекте обязательно требуется дополнительный метод, который “умеет” искать метку по имени, а не только по идентификатору, как указано в базовом интерфейсе (см. метод GetById).
Расширяемся
Расширим базовый репозиторий. Теперь мой ITagRepository будет выглядеть так:
1: publicinterface ITagRepository : IRepository<Tag> { 2: Tag FindName(string name); 3: }
Более того, если я не хочу использовать предопределенные методы и свойства, их можно легко переопределить, потому что они помечены модификатором virtual. Итак, реализация ITagRepository теперь будет такая:
1: publicclass TagRepository : EntityRepository<Tag>, ITagRepository { 2: public TagRepository(DbContext context) : base(context) { } 3: 4: public Tag FindName(string name) { 5: return DbSet.SingleOrDefault(x => x.Name.ToLower().Equals(name.ToLower())); 6: } 7: }
Обратите внимание, как и прошлой реализации для интерфейса IExhibitRepository, мой TagRepository унаследован от базового класса EntityRepository<T>, а в базовый конструктор “проваливается” экземпляр класса DbContext.
И еще одна немаловажная вещь. Для доступа к таблице (DbSet<T>) в базе данных (DbContext) нужно использовать (если вы будете использовать nuget-пакет Mvc.UnityOfWork) свойство DbSet, как показано в предыдущем листинге.
Unit of Work (UoW) – реализация
Так как выше вначале статьи IMuseumUow уже приведен полностью, давайте создадим класс-реализацию этого интерфейса (весь список будет в полном листинге в конце статьи). Но для начала унаследуем наш класс от базового класса из nuget-пакета UnitOfWorkBase:
1: /// <summary> 2: /// Main class Unit of Work for Museum of Humor 3: /// </summary> 4: publicclass MuseumUoW : UnitOfWorkBase, IMuseumUoW, IDisposable { 5: 6: public MuseumUoW(IRepositoryProvider provider) 7: : base(provider) { 8: InitializeDbContext(); 9: } 10: 11: // часть кода скрыта для краткости 12: }
В строке номер 8 инициализируем DbContext, при этом сразу задаем параметры и вызываем инициализацию провайдера репозиториев:
1: publicoverridevoid InitializeDbContext() { 2: DbContext = new MuseumContext(); 3: 4: // запретим создавать proxy-классы для сущностей 5: // чтобы избежать проблем при сериализации объектов 6: DbContext.Configuration.ProxyCreationEnabled = false; 7: 8: // Отключим неявную «ленивую» загрузку 9: // (избежим проблемы с сериализацией) 10: DbContext.Configuration.LazyLoadingEnabled = false; 11: 12: // Отключим для повышения увеличения производительности 13: // валидацию при записи в базу данных. 14: // Я ее отключил, потому что уверен в том, что 15: // реализую валидацию объектов на форме (представления), 16: // а при реализации Web API я буду использовать валидацию 17: // для Knockout.Validation 18: DbContext.Configuration.ValidateOnSaveEnabled = false; 19: 20: // Инициализируем провайдера репозиториев 21: InitializeProvider(this.DbContext); 22: }
В комментария кода всё описал “что” и “для чего”. Регистрируем репозитории, которые потребуются для работы Unit of Work:
1: // … тут другие репозитории 2: 3: public IRepository<LinkCatalog> LinkCatalogs { 4: get { 5: return GetRepository<LinkCatalog>(); 6: } 7: } 8: public ISubscriberRepository Subscribers { 9: get { 10: return GetRepositoryExt<ISubscriberRepository>(); 11: } 12: } 13:
Я сократил список регистрируемых репозиториев (полный в конце, хотя и ни к чему). Вы обратили внимания, что в конструктор MuseumUoW через инъекцию (Dependency Injection) вливается IRepositoryProvider. И на последок, реализуем IDisposable в нашем классе, мы же не хотим, чтобы были утечки памяти? Наверное, я приведу весь код моего класс MuseumUoW целиком:
1: /// <summary> 2: /// Main class Unit of Work for Museum of Humor 3: /// </summary> 4: publicclass MuseumUoW : UnitOfWorkBase, IMuseumUoW, IDisposable { 5: 6: public MuseumUoW(IRepositoryProvider provider) 7: : base(provider) { 8: InitializeDbContext(); 9: } 10: 11: 12: 13: private MuseumContext DbContext; 14: 15: #region IDisposable 16: 17: publicvoid Dispose() { 18: Dispose(true); 19: GC.SuppressFinalize(this); 20: } 21: 22: protectedvirtualvoid Dispose(bool disposing) { 23: if (disposing) { 24: if (DbContext != null) { 25: DbContext.Dispose(); 26: } 27: } 28: } 29: 30: #endregion 31: 32: 33: #region Репозитории 34: 35: public IRepository<Exhibit> Exhibits { 36: get { return GetRepository<Exhibit>(); } 37: } 38: public IRepository<Hall> Halls { 39: get { return GetRepository<Hall>(); } 40: } 41: public IRepository<LinkItem> LinkItems { 42: get { return GetRepository<LinkItem>(); } 43: } 44: public IRepository<LinkCatalog> LinkCatalogs { 45: get { return GetRepository<LinkCatalog>(); } 46: } 47: public ISubscriberRepository Subscribers { 48: get { return GetRepositoryExt<ISubscriberRepository>(); } 49: } 50: public ILentaRepository Lentas { 51: get { return GetRepositoryExt<ILentaRepository>(); } 52: } 53: public ILogRepository Logs { 54: get { return GetRepositoryExt<ILogRepository>(); } 55: } 56: public ITagRepository Tags { 57: get { return GetRepositoryExt<ITagRepository>(); } 58: } 59: 60: #endregion 61: 62: publicoverridevoid InitializeDbContext() { 63: DbContext = new MuseumContext(); 64: 65: // запретим создавать proxy-классы для сущностей 66: // чтобы избежать проблем при сериализации объектов 67: DbContext.Configuration.ProxyCreationEnabled = false; 68: 69: // Отключим неявную «ленивую» загрузку 70: // (избежим проблемы с сериализацией) 71: DbContext.Configuration.LazyLoadingEnabled = false; 72: 73: // Отключим для повышения увеличения производительности 74: // валидацию при записи в базу данных. 75: // Я ее отключил, потому что уверен в том, что 76: // реализую валидацию объектов на форме (представления), 77: // а при реализации Web API я буду использовать валидацию 78: // для Knockout.Validation 79: DbContext.Configuration.ValidateOnSaveEnabled = false; 80: 81: // Инициализируем провайдера репозиториев 82: InitializeProvider(this.DbContext); 83: } 84: 85: publicoverridevoid Commit() { 86: DbContext.SaveChanges(); 87: } 88: }
Регистрируем в контейнере всего три типа:
1: container.RegisterInstance<RepositoryFactories>(new RepositoryFactories(data)); 2: container.RegisterType<IMuseumUoW, MuseumUoW>(); 3: container.RegisterType<IRepositoryProvider, RepositoryProvider>();
Или нет, давайте я снова приведу код (нового) Bootstrapper’а целиком, потому что надо показать как регистрируются типы для фабрики репозиториев:
1: publicstaticclass Bootstrapper { 2: 3: publicstaticvoid Initialise() { 4: var container = BuildUnityContainer(); 5: 6: DependencyResolver.SetResolver(new UnityDependencyResolver(container)); 7: GlobalConfiguration.Configuration.DependencyResolver =
new Unity.WebApi.UnityDependencyResolver(container); 8: } 9: 10: privatestatic IUnityContainer BuildUnityContainer() { 11: var container = new UnityContainer(); 12: var data = new Dictionary<Type, Func<DbContext, object>>{ 13: {typeof(IExhibitRepository), dbContext => new ExhibitRepository(dbContext)}, 14: {typeof(IHallRepository), dbContext => new HallRepository(dbContext)}, 15: {typeof(ILentaRepository), dbContext => new LentaRepository(dbContext)}, 16: {typeof(ILinkItemRepository), dbContext => new LinkItemRepository(dbContext)}, 17: {typeof(ILogRepository), dbContext => new LogRepository(dbContext)}, 18: {typeof(ISubscriberRepository), dbContext => new SubscriberRepository(dbContext)}, 19: {typeof(ILinkCatalogRepository), dbContext => new LinkCatalogRepository(dbContext)}, 20: {typeof(ITagRepository), dbContext => new TagRepository(dbContext)} 21: }; 22: 23: container.RegisterInstance<RepositoryFactories>(new RepositoryFactories(data)); 24: container.RegisterType<IMuseumUoW, MuseumUoW>(); 25: container.RegisterType<IRepositoryProvider, RepositoryProvider>(); 26: 27: return container; 28: } 29: }
Разберем немного последний листинг. В строке 12 создаем словарь (Dictionary) типов для разрешения (resolve). В строках с 13 по 20 перечисляем репозитории используемые в моем Unit of Work (MuseumUow). В строке 23 в фабрику репозиториев в конструктор “подставляем” созданный словарь типов.
Так как IMuseumUoW и IRepositoryProvider используются для вливаний (DI), практически везде, то также регистрируем их в контейнере в строке 24 и 25 соответственно.
Ссылки
Пример реализации на сайте asp.net
Заключение
В качестве заключения хочется сказать, что именно IMuseumUoW в дальнейшем будет использован (как вливание Dependency Injection) при разработке WEB API сервиса (ApiController). А потом (когда-нибудь… если вдруг не будет конца света), WEB API будет использован для написания программы для Windows Phone и потом еще для одной программы для Windows 8 для размещения в Windows Store.
Подробнее: http://feedproxy.google.com/~r/blogmusor/~3/PhrTL2naovM/101
Источник: