Репозиторий + UnitOfWork с упором на атомарность. Создание программного обеспечения с правильным шаблоном дизайна.

Иногда назад я читал статью о том, чем старший разработчик отличается от среднего и младшего, в то время как в статье подчеркивается тот факт, что старший разработчик не мыслит категориями отдельных классов, методов, функций или низкоуровневых разработчиков. технические подробности, они думают в терминах объектно-ориентированных шаблонов проектирования и разработки приложений. Вдобавок я твердо верю, что скачок в рейтинге разработчика основан на тенденции постоянно задавать себе вопросы. Да, вы смогли сократить время отклика, но можно ли его улучшить? Вы предлагаете алгоритм решения задачи, какова временная сложность?

Со временем у меня развился этот тип личности, и это поднимает мне голову всякий раз, когда я к чему-то стремлюсь, независимо от того, насколько просто я в большинстве случаев опасаюсь заниматься помолвкой.

Я начал программировать как Backend Engineer, в основном используя .Net. Каждый раз, когда я пишу коды для взаимодействия с базой данных, у меня в голове всегда возникает множество вопросов. Самые большие из них:

Q1: Как мне создать повторно используемый шаблон проектирования, который не зависит от репозитория с поддержкой включения реализации по желанию без каких-либо изменений со стороны потребителя.

Q2: Можно ли одновременно поместить в базу данных столько сущностей? вместо того, чтобы писать их 1 на 1. Разве это не значительно улучшит время отклика.

Q3: Если отправка в хранилище данных не удалась, как можно гарантировать, что хранилище данных останется согласованным, удалив текущее обновление?

Я знаю, что вы говорите: Этого довольно легко достичь, многие фреймворки уже делают это возможным. Что ж, я тоже знаю, потому что я использовал Entityframework в .net и с Entityframework, это довольно просто, за исключением того, что EF доступен не для всех хранилищ данных, а преимущество EF в режиме отложенной загрузки может быть чрезмерно злоупотреблено большинством разработчиков, что приведет к перегруженным запросам. с ударом по производительности.

Q4: Что, если вы окажетесь в ситуации, когда вам потребуется 2 или 3 источника данных? С таким фреймворком, как EF? Ну, может быть, реализовать несколько DbContext, каждый со своей собственной строкой подключения, но что, если вы не хотите быть подчиненным для EF, что, если EF не поддерживает одно из хранилищ данных?

Q5: Кроме того, скажем, завтра будет выпущено что-то большее, чем EF, насколько гибко вы можете интегрировать, не отказываясь от всей кодовой базы проекта? Многие могут согласиться выйти замуж за фреймворк и придерживаться его вечно, но я нет.

Все эти вопросы привели меня к тому, чтобы найти ответ - архитектуру дизайна, которая использует репозиторий и шаблон единицы работы с акцентом на обеспечение атомарности. В этой статье я расскажу о мотивах такого паттерна проектирования, а также пошагово рассмотрю его реализацию. Я оставлю ссылку репозиторий github в конце статьи, если кто-то захочет просмотреть всю базу кода.

Чтобы ответить на первый вопрос: «Как мне создать повторно используемый шаблон проектирования, не зависящий от репозитория», я по умолчанию использовал интерфейс IRepository. Реализация выглядит так:

public interface IRepository<T> where T:class,IEntity
{
Task<List<T>> GetEntitiesAsync(GetOption<T> option = null);
Task<T> GetEntityAsync(string entityId, GetOption<T> option = null);
Task<CrudResult<string>> PersistAddEntityAsync(T entity);
Task<CrudResult<bool>> PersistUpdateEntityAsync( T entity);
Task<CrudResult<bool>> PersistDeleteEntityAsync(T entity);
}

Здесь я должен кое-что упомянуть. Шаблон дизайна - это все о дизайн-мышлении. Мне не нужно выбирать новейший шаблон проектирования Google или Microsoft, а затем придерживаться его на всю жизнь. Если вы понимаете необходимость и необходимость создания масштабируемого решения, вы подумаете, как лучше всего написать свой код для его достижения. Это именно то, что ребята из Microsoft и Google умеют делать лучше всего, поэтому они придумали лучший шаблон дизайна для всех нас.

Итак, вернемся к нашему маленькому репозиторию. Как это решает проблему повторного использования?

Одно из правил шаблона проектирования - кодировать интерфейс, а не реализацию, и это именно то, что мы здесь сделали. Чтобы создать функциональный репозиторий, который действительно может сохраняться в хранилище данных, требуется реализация IRepository. Каждый репозиторий может определять, как и что он использовал для хранения данных. Однако потребителю нашего сервиса не нужно ничего знать об этих реализациях, потому что мы будем предоставлять только тип IRepository.

Предположим, что нам нужно регистрировать каждую регистрацию в нашей системе. Допустим, мы аутентифицируем пользователя из источника данных, такого как SQL Server, и записываем наш журнал в файл. В этом случае нам понадобятся два абстрактных класса, унаследованных от RepositoryBase. UserRepositoryBase и LogRepositoryBase расширяют нашу RepositoryBase.

public abstract class UserRepositoryBase:IRepository<User>
{
}
public abstract class LogRepositoryBase:IRepository<Log>
{
}

С этими производными расширениями UserRepositoryBase и LogRepositoryBase мы сделали это на всю жизнь. Чтобы обеспечить аутентификацию пользователей с помощью SQL Server, все, что необходимо, - это предоставить и реализовать UserRepositoryBase, возможно, назвать его SQLUserRepository, а для журнала предоставить реализацию LogRepositoryBase, которая знает, как использовать класс FileStream.

Помните, что все, что мы предоставляем нашему потребителю, - это базовый класс. В случае UserRepository мы никогда не будем раскрывать реализацию SQLUserRepository, скорее, мы всегда будем предоставлять UserRepositoryBase. При таком подходе, если мы решим перенести наши пользовательские данные в другой источник данных завтра, скажем Oracle, потребителю нашей службы не нужно будет вносить какие-либо изменения. Все, что нам нужно сделать, это просто предоставить реализацию UserRepositoryBase, которая знает, как взаимодействовать с Oracle DB, и просто внедрить нашу новую реализацию, чтобы заменить старый SQLUserRepository.

Дело 1 решено. Но впереди еще долгий путь.

PS: Я видел много решений для первого квартала, не допускающих специфического для модели создания IRepository, как в случае UserRepositoryBase и LogRepositoryBase. Они делают что-то вроде этого:

public class SQLRepository<T>:IRepository<T>
{
}

Мое первое мнение об этом подходе состоит в том, что это ленивый подход, и он скрывает свою лень, заставляя методы Get IRepository принимать выражение ‹T›, забывая, что не каждый механизм источников данных будет поддерживать LINQ для создания выражений, например, хранилище таблиц Azure и Redis. .

Большинство разработчиков, идущих в этом направлении, поступают так, потому что не хотят заранее предвосхищать необходимое поведение, ожидаемое от каждой сущности, а затем предоставлять метод для их выполнения. Скорее они хотят полагаться на общий метод Get, который предлагает синтаксис выражения для извлечения чего угодно в любое время. Это делает кодовую базу очень сложной и неструктурированной.

Теперь к Q2; Можно ли поместить сразу столько объектов во все хранилище данных? вместо того, чтобы писать их 1 на 1?

Скажем, в нашем случае пользователь и журнал. Помните, что мы подталкиваем нашего пользователя к SQL Server, а наш журнал переходит в файл.

У таких фреймворков, как EntityFramework, есть шаблон, который отвечает на эти вопросы. Шаблон EF UnitOfWork, DbContext, позволяет добавлять несколько сущностей перед вызовом SubmitChanges. Но мотивация к работе над этой архитектурой заключается не только в том, чтобы создать шаблон, который будет независимым только от хранилища, мы также имеем в виду создание шаблона проектирования, который не будет зависеть от Framework. Это означает, что вы по-прежнему можете использовать EF в нашем шаблоне проектирования, а также в других фреймворках, и все они будут работать вместе с минимальной или нулевой работой со стороны разработчика.

Итак, чтобы обеспечить одновременную отправку нескольких объектов, наша архитектура должна поддерживать шаблон UniOfWork. Шаблон UniOfWork управляет RepositoryBase, сообщая ему, когда следует сохранять. Успешно реализовав шаблон uniofwork над нашим IRepository, несколько объектов могут быть поставлены в очередь с использованием нашего шаблона UniOfWork, а затем зафиксированы все сразу.

Наш интерфейс UnifOfWork выглядит так.

public interface IUnitOfWork
{
void RegisterDelete(IEntity entity, UnitOfWorkRepositoryBase<IEntity> entityRepository);
void RegisterUpdate(IEntity entity, UnitOfWorkRepositoryBase<IEntity> entityRepository);
void RegisterAdd(IEntity entity, UnitOfWorkRepositoryBase<IEntity> entityRepository);
Task<bool> CommitChangesAsync();
}

Внедрение unitofwork для управления всем нашим IRepository также представляет кое-что новое, UnitOfWorkRepositoryBase.

Изначально у нас есть IRepository, почему снова UnitOfWorkRepositoryBase?

Что ж, то, что здесь происходит, - это, по сути, разделение проблем. Основная функция нашего IRepository - сохранять и извлекать данные из нашего магазина. Но с появлением UnitOfWork любой репозиторий, который будет с ним работать, должен определить способ хранения его сущности с помощью UnitOfWork.

Итак, вот скелетное определение нашего UnitOfWorkRepositoryBase

public abstract class UnitOfWorkRepositoryBase<T>: IRepository<T> where T:class,IEntity
{
private IUnitOfWork unitOfWork;
public UnitOfWorkRepositoryBase(IUnitOfWork _unitOfWork)
{
this.unitOfWork = _unitOfWork ?? throw new Exception("IUnitOfWork instance is require for class initialization");
}
public abstract Task<List<T>> GetEntitiesAsync(GetOption<T> option = null);
public abstract Task<T> GetEntityAsync(string entityId, GetOption<T> option = null);
public abstract Task<CrudResult<string>> PersistAddEntityAsync(T entity);
public abstract Task<CrudResult<bool>> PersistUpdateEntityAsync(T entity);
public abstract Task<CrudResult<bool>> PersistDeleteEntityAsync(T entity);
//
public void QueueForRemove(T entity)
{
this.unitOfWork.RegisterDelete(entity, this as UnitOfWorkRepositoryBase<IEntity>);
}
public void QueueForUpdate(T entity)
{
this.unitOfWork.RegisterUpdate(entity, this as UnitOfWorkRepositoryBase<IEntity>);
}
public void QueueForAdd(T entity)
{
this.unitOfWork.RegisterAdd(entity, this as UnitOfWorkRepositoryBase<IEntity>);
}
}

Во-первых, мы гарантируем, что любая реализация UnitOfWorkRepositoryBase передает экземпляр IUnitOfWork, который позволяет ему принимать участие в транзакционной операции. Мы генерируем исключение, если передано значение null.

Следующие методы передают реализацию интерфейса IRepository производному классу, который будет реализовывать UnitOfWorkRepository.

Затем у нас есть реальные методы, позволяющие нашему репозиторию участвовать в единице транзакции. Шаблон UnitOfWork в основном связан с операциями записи, такими как Create, Delete и Update, что является мотивацией для 3 поведения UnitOfWork esque QueueAdd, QueueDelete и QueueUpdate. Хотя мы могли бы обойтись без этих методов в нашем UnitOfWorkRepositoryBase, используя только экземпляр UnitOfWork для регистрации изменений сущности перед фиксацией, но с такой гибкостью, это означает, что как с экземпляром unitOfWork, так и с экземпляром UnitOfWorkRepositoryBase мы можем зарегистрировать сущности, которые будут зафиксированы экземпляр unitOfWork.

Наш шаблон формируется, но у нас есть проблема, небольшая проблема, которую мы почти не замечаем. Если вы внимательно посмотрите на нашу UnitOfWorkRepositoryBase, вы поймете, что все его производные классы должны будут реализовать общедоступные методы интерфейса IRepository, такие как GetEntity, GetEntities, PersistDelete, PersistAdd и PersistUpdate. Проблема в том, что когда вы обращаетесь к экземпляру UnitOfWorkRepositoryBase, у вас по-прежнему будет доступ к PersistDelete, PersistAdd и PersistUpdate, которые потребитель нашего сервиса может неправильно использовать, отказавшись от того факта, что с помощью UnitOfWorkRepositoryBase мы хотим заставить вас обеспечить согласованность транзакций.

public class UserRepositoryBase:UnitOfWorkRepositoryBase<User>
{
.
.
.
.
}
public class SQLUserRepository:UserRepositoryBase{
.
.
.
.
}
static void main(){
IUnitOfWork unitOfWork= UnitofWork.instance;
UserRepositoryBase userRepository= new SQLUserRepository(unitOfWork);
//If this 2 users must be added together,
//A mis-used can come like this
User user1=new User(){...};
User user2=new User(){...};
userRepository.PersistAdd(user1);
userRepository.PersistAdd(user2);
//instead of  
userRepository.QueueForAdd(user1);
userRepository.QueueForAdd(user2);
unitOfWork.CommitChanges();
//or like this
unitOfWork.RegisterAdd(user1,userRepository);
unitOfWork.RegisterAdd(user2,userRepository);
unitOfWork.CommitChanges();
}

Итак, наш лучший вариант - скрыть такое поведение, как PersistUpdate, PersistDelete и PersistAdd, из экземпляров UnitOfWorkRepositoryBase и сделать это?…. мы должны сделать их «защищенными», что подразумевает, что только классы, наследующие UnitOfWorkRepositoryBase, смогут обеспечить реализацию только для PersistAdd, PersistDelete и PersistUpdate. Однако, если мы изменим модификатор с общедоступного на защищенный, мы нарушим правило реализации интерфейса, в котором говорится, что модификатор доступа интерфейса не может быть изменен в производном классе. Итак, как мы идем? Ну, а пока мы прощаемся с интерфейсом IRepository и позволяем UnitOfWorkRepositoryBase проектировать свое собственное базовое поведение. В этом направлении наша UnitOfWorkRepositoryBase будет выглядеть так:

public abstract class UnitOfWorkRepositoryBase<T> where T:class,IEntity
{
private IUnitOfWork unitOfWork;
public UnitOfWorkRepositoryBase(IUnitOfWork _unitOfWork)
{
this.unitOfWork = _unitOfWork ?? throw new Exception("IUnitOfWork instance is require for class initialization");
}
public abstract Task<List<T>> GetEntitiesAsync(GetOption<T> option = null);
public abstract Task<T> GetEntityAsync(string entityId, GetOption<T> option = null);
protected abstract Task<CrudResult<string>> PersistAddEntityAsync(T entity);
protected abstract Task<CrudResult<bool>> PersistUpdateEntityAsync(T entity);
protected abstract Task<CrudResult<bool>> PersistDeleteEntityAsync(T entity);
//
public void QueueForRemove(T entity)
{
this.unitOfWork.RegisterDelete(entity, this as UnitOfWorkRepositoryBase<IEntity>);
}
public void QueueForUpdate(T entity)
{
this.unitOfWork.RegisterUpdate(entity, this as UnitOfWorkRepositoryBase<IEntity>);
}
public void QueueForAdd(T entity)
{
this.unitOfWork.RegisterAdd(entity, this as UnitOfWorkRepositoryBase<IEntity>);
}
}

Хорошо, дело сделано. Теперь вызов кода, подобный приведенному выше userRepository.PersistAdd (user1), вызовет ошибку времени компиляции, потому что экземпляр нашего UnitOfWorkRepositoryBase не имеет к нему доступа.

Теперь стоит подумать о том, как unitOfWork обрабатывает нашу единицу транзакции. В нашей архитектуре по умолчанию реализован интерфейс IUnitOfWork. Весь исходный код доступен на github, но вот фрагмент.

public class UnitOfWork : IUnitOfWork
{
Dictionary<IEntity, UnitOfWorkRepositoryBase<IEntity>> deletedEntities = new Dictionary<IEntity, UnitOfWorkRepositoryBase<IEntity>>();
Dictionary<IEntity, UnitOfWorkRepositoryBase<IEntity>> updatedEntities = new Dictionary<IEntity, UnitOfWorkRepositoryBase<IEntity>>();
Dictionary<IEntity, UnitOfWorkRepositoryBase<IEntity>> addedEntities = new Dictionary<IEntity, UnitOfWorkRepositoryBase<IEntity>>();
private static UnitOfWork instance = null;
public static UnitOfWork Instance
{
get
{
if (instance == null)
instance = new UnitOfWork();
return instance;
}
}
private UnitOfWork()
{
}
public void RegisterDelete(IEntity entity, UnitOfWorkRepositoryBase<IEntity> entityRepository)
{
this.deletedEntities.Add(entity, entityRepository);
}
public void RegisterUpdate(IEntity entity, UnitOfWorkRepositoryBase<IEntity> entityRepository)
{
this.updatedEntities.Add(entity, entityRepository);
}
public void RegisterAdd(IEntity entity, UnitOfWorkRepositoryBase<IEntity> entityRepository)
{
this.addedEntities.Add(entity, entityRepository);
}
public Task<bool> CommitChangesAsync()
{
.
.
.
}
}

Стоит обратить внимание на то, что мы определили нашу реализацию UnitOfWork, используя шаблон проектирования Singleton. Шаблон Singleton ограничивает доступ к UnitOfWork только одним экземпляром в любом месте и в любое время.

Причина этого - не что иное, как наша цель обеспечить единую точку фиксации для всех наших репозиториев. Если мы разрешили создание и использование нескольких экземпляров UnitOfWork, неправильное использование могло бы возникнуть следующим образом:

static void main(){
IUnitOfWork unitOfWork1= new UnitOfWork();
UserRepositoryBase userRepository= new SQLUserRepository(unitOfWork);
IUnitOfWork unitOfWork2=new UnitOfWork();
LogRepositoryBase logRepository= new FileLogRepository(unitOfWork2);
//A mis-used can come like this
User user=new User(){...};
Log log =new Log(){...};
unitOfWork1.RegisterForAdd(user,userRepository);
unitOfWork2.RegisterForAdd(log,logRepository);
//then follow with
unitOfWork1.CommitChanges();
uniOfWork2.CommitChanges();
}

Очевидно, что вышеупомянутое будет работать, но если unitOfWork1 завершится с ошибкой во время фиксации, это не приведет к откату unitOfWork2, что подорвет наше представление об атомарности.

Затем мы определяем объект словаря, который действует как очередь для хранения сущностей, участвующих в единице транзакции. Как вы помните, наш UnitOfWorkRepositoryBase предоставляет такие методы, как QueueForAdd, QueueForUpdate и QueueForDelete. Эти методы вызывают эквивалент unitOfWork Rgister. Эти методы являются необязательным определением в нашем UnitOfWorkRepositoryBase, но дают нам гибкость для передачи сущностей в экземпляр unitOfWork либо через сам экземпляр unitOfWork, либо через экземпляр UnitOfWorkRepositoryBase.

Метод commitChangesAsync () выполняет фактическое сохранение сущностей.

Вопрос 2 снят. Рядом с вопросом 3 говорится:

«Если принудительная отправка в базу данных не удалась, как можно гарантировать, что база данных останется согласованной, удалив текущее обновление?»

Меня это особенно беспокоит. Поскольку от разработчиков не требуется реализации, я хочу, чтобы все производные классы UnitOfWorkRepositoryBase автоматически обеспечивали атомарность. В настоящее время, пока unitOfWork фиксирует изменения, если одна из сущностей не может быть добавлена, операция должна прерваться, а затем откатиться предыдущие фиксации. Это предотвращает сохранение частичной фиксации в нашем хранилище данных.

Чтобы гарантировать, что каждый из наших UnitOfWorkRepositoryBase будет выполнять откат операции при выходе из строя единицы операции, мне нужно было обратиться к полезному интерфейсу, предоставляемому .NET Framework, - интерфейсу IEnlistmentNotification.

Платформа .net обеспечивает поддержку транзакций, начиная с .net 2.0. Красота этого пространства имен Transaction заключается в решении проблем, которые могут привести к несогласованности транзакций - проблеме, с которой мы сейчас сталкиваемся.

Но прежде чем какая-либо операция сможет воспользоваться этой полезной транзакционной парадигмой, необходимы две вещи.

  1. Менеджер ресурсов
  2. Менеджер транзакций

К счастью для нас, .net предоставляет нам менеджеров транзакций, таких как MSDTC (координатор распределенных транзакций Microsoft), который управляет всеми менеджерами ресурсов, участвующими в транзакции. Скажите им, когда совершить или прервать действие.

Однако диспетчер ресурсов должен зарегистрироваться в диспетчере транзакций, и именно этого помогает IEnlistmentNotification. Для любого менеджера ресурсов, который хочет принять участие в транзакции, он должен реализовать интерфейс IEnlistmentNotification.

Итак, на данном этапе моя мысль состоит в том, чтобы сделать наш UnitOfWorkRepositoryBase ResourceManager, и это подразумевает реализацию IEnlistmentNotification, ожидаемого диспетчером транзакций.

IEnlistementNotification предоставляет 3 метода со следующими подписями

  1. void Commit (зачисление в список)
  2. void InDoubt (зачисление в список)
  3. void Prepare (Подготовка к зачислению, подготовка к зачислению)
  4. void Откат (зачисление в список)

При этом наша UnitOfWorkRepositoryBase становится,

public abstract class UnitOfWorkRepositoryBase<T> :  IEnlistmentNotification where T:class,IEntity
{
Boolean enlisted;
Dictionary<IEntity, EnlistmentOperations> transactionElements = new Dictionary<IEntity, EnlistmentOperations>();
.
.
.
.
void enlistForTransaction()
{
if (this.enlisted == false)
{
Transaction.Current.EnlistVolatile(this, EnlistmentOptions.None);
this.enlisted = true;
}
}
public void Commit(Enlistment enlistment)
{
enlistment.Done();
}
public void InDoubt(Enlistment enlistment)
{
Rollback(enlistment);
}
public void Prepare(PreparingEnlistment preparingEnlistment)
{
preparingEnlistment.Prepared();
}
public void Rollback(Enlistment enlistment)
{
foreach (IEntity entity in transactionElements.Keys)
{
EnlistmentOperations operation = transactionElements[entity];
switch (operation)
{
case EnlistmentOperations.Add: PersistDeleteEntityAsync(entity as T); break;
case EnlistmentOperations.Delete: PersistAddEntityAsync(entity as T); break;
case EnlistmentOperations.Update: PersistUpdateEntityAsync(entity as T); break;
}
}
enlistment.Done();
}
}

Официально наш UnitOfWorkRepositoryBase теперь является менеджером ресурсов и может полностью участвовать в транзакции.

Есть еще кое-что, что-то очень важное. Идея состоит в том, чтобы этот шаблон проектирования по умолчанию поддерживал атомарность. Как нам этого добиться? Мы также создаем это так, чтобы не зависеть от хранилища данных, но мы даже не знаем, какое хранилище данных будет использовать кто-либо, использующий эту архитектуру. Различные хранилища данных предоставляют средства для инициирования транзакционных запросов, а другие - нет. Но он нужен нам для гарантии атомарности.

Разумный способ, который мы можем придумать, - это предположить, что по умолчанию ни одно хранилище данных не предоставляет способ инициировать транзакционный запрос. Следовательно, чтобы имитировать транзакционный запрос, нам нужно будет действительно зафиксировать объект, и когда произойдет откат, мы откатим фиксацию.

Недостатком этого подхода является то, что он нарушает свойство транзакции хранилища данных, называемое Согласованность чтения. Это означает, что другой пользователь может прочитать частичную фиксацию до ее отката.

Стандартные базы данных, такие как SQL-сервер, обрабатывают транзакционные запросы, не сохраняя их непосредственно в реальной таблице, а используя хранилище в памяти. Когда дается кивок, т.е. ошибки не возникает, фиксация в памяти помещается в реальную таблицу базы данных. Такой подход обеспечивает поддержание согласованности чтения. Но, как было сказано ранее, не каждое хранилище данных предоставляет средства для обработки транзакционных запросов, и наша реализация по умолчанию работает для них. Но как быть с такими хранилищами данных, как SQL-SERVER, которые обеспечивают удобный подход к транзакционным запросам?

Хм, это действительно большой вопрос. Нам нужно найти способ сделать архитектуру гибкой для обоих сценариев, не снижая производительности в обоих случаях.

Для этого нам нужно ввести 3 защищенных виртуальных метода

  1. TransactionalAdd
  2. TransactionaalDelete
  3. TransactionalUpdate

вы догадались, почему мы сделали их виртуальными? правильно .. Мы хотим, чтобы производные классы переопределяли их или использовали нашу реализацию по умолчанию. Есть ли предположения, почему они тоже защищены? Опять же правильно. мы не хотим, чтобы член экземпляра имел доступ к этому методу. Почему? вы должны ответить на это сами.

Итак, чтобы обеспечить максимальную производительность при использовании хранилища данных, такого как SQL-SERVER, которое обеспечивает встроенную поддержку транзакций, мы должны переопределить эти методы и обеспечить надлежащую поддержку транзакций SQL-SERVER, также предоставив диспетчер ресурсов, который присоединяется к текущей .Net Framework Объем транзакции. Я реализовал пример для MongoDB, и он тоже прямо в репозитории этого проекта на github.

Наконец, наш UnitOfWorkRepositoryBase становится.

public abstract class UnitOfWorkRepositoryBase<T> :  IEnlistmentNotification where T:class,IEntity
{
.
.
.
.
protected virtual Task<CrudResult<string>> TransactionalAddAsync(IEntity entity)
{
if (Transaction.Current != null)
{
enlistForTransaction();
transactionElements.Add(entity, EnlistmentOperations.Add);
return this.PersistAddEntityAsync(entity as T);
}
else
return this.PersistAddEntityAsync(entity as T);
}
protected virtual Task<CrudResult<bool>> TransactionalUpdateAsync(IEntity entity)
{
if (Transaction.Current != null)
{
enlistForTransaction();
GetEntityAsync(entity.Id).ContinueWith((resultTask) =>
{
T original = resultTask.Result as T;
transactionElements.Add(original, EnlistmentOperations.Update);
});
return this.PersistUpdateEntityAsync(entity as T);
}
else
return this.PersistUpdateEntityAsync(entity as T);
}
protected virtual Task<CrudResult<bool>> TransactionalDeleteAsync(IEntity entity)
{
if (Transaction.Current != null)
{
enlistForTransaction();
GetEntityAsync(entity.Id).ContinueWith((resultTask) =>
{
T original = resultTask.Result as T;
transactionElements.Add(original, EnlistmentOperations.Delete);
});
return this.PersistDeleteEntityAsync(entity as T);
}
else
return this.PersistDeleteEntityAsync(entity as T);
}
}

На этом этапе мы наконец ответили на третий вопрос. Если какая-либо фиксация завершится неудачей, по крайней мере, мы можем быть уверены, что произойдет обратное изменение независимо от того, поддерживает ли наше хранилище данных транзакционную фиксацию или нет.

Потому что мы создали наш шаблон, не зависящий от фреймворка. (в настоящее время мы можем заставить его работать с Entity Framework, не полагаясь исключительно на entityframework, мы можем ввести несколько хранилищ данных, не задумываясь). Мы можем легко заключить, что ответили на Q4 и Q5.

Я надеюсь написать больше с примерами того, как другие фреймворки могут быть легко интегрированы в наш шаблон проектирования в будущем. Но пока весь исходный код проекта можно найти здесь, на Github.