ASP.NET MVC: Unit of Work или продолжаем оптимизировать сайт
Сайтостроение | создано: 11.01.2013 | опубликовано: 12.01.2013 | обновлено: 13.01.2024 | просмотров: 29723 | всего комментариев: 5
В этой статье продолжим оптимизацию производительности на сайте "Музей юмора". Будем реализовывать патерн Unit of Work, который достаточно подробно описана на сайте asp.net.
Почему 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, который в выполняет метод SaveChanges у DbContext:
public interface IMuseumUoW { // Сохранение данных // ожидающих сохранения void Commit(); // Репозитории IRepository<Exhibit> Exhibits { get; } IRepository<Hall> Halls { get; } IRepository<LinkItem> LinkItems { get; } IRepository<LinkCatalog> LinkCatalogs { get; } ISubscriberRepository Subscribers { get; } ILentaRepository Lentas { get; } ILogRepository Logs { get; } ITagRepository Tags { get; } }
Думаю, вы обратили внимание на некоторое количество репозиториев, которые перекочевали сюда из инициализации 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, которые был ранее в статьях “История одного проекта” описан как:
public interface IExhibitRepository { IQueryable All { get; } IQueryable AllIncluding(params Expression<object>>[] includeProperties); Exhibit Find(int id); void Insert(Exhibit item); void Update(Exhibit item); void Delete(int id); void Save(); }
В силу того, что реализация интерфейсов убрана в обобщенные базовые классы (в nuget-пакете валяются):
public interface IRepository<T> where T : class { IQueryable<T> All(); IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties); T GetById(int id); void Add(T entity); void Update(T entity); void Delete(T entity); void Delete(int id); }
Теперь мой интерфейс будет выглядеть так:
public interface IExhibitRepository : IRepository { }
С реализацией базовых интерфейсов (interface) всё заметно упростилось, не правда ли? А теперь реализуем IExhibitRepository, описанный ранее.
public interface IExhibitRepository : IRepository<Exhibit> { } public class ExhibitRepository : EntityRepository<Exhibit>, IExhibitRepository { public ExhibitRepository(DbContext context) : base(context) { } }
Этого достаточно, чтобы получит контроль надо CRUD-операциями этой сущности (Create/Read/Update/Delete = CRUD).
Но что делать, если нам потребуется немного больше свойств и методов, нежели уже описанных в базовых классах. Примером тому сущность “метка” (Tag). У меня в проекте обязательно требуется дополнительный метод, который “умеет” искать метку по имени, а не только по идентификатору, как указано в базовом интерфейсе (см. метод GetById).
Расширяемся
Расширим базовый репозиторий. Теперь мой ITagRepository будет выглядеть так:
public interface ITagRepository : IRepository { Tag FindName(string name); }
Более того, если я не хочу использовать предопределенные методы и свойства, их можно легко переопределить, потому что они помечены модификатором virtual. Итак, реализация ITagRepository теперь будет такая:
public class TagRepository : EntityRepository<Tag>, ITagRepository { public TagRepository(DbContext context) : base(context) { } public Tag FindName(string name) { return DbSet.SingleOrDefault(x => x.Name.ToLower().Equals(name.ToLower())); } }
Обратите внимание, как и прошлой реализации для интерфейса IExhibitRepository, мой TagRepository унаследован от базового класса EntityRepository, а в базовый конструктор “проваливается” экземпляр класса DbContext.
И еще одна немаловажная вещь. Для доступа к таблице (DbSet) в базе данных (DbContext) нужно использовать (если вы будете использовать nuget-пакет Mvc.UnityOfWork) свойство DbSet, как показано в предыдущем листинге.
Unit of Work (UoW) – реализация
Так как выше вначале статьи IMuseumUow уже приведен полностью, давайте создадим класс-реализацию этого интерфейса (весь список будет в полном листинге в конце статьи). Но для начала унаследуем наш класс от базового класса из nuget-пакета UnitOfWorkBase:
/// /// Main class Unit of Work for Museum of Humor /// public class MuseumUoW : UnitOfWorkBase, IMuseumUoW, IDisposable { public MuseumUoW(IRepositoryProvider provider) : base(provider) { } // часть кода скрыта для краткости }
В строке номер 8 инициализируем DbContext, при этом сразу задаем параметры и вызываем инициализацию провайдера репозиториев:
public override DbContext InitializeDbContext() { DbContext = new MuseumContext(); // запретим создавать proxy-классы для сущностей // чтобы избежать проблем при сериализации объектов DbContext.Configuration.ProxyCreationEnabled = false; // Отключим неявную "ленивую" загрузку // (избежим проблемы с сериализацией) DbContext.Configuration.LazyLoadingEnabled = false; // Отключим для повышения увеличения производительности // валидацию при записи в базу данных. // Я ее отключил, потому что уверен в том, что // реализую валидацию объектов на форме (представления), // а при реализации Web API я буду использовать валидацию // для Knockout.Validation DbContext.Configuration.ValidateOnSaveEnabled = false; // Инициализируем провайдера репозиториев return DbContext; }
В комментария кода всё описал “что” и “для чего”. Регистрируем репозитории, которые потребуются для работы Unit of Work. Для примера я приведу пару разных типов репозиториев, хотя на самом деле немного больше:
public IRepository<LinkCatalog> LinkCatalogs { get { return GetRepository<LinkCatalog>(); } } public ISubscriberRepository Subscribers { get { return GetRepositoryExt<ISubscriberRepository>(); } }
Строки с 1-3 описывают “простой” (взятый от базового) тип, а строки 4-5 показывают использование унаследованного (расширенного) репозитория.
Вы обратили внимания, что в конструктор MuseumUoW через инъекцию (Dependency Injection) вливается IRepositoryProvider. И на последок, реализуем IDisposable в нашем классе, мы же не хотим, чтобы были утечки памяти? Наверное, я приведу весь код моего класс MuseumUoW целиком:
/// <summary> /// Main class Unit of Work for Museum of Humor /// </summary> public sealed class MuseumUoW : UnitOfWorkBase, IMuseumUoW, IDisposable { public MuseumUoW(IRepositoryProvider provider) : base(provider) { } private MuseumContext _dbContext; #region Репозитории public IRepository<Exhibit> Exhibits { get { return GetRepository<Exhibit>(); } } public IRepository<Hall> Halls { get { return GetRepository<Hall>(); } } public IRepository<LinkItem> LinkItems { get { return GetRepository<LinkItem>(); } } public IRepository<LinkCatalog> LinkCatalogs { get { return GetRepository<LinkCatalog>(); } } public ISubscriberRepository Subscribers { get { return GetRepositoryExt<ISubscriberRepository>(); } } public ILentaRepository Lentas { get { return GetRepositoryExt<ILentaRepository>(); } } public ILogRepository Logs { get { return GetRepositoryExt<ILogRepository>(); } } public ITagRepository Tags { get { return GetRepositoryExt<ITagRepository>(); } } #endregion protected override DbContext InitializeDbContext() { _dbContext = new MuseumContext(); // запретим создавать proxy-классы для сущностей // чтобы избежать проблем при сериализации объектов _dbContext.Configuration.ProxyCreationEnabled = false; // Отключим неявную "ленивую" загрузку // (избежим проблемы с сериализацией) _dbContext.Configuration.LazyLoadingEnabled = false; // Отключим для повышения увеличения производительности // валидацию при записи в базу данных. // Я ее отключил, потому что уверен в том, что // реализую валидацию объектов на форме (представления), // а при реализации Web API я буду использовать валидацию // для Knockout.Validation _dbContext.Configuration.ValidateOnSaveEnabled = false; // Инициализируем провайдера репозиториев return _dbContext; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (!disposing) return; if (_dbContext != null) { _dbContext.Dispose(); } } public override void Commit() { _dbContext.SaveChanges(); } }
Регистрируем в контейнере всего три типа:
container.RegisterInstance(new RepositoryFactories(data)); container.RegisterType(IMuseumUoW, MuseumUoW); container.RegisterType(IRepositoryProvider, RepositoryProvider);
Или нет, давайте я снова приведу код (нового) Bootstrapper’а целиком, потому что надо показать как регистрируются типы для фабрики репозиториев:
public static class Bootstrapper { public static void Initialise() { var container = BuildUnityContainer(); DependencyResolver.SetResolver(new UnityDependencyResolver(container)); GlobalConfiguration.Configuration.DependencyResolver = new Unity.WebApi.UnityDependencyResolver(container); } private static IUnityContainer BuildUnityContainer() { var container = new UnityContainer(); var data = new Dictionaryobject <Type, Func<DbContext, object>> { {typeof(IExhibitRepository), dbContext => new ExhibitRepository(dbContext)}, {typeof(IHallRepository), dbContext => new HallRepository(dbContext)}, {typeof(ILentaRepository), dbContext => new LentaRepository(dbContext)}, {typeof(ILinkItemRepository), dbContext => new LinkItemRepository(dbContext)}, {typeof(ILogRepository), dbContext => new LogRepository(dbContext)}, {typeof(ISubscriberRepository), dbContext => new SubscriberRepository(dbContext)}, {typeof(ILinkCatalogRepository), dbContext => new LinkCatalogRepository(dbContext)}, {typeof(ITagRepository), dbContext => new TagRepository(dbContext)} }; container.RegisterInstance(new RepositoryFactories(data)); container.RegisterType(IMuseumUoW, MuseumUoW); container.RegisterType(IRepositoryProvider, RepositoryProvider); return container; } }
Разберем немного последний листинг. В строке 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.
Комментарии к статье (5)
Я использую в подобном случае возможность IOC создавать дочерний контейнер. Тогда при инжекшене UnitOf Work новый экземляр его создает дочерний контейнер, а все репозитории регистрируются как Per Container. В результате можно обойтись одним методом ResolveRepository и не описывать в UnitOfWork новые. Пример кода с использованием Autofac:
// Обертка над IoC
public interface IExistenceScope : IServiceProvider, IDisposable { object GetService(Type serviceType, object tag); IExistenceScope CreateNestedScope(INestedScopeInitializer initializer); } public interface INestedScopeInitializer { void InitializeNestedScope(IExistenceScope parentScope, IExistenceScope nestedScope); } public interface IFreezeNestedScopeInitializer<TService> : INestedScopeInitializer where TService : class { } public static class ExistenceScopeExtensions { public static TService GetService<TService>(this IServiceProvider provider) { return (TService) provider.GetService(typeof (TService)); } public static TService Get<TService>(this IServiceProvider provider) { return provider.GetService<TService>(); } public static object Get(this IServiceProvider provider, Type serviceType) { return provider.GetService(serviceType); } public static TService GetService<TService>(this IExistenceScope scope, object tag) { return (TService) scope.GetService(typeof (TService), tag); } public static TService Get<TService>(this IExistenceScope scope, object tag) { return scope.GetService<TService>(tag); } }
// Реализация для Autofac
public class AutofacExistenceScope : IExistenceScope { private static readonly MethodInfo ResolveKeyedMethod; static AutofacExistenceScope() { var extension = typeof(ResolutionExtensions); ResolveKeyedMethod = extension.GetMethod("ResolveKeyed", new[] { typeof(IComponentContext), typeof(object) }); } public AutofacExistenceScope(ILifetimeScope lifetimeScope) { LifetimeScope = lifetimeScope; } public ILifetimeScope LifetimeScope { get; private set; } public object GetService(Type serviceType) { return LifetimeScope.Resolve(serviceType); } public object GetService(Type serviceType, object tag) { var serviceName = tag as string; if (serviceName != null) return LifetimeScope.ResolveNamed(serviceName, serviceType); var method = ResolveKeyedMethod.MakeGenericMethod(serviceType); return method.Invoke(null, new object[] {LifetimeScope, tag}); } public IExistenceScope CreateNestedScope(INestedScopeInitializer initializer) { var nestedScope = new AutofacExistenceScope(LifetimeScope.BeginLifetimeScope()); initializer.InitializeNestedScope(this, nestedScope); return nestedScope; } private bool _disposed; public void Dispose() { if(_disposed) return; LifetimeScope.Dispose(); _disposed = true; } } public class AutofacFreezeNestedScopeInitializer<TService> : IFreezeNestedScopeInitializer<TService> where TService: class { public void InitializeNestedScope(IExistenceScope parentScope, IExistenceScope nestedScope) { var parentLifetime = ((AutofacExistenceScope) parentScope).LifetimeScope; var nestedLifetime = ((AutofacExistenceScope) nestedScope).LifetimeScope; var service = parentLifetime.Resolve<TService>(); var builder = new ContainerBuilder(); builder.RegisterInstance(service).SingleInstance(); builder.Update(nestedLifetime.ComponentRegistry); } }
Thank you very much for your efforts, your examples are simple and very understandable.
IamStalker
Да все хорошо... Радуги и пони вокруг... Вот только зачем тебе IOC контейнер? Ты же не собираешься поддерживать несколько видов ОРМ? Вот еслибы ты написал проект который поддерживает несколько видов ORM и еще например получение данных с файлов, и показал пример как IOC контейнер помогает решить проблему переключени. В "музее юмора" IOC это лишнее звено, и статья не раскрывает сути зачем нужен IOC.