ASP.NET MVC: Unit of Work или продолжаем оптимизировать сайт
Сайтостроение | создано: 11.01.2013 | опубликовано: 12.01.2013 | обновлено: 13.01.2024 | просмотров: 30166 | всего комментариев: 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.