В этой статье я продемонстрирую реализацию внедрения зависимостей, репозитория и единиц работы с использованием Castle Windsor в качестве DI-контейнера и NHibernate в качестве инструмента объектно-реляционного сопоставления (ORM).
→ Скачать исходный код – 962 КБ
Внедрение зависимостей — это шаблон разработки программного обеспечения, который позволяет удалять и изменять жестко закодированные зависимости во время выполнения и компиляции.
[1] .
Репозиторий — это посредник между уровнями домена и отображения данных, который использует специальный интерфейс для доступа к объектам домена.
[2] .
Единица работы — это шаблон, который используется для определения транзакционных функций вашего приложения и управления ими.
[4] .
На тему реализующих сущностей и единиц работы существует множество статей, уроков и других ресурсов, поэтому я не буду их определять.
Данная статья посвящена решению трудностей, о которых мы поговорим позже.
Сложности
Разработка приложения, управляемого данными, должна следовать определенным принципам.Вот о каких принципах я хочу поговорить.
Как открывать и закрывать соединения Конечно, соединениями лучше всего управлять на уровне базы данных (в репозиториях).
Например, вы можете открыть соединение, выполнить команду в базе данных и закрыть соединение при каждом вызове метода репозитория.
Но этот вариант не будет эффективным, если нам нужно использовать одно и то же соединение для разных методов в одном репозитории или для разных методов в разных репозиториях (представьте себе транзакцию, использующую методы разных репозиториев).
При создании сайта (с помощью ASP.NET MVC или веб-форм) вы можете открыть соединение с помощью Application_BeginRequest и закрыть его с помощью Application_EndRequest. Но у этого подхода есть недостатки:
- База данных открывается и закрывается при каждом запросе, даже если не все из них используют базу данных.
Получается, что соединение берётся из пула даже тогда, когда оно не используется.
- База данных открывается в начале запроса и закрывается в конце.
Но иногда запрос оказывается слишком длинным, а работа с базой данных занимает небольшую часть его времени, что опять-таки приводит к неэффективному использованию пула соединений.
Но что, если вы разрабатываете не веб-сайт, а службу Windows, которая в течение некоторого времени запускает множество потоков, использующих базу данных? Где открывать и закрывать соединения в этом случае? Как управлять транзакциями Если ваше приложение (как и большинство приложений) использует транзакции базы данных, где вам следует начать, зафиксировать или откатить транзакцию? В методах репозитория это сделать невозможно — транзакция может включать в себя множество различных вызовов методов репозитория.
Следовательно, все эти операции могут выполняться на уровне домена.
Но у этого подхода есть и недостатки:
- Уровень предметной области включает код, специфичный для базы данных, что нарушает принцип единой ответственности и использование многоуровневого представления.
- Этот подход дублирует логику транзакции в каждом методе уровня домена.
Кроме того, вам придется при необходимости откатить некоторые из них после исправления ошибок.
Если вы разрабатываете приложение, а не веб-сайт, будет сложно найти подходящее место для запуска, фиксации и отката транзакций.
Поэтому лучше всего начинать транзакцию, когда она вам действительно нужна, фиксировать ее, если все ваши операции завершились успешно, и откатывать ее только в случае сбоя какой-либо из ваших операций.
Именно этим принципом я и дальше буду руководствоваться.
Выполнение
Мое приложение представляет собой телефонную книгу, созданную с использованием ASP.NET MVC (в качестве веб-инфраструктуры), Sql Server (в качестве СУБД), NHibernate (в качестве ORM) и Castle Windsor (в качестве контейнера для внедрения зависимостей).Сущности В моей реализации сущность преобразуется в запись таблицы в базе данных.
Сущность в доменно-ориентированном дизайне — это постоянный объект с уникальный идентификатор .
В этом случае все сущности являются производными от класса Entity, показанного ниже:
Сущность имеет уникальный идентификатор — первичный ключ, который может иметь разные типы (int, long, guid и т. д.).public interface IEntity<TPrimaryKey> { TPrimaryKey Id { get; set; } } public class Entity<TPrimaryKey> : IEntity<TPrimaryKey> { public virtual TPrimaryKey Id { get; set; } }
Соответственно, мы имеем дело с родовым классом, а сущности People, Phone, City и т.д. являются производными от него.
Вот как выглядит определение класса People:
public class Person : Entity<int>
{
public virtual int CityId { get; set; }
public virtual string Name { get; set; }
public virtual DateTime BirthDay { get; set; }
public virtual string Notes { get; set; }
public virtual DateTime RecordDate { get; set; }
public Person()
{
Notes = "";
RecordDate = DateTime.Now;
}
}
Как видите, первичный ключ Person определен как int. Преобразование сущности
Инструменты объектно-реляционного сопоставления, такие как Entity framework и NHibernate, требуют определения того, как сущности сопоставляются с таблицами базы данных.
Есть много способов реализовать это.
Например, я использовал NHibernate Fluent API. Вам необходимо определить класс преобразования для всех сущностей, как показано ниже, на примере сущности «Люди»: public class PersonMap : ClassMap<Person>
{
public PersonMap()
{
Table("People");
Id(x => x.Id).
Column("PersonId");
Map(x => x.CityId);
Map(x => x.Name);
Map(x => x.BirthDay);
Map(x => x.Notes);
Map(x => x.RecordDate);
Репозитории (уровень БД)
Репозитории используются для создания уровня базы данных для отделения логики доступа к данным от верхних уровней.Класс репозитория обычно создается для каждой сущности или агрегации — группы сущностей.
Я создал репозиторий для каждой сущности.
Сначала я определил интерфейс, который должен быть реализован всеми классами репозитория: /// <summary>
/// This interface must be implemented by all repositories to ensure UnitOfWork to work.
/// </summary>
public interface IRepository
{
}
/// <summary>
/// This interface is implemented by all repositories to ensure implementation of fixed methods.
/// </summary>
/// <typeparam name="TEntity">Main Entity type this repository works on</typeparam>
/// <typeparam name="TPrimaryKey">Primary key type of the entity</typeparam>
public interface IRepository<TEntity, TPrimaryKey> : IRepository where TEntity : Entity<TPrimaryKey>
{
/// <summary>
/// Used to get a IQueryable that is used to retrive entities from entire table.
/// </summary>
/// <returns>IQueryable to be used to select entities from database</returns>
IQueryable<TEntity> GetAll();
/// <summary>
/// Gets an entity.
/// </summary>
/// <param name="key">Primary key of the entity to get</param>
/// <returns>Entity</returns>
TEntity Get(TPrimaryKey key);
/// <summary>
/// Inserts a new entity.
/// </summary>
/// <param name="entity">Entity</param>
void Insert(TEntity entity);
/// <summary>
/// Updates an existing entity.
/// </summary>
/// <param name="entity">Entity</param>
void Update(TEntity entity);
/// <summary>
/// Deletes an entity.
/// </summary>
/// <param name="id">Id of the entity</param>
void Delete(TPrimaryKey id);
}
Таким образом, все классы репозитория должны реализовывать вышеуказанные методы.
Но NHibernate имеет почти аналогичную реализацию этих методов.
Оказывается, вы можете определить базовый класс для всех репозиториев, не применяя ко всем одну и ту же логику.
Определение NhRepositoryBase показано ниже: /// <summary>
/// Base class for all repositories those uses NHibernate.
/// </summary>
/// <typeparam name="TEntity">Entity type</typeparam>
/// <typeparam name="TPrimaryKey">Primary key type of the entity</typeparam>
public abstract class NhRepositoryBase<TEntity, TPrimaryKey> : IRepository<TEntity, TPrimaryKey> where TEntity : Entity<TPrimaryKey>
{
/// <summary>
/// Gets the NHibernate session object to perform database operations.
/// </summary>
protected ISession Session { get { return NhUnitOfWork.Current.Session; } }
/// <summary>
/// Used to get a IQueryable that is used to retrive object from entire table.
/// </summary>
/// <returns>IQueryable to be used to select entities from database</returns>
public IQueryable<TEntity> GetAll()
{
return Session.Query<TEntity>();
}
/// <summary>
/// Gets an entity.
/// </summary>
/// <param name="key">Primary key of the entity to get</param>
/// <returns>Entity</returns>
public TEntity Get(TPrimaryKey key)
{
return Session.Get<TEntity>(key);
}
/// <summary>
/// Inserts a new entity.
/// </summary>
/// <param name="entity">Entity</param>
public void Insert(TEntity entity)
{
Session.Save(entity);
}
/// <summary>
/// Updates an existing entity.
/// </summary>
/// <param name="entity">Entity</param>
public void Update(TEntity entity)
{
Session.Update(entity);
}
/// <summary>
/// Deletes an entity.
/// </summary>
/// <param name="id">Id of the entity</param>
public void Delete(TPrimaryKey id)
{
Session.Delete(Session.Load<TEntity>(id));
}
}
Свойство сеанса используется для получения объекта сеанса (объекта подключения к базе данных в NHibernate) из NhUnitOfWork.Current.Session, который получает правильный объект сеанса для текущей транзакции.
Таким образом, вам не придется решать, как открывать и закрывать соединение или транзакцию.
Далее я постараюсь подробнее остановиться на описании этого механизма.
Все операции CRUD реализованы по умолчанию для всех репозиториев.
Теперь вы можете создать PersonRepository с возможностью выбирать, обновлять и удалять записи.
Для этого вам необходимо объявить два типа, как показано ниже.
public interface IPersonRepository : IRepository<Person, int>
{
}
public class NhPersonRepository : NhRepositoryBase<Person, int>, IPersonRepository
{
}
То же самое можно сделать и для сущностей «Телефон» и «Город».
Если вам нужно добавить собственный метод репозитория, вы можете сделать это в репозитории соответствующей сущности.
Например, добавьте в PhoneRepository новый метод, чтобы можно было удалять телефоны конкретного человека: public interface IPhoneRepository : IRepository<Phone, int>
{
/// <summary>
/// Deletes all phone numbers for given person id.
/// </summary>
/// <param name="personId">Id of the person</param>
void DeletePhonesOfPerson(int personId);
}
public class NhPhoneRepository : NhRepositoryBase<Phone, int>, IPhoneRepository
{
public void DeletePhonesOfPerson(int personId)
{
var phones = GetAll().
Where(phone => phone.PersonId == personId).
ToList();
foreach (var phone in phones)
{
Session.Delete(phone);
}
}
}
Единица работы
Единица работы используется для определения транзакционных функций приложения и управления ими.
Прежде всего нам нужно определить интерфейс IUnitOfWork: /// <summary>
/// Represents a transactional job.
/// </summary>
public interface IUnitOfWork
{
/// <summary>
/// Opens database connection and begins transaction.
/// </summary>
void BeginTransaction();
/// <summary>
/// Commits transaction and closes database connection.
/// </summary>
void Commit();
/// <summary>
/// Rollbacks transaction and closes database connection.
/// </summary>
void Rollback();
}
Реализация IUnitOfWork для NHibernate показана ниже: /// <summary>
/// Implements Unit of work for NHibernate.
/// </summary>
public class NhUnitOfWork : IUnitOfWork
{
/// <summary>
/// Gets current instance of the NhUnitOfWork.
/// It gets the right instance that is related to current thread.
/// </summary>
public static NhUnitOfWork Current
{
get { return _current; }
set { _current = value; }
}
[ThreadStatic]
private static NhUnitOfWork _current;
/// <summary>
/// Gets Nhibernate session object to perform queries.
/// </summary>
public ISession Session { get; private set; }
/// <summary>
/// Reference to the session factory.
/// </summary>
private readonly ISessionFactory _sessionFactory;
/// <summary>
/// Reference to the currently running transcation.
/// </summary>
private ITransaction _transaction;
/// <summary>
/// Creates a new instance of NhUnitOfWork.
/// </summary>
/// <param name="sessionFactory"></param>
public NhUnitOfWork(ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
/// <summary>
/// Opens database connection and begins transaction.
/// </summary>
public void BeginTransaction()
{
Session = _sessionFactory.OpenSession();
_transaction = Session.BeginTransaction();
}
/// <summary>
/// Commits transaction and closes database connection.
/// </summary>
public void Commit()
{
try
{
_transaction.Commit();
}
finally
{
Session.Close();
}
}
/// <summary>
/// Rollbacks transaction and closes database connection.
/// </summary>
public void Rollback()
{
try
{
_transaction.Rollback();
}
finally
{
Session.Close();
}
}
}
Статическое свойство Current является ключевым для всего класса.
Он получает и устанавливает поле _current, отмечен как ThreadStatic .
Таким образом, я могу использовать один и тот же объект единицы работы в одном потоке.
Это означает, что несколько объектов могут использовать одно соединение или транзакцию.
Наконец, я определяю атрибут, используемый для обозначения метода как транзакционного: /// <summary>
/// This attribute is used to indicate that declaring method is transactional (atomic).
/// A method that has this attribute is intercepted, a transaction starts before call the method.
/// At the end of method call, transaction is commited if there is no exception, othervise it's rolled back.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class UnitOfWorkAttribute : Attribute
{
}
Если определенный метод должен быть транзакционным, его необходимо пометить атрибутом UnitOfWork. Затем я перехватываю эти методы, используя внедрение зависимостей, как показано ниже.
Уровень обслуживания При проектировании на основе домена службы домена используются для реализации логики домена и могут использовать репозитории для выполнения задач базы данных.
Например, вот как выглядит определение PersonService: public class PersonService : IPersonService
{
private readonly IPersonRepository _personRepository;
private readonly IPhoneRepository _phoneRepository;
public PersonService(IPersonRepository personRepository, IPhoneRepository phoneRepository)
{
_personRepository = personRepository;
_phoneRepository = phoneRepository;
}
public void CreatePerson(Person person)
{
_personRepository.Insert(person);
}
[UnitOfWork]
public void DeletePerson(int personId)
{
_personRepository.Delete(personId);
_phoneRepository.DeletePhonesOfPerson(personId);
}
//.
some other methods are not shown here since it's not needed. See source codes.
}
Обратите внимание на использование атрибута UnitOfWork, определенного выше.
Метод DeletePerson помечен как UnitOfWork. Он вызывает два разных метода репозитория, и эти вызовы методов должны быть транзакционными.
С другой стороны, метод CreatePerson не помечен как UnitOfWork, поскольку он вызывает только один метод репозитория, Insert в репозитории person, который может управлять собственной транзакцией: открывать и закрывать ее.
Далее мы увидим, как это реализовано.
Внедрение зависимостей (DI) Контейнеры DI, такие как Castle Windsor, используются для управления зависимостями и жизненными циклами объекта приложения.
Это позволяет вам создавать слабосвязанные компоненты и модули внутри вашего приложения.
В приложении ASP.NET контейнер DI обычно инициализируется в файле global.asax. Обычно это происходит при запуске.
public class MvcApplication : System.Web.HttpApplication
{
private WindsorContainer _windsorContainer;
protected void Application_Start()
{
InitializeWindsor();
// Other startup logic.
}
protected void Application_End()
{
if (_windsorContainer != null)
{
_windsorContainer.Dispose();
}
}
private void InitializeWindsor()
{
_windsorContainer = new WindsorContainer();
_windsorContainer.Install(FromAssembly.This());
ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(_windsorContainer.Kernel));
}
}
Объект WindsowContainer, который является ключевым для внедрения зависимостей, создается при запуске приложения и удаляется в конце.
Для внедрения зависимостей вам также необходимо изменить настройки по умолчанию фабрики контроллера MVC в методе InitializeWindsor. Всякий раз, когда шаблону ASP.NET MVC требуется контроллер (в каждом веб-запросе), он создает его, используя внедрение зависимости.
Более подробную информацию о замке Виндзор можно найти.
Здесь .
Вот как выглядит фабрика контроллеров: public class WindsorControllerFactory : DefaultControllerFactory
{
private readonly IKernel _kernel;
public WindsorControllerFactory(IKernel kernel)
{
_kernel = kernel;
}
public override void ReleaseController(IController controller)
{
_kernel.ReleaseComponent(controller);
}
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
if (controllerType == null)
{
throw new HttpException(404, string.Format("The controller for path '{0}' could not be found.", requestContext.HttpContext.Request.Path));
}
return (IController)_kernel.Resolve(controllerType);
}
}
Это довольно просто и понятно даже на первый взгляд. Вам следует внедрить наши собственные объектные зависимости с помощью класса PhoneBookDependencyInstaller. Castle Windsor автоматически находит этот класс благодаря реализации IWindsorInstaller. Помните строку _windsorContainer.Install(FromAssembly.This()); в файле global.asax. public class PhoneBookDependencyInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Kernel.ComponentRegistered += Kernel_ComponentRegistered;
//Register all controllers
container.Register(
//Nhibernate session factory
Component.For<ISessionFactory>().
UsingFactoryMethod(CreateNhSessionFactory).
LifeStyle.Singleton, //Unitofwork interceptor Component.For<NhUnitOfWorkInterceptor>().
LifeStyle.Transient, //All repoistories Classes.FromAssembly(Assembly.GetAssembly(typeof(NhPersonRepository))).
InSameNamespaceAs<NhPersonRepository>().
WithService.DefaultInterfaces().
LifestyleTransient(), //All services Classes.FromAssembly(Assembly.GetAssembly(typeof(PersonService))).
InSameNamespaceAs<PersonService>().
WithService.DefaultInterfaces().
LifestyleTransient(), //All MVC controllers Classes.FromThisAssembly().
BasedOn<IController>().
LifestyleTransient() ); } /// <summary> /// Creates NHibernate Session Factory. /// </summary> /// <returns>NHibernate Session Factory</returns> private static ISessionFactory CreateNhSessionFactory() { var connStr = ConfigurationManager.ConnectionStrings["PhoneBook"].
ConnectionString; return Fluently.Configure() .
Database(MsSqlConfiguration.MsSql2008.ConnectionString(connStr)) .
Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetAssembly(typeof(PersonMap)))) .
BuildSessionFactory();
}
void Kernel_ComponentRegistered(string key, Castle.MicroKernel.IHandler handler)
{
//Intercept all methods of all repositories.
if (UnitOfWorkHelper.IsRepositoryClass(handler.ComponentModel.Implementation))
{
handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(NhUnitOfWorkInterceptor)));
}
//Intercept all methods of classes those have at least one method that has UnitOfWork attribute.
foreach (var method in handler.ComponentModel.Implementation.GetMethods())
{
if (UnitOfWorkHelper.HasUnitOfWorkAttribute(method))
{
handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(NhUnitOfWorkInterceptor)));
return;
}
}
}
}
Как видите, я регистрирую все компоненты с помощью метода Register в Castle Windsor. Обратите внимание, что все классы репозитория прописываются в одной строке.
То же самое нужно сделать для сервисов и контроллеров.
Я использую фабричный метод для создания ISessionFactory, который создает объекты ISession (соединение с БД) для использования с NHibernate. В начале метода Install я регистрирую событие ComponentRegistered для реализации логики перехвата.
Взгляните на Kernel_ComponentRegistered. Если метод является методом репозитория, я всегда буду использовать для него перехват. Кроме того, если метод помечен атрибутом UnitOfWork, он также перехватывается классом NhUnitOfWorkInterceptor. Перехват Перехват — это специальный метод, позволяющий выполнить некоторый код в начале и конце вызова метода.
Перехват обычно используется для логирования, профилирования, кэширования и т. д. Он позволяет динамически внедрять код в необходимые методы, не меняя сами методы.
В нашем случае перехват полезен для реализации единицы работы.
Если определенный метод является методом репозитория или помечен атрибутом UnitOfWork (описан выше), я открываю соединение с базой данных и (Session в NHibernate) и запускаю транзакцию в начале метода.
Если перехваченный метод не генерирует никаких исключений, транзакция фиксируется в конце метода.
Если метод выдает исключение, вся транзакция откатывается.
Давайте посмотрим на мою реализацию класса NhUnitOfWorkInterceptor:
/// <summary>
/// This interceptor is used to manage transactions.
/// </summary>
public class NhUnitOfWorkInterceptor : IInterceptor
{
private readonly ISessionFactory _sessionFactory;
/// <summary>
/// Creates a new NhUnitOfWorkInterceptor object.
/// </summary>
/// <param name="sessionFactory">Nhibernate session factory.</param>
public NhUnitOfWorkInterceptor(ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
/// <summary>
/// Intercepts a method.
/// </summary>
/// <param name="invocation">Method invocation arguments</param>
public void Intercept(IInvocation invocation)
{
//If there is a running transaction, just run the method
if (NhUnitOfWork.Current != null || !RequiresDbConnection(invocation.MethodInvocationTarget))
{
invocation.Proceed();
return;
}
try
{
NhUnitOfWork.Current = new NhUnitOfWork(_sessionFactory);
NhUnitOfWork.Current.BeginTransaction();
try
{
invocation.Proceed();
NhUnitOfWork.Current.Commit();
}
catch
{
try
{
NhUnitOfWork.Current.Rollback();
}
catch
{
}
throw;
}
}
finally
{
NhUnitOfWork.Current = null;
}
}
private static bool RequiresDbConnection(MethodInfo methodInfo)
{
if (UnitOfWorkHelper.HasUnitOfWorkAttribute(methodInfo))
{
return true;
}
if (UnitOfWorkHelper.IsRepositoryMethod(methodInfo))
{
return true;
}
return false;
}
}
Перехват — основной метод. Сначала он проверяет, существует ли ранее запущенная транзакция для данного потока.
Если да, то он не запускает новую транзакцию, а использует текущую (см.
NhUnitOfWork.Current).
Таким образом, вызовы вложенных методов с атрибутом UnitOfWork могут использовать одну и ту же транзакцию.
Транзакция создается или фиксируется только при первом использовании метода единицы работы.
Кроме того, если метод не является транзакционным, он просто вызывает и возвращает результат. Команда invoction.Proceed() выполняет вызов перехваченного метода.
Если текущих транзакций нет, нужно начать новую транзакцию и зафиксировать ее, если нет ошибок.
В противном случае вам придется откатить его обратно.
После этого следует установить NhUnitOfWork.Current = null, чтобы потом, при необходимости, можно было запускать другие транзакции для этого потока.
Возможно, вам также захочется взглянуть на класс UnitOfWorkHelper. Таким образом, код для открытия и закрытия соединений, а также для запуска, фиксации и отката транзакций определяется только в одной точке приложения.
Теги: #nhibernate #windsor #ASP.NET #программирование #.
NET #Разработка мобильных приложений #ASP #ASP
-
Киберцветок
19 Oct, 24 -
Рекламная Кампания Началась
19 Oct, 24