ASP.NET MVC: Unit of Work или продолжаем оптимизировать сайт

Сайтостроение | создано: 11.01.2013 | опубликовано: 12.01.2013 | обновлено: 13.01.2024 | просмотров: 29700 | всего комментариев: 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.

calabonga.net

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.

"Разрешение экземпляров, инъекции вливания, метка" - лол, вот это перлы. А по сути, зря вы отключаете lazy loading, ещё чуть побольше связей в модели и IIS обидится. А отключенная валидация увеличивает вероятность появления всяких неочевидных багов. И ещё нехорошо из репозитория возвращать IQueryable, все остальное приложение получает неконтролируемый доступ к БД и тестировать такое уже не получится.
"Разрешение экземпляров, инъекции вливания, метка" - лол, вот это перлы. А по сути, зря вы отключаете lazy loading, ещё чуть побольше связей в модели и IIS обидится. А отключенная валидация увеличивает вероятность появления всяких неочевидных багов. И ещё нехорошо из репозитория возвращать IQueryable, все остальное приложение получает неконтролируемый доступ к БД и тестировать такое уже не получится.