Когда в последний раз я консультировал одну компанию, мы обсуждали внутренний SOA фреймворк, который должен взаимодействовать с базами данных предприятия. Этот фреймворк был SOA только разве что по названию, полностью доморощенный, и что самое грустное, он был «любимым проектом» начальника отдела ИТ. Он был не безопасным и построен на сомнительных технологиях и решениях. В общем он был сделан для решения некоторой проблемы, которой либо не существует, либо она не могла была разрешима боле простыми средствами. Моя команда была крайне разочарована структурой этого фреймворка. Но, в качестве консультанта, очень часто приходится сначала продумать пути решения проблемы, нельзя порсто сказать заказчику что его продукт «плохой», необходимо сначала построить его доверие к себе, и только потом решать более серъезные проблемы. Так или иначе такой процесс может занять годы, и все зависит от фирмы, в которой будут происходить такого рода изменения. Тогда я и задумался над тем чтобы начать использовать аспектно-ориентированный фреймворк чтобы решить эту проблему
В тоже самое время мы были обречены на провал с этим фреймворком, поскольку он абсолютно не предназначен для многопоточных приложений (например, для web-сервера). Из-за этого постоянно случаются аварии, а иногда он может просто ни работать ни запускаться. Мы должны были построить рабочие и хорошо отлаженные приложения вокруг этого беспорядка и хаоса в первую очередь потому что мы хотим построить к себе определенный уровень доверия со стороны фирмы-заказчика. То что мы уже сделали для этой фирмы, можно посмотреть в предыдущих статьях: это и логгирование и перехват ошибок. Это работало хорошо, и скрывало все «гадости» от конечного пользователя. Однако для того чтобы пользоваться этой системой, необходимо защищать каждый участок кода, который с ней соприкасается. И, делая это, я довольно быстро устал. Ведь представьте: для каждого метода, который вызывали во фреймворке или считывали значения свойств и полей классов, было необходимо провесети одинаковые проверки, и при этом не переборщить: иначе мы теряем в производительности.
Однако, хватит разглагольствовать о старых проектах, поговорим о деле: вы можете использовать PostSharp для управления транзакциями, независимо от того, строите вы свое приложение на доморощенных фреймворках или нет. Вот простой пример OnMethodBoudaryAspect, который даст вам пример основ типичной поддержки транзакций:
Все действительно просто и здесь нет абсолютно ничего нового относительно моих более старых статей. Заметьте, что здесь было бы здорово перегенерировать исключение в случае отката транзакции, и именно это и делается по умолчанию (в коде, который вызывает onException). Все что вам надо сделать – пометить методы, в которых необходимо поддержать транзакции атрибутом [TransactionScope], и вы убережете себя от написания try/catch begin/commit/rollback кода все снова и снова одним простым аспектом. Также мы вынесли общий код в одно место, и можем таким же образом добавить логгирование или какой-либо специальный метод перехвата исключений.
Один из случаев, кода код выше не будет работать должным образом, это вложенные транзакции. В моем коде, изложенном выше, методы Begin/Commit/Rollback всего лишь пустышки, которые на самом деле ничего не делают. В конечном приложении, которое вы будете разрабатывать, менеджмент транзакий сильно зависит от метода доступа к данным. Если вы используете ADO.NET соединения, такие как SqlConnection, ODBC, Oracle, и проч., тогда вы скорее всего будете использовать TransactionScope (не перепутайте с названием аспекта) чтобы сделать внедрение транзакций простым и поддерживаемым. Однако если вы используете некоторую самостоятельную технологию, вы будете использовать совершенно иной API и вам потребуются дополнительные решения.
Теперь, когда у нас есть базовый аспект поддержки транзакций, давайте вернемся к моему изначальному примеру. Сервис, который я использую, постоянно ломается. Потому я напишу некий цикл, который будет пытаться сделать операцию до тех пор, пока операция не пройдет успешно. И, конечно, я не хочу чтобы попытки были постоянными и переросли в бесконечный цикл. Поэтому, введем ограничения на количество попыток. Также пусть мы точно знаем что признаком неудачной попытки служит исключение DataException, а все остальные исключения говорят нам о том что все дальнейшие попытки провести операцию предпринимать бесполезно. Поэтому мы залоггируем такие исключения и перестаним предпринимать попытки:
Да, это выглдит несколько пугающе! Но не беспокойтесь, давайте разберем все по кусочкам. CompileTimeInitialize и RuntimeInitialize вовсе не выглядят сильно отличными от тех, что описаны раньше: они всего лишь запоминают имена класса и метода и инициализируя сервисы, которые нам необходимы.
Для метода OnInvoke, давайте рассмотрим четыре возможных сценария работы:
В моем простом приложении, я сделал сервис CharityService, который вылетает с ошибкой DataException в 50% случаев, а в оставшихся 50% — заканчивается удачно. Также примерно в 1 из 50 вызовов он возвращает не DataException, а какой-то другой. Если запустить пример, то сможете увидеть окно лога, в котором будет зафиксировано огромное количество попыток, и вызовов Begin, Rollback и Commit. Также пользователь увидит сообщение об ошибке в случае если программа сделала попыток больше чем разрешено либо если произошло исключение не явлюяющееся DataException.
Используя этот аспект, вы наделяете свое приложение некоторой роботизированностью в решении такого рода проблем. Код аспекта стал больше но он управляем. Если вы захотите получить что-то сложнее, тут будет иметь место рефакторинг: разделение на классы и методы, и прочие операции. Также можно использовать этот способ для того чтобы выстроить многопоточные обращения к методу в один поток.
Ссылки:
Все исходные тексты приведенных программ доступны по ссылке: GitHub
В тоже самое время мы были обречены на провал с этим фреймворком, поскольку он абсолютно не предназначен для многопоточных приложений (например, для web-сервера). Из-за этого постоянно случаются аварии, а иногда он может просто ни работать ни запускаться. Мы должны были построить рабочие и хорошо отлаженные приложения вокруг этого беспорядка и хаоса в первую очередь потому что мы хотим построить к себе определенный уровень доверия со стороны фирмы-заказчика. То что мы уже сделали для этой фирмы, можно посмотреть в предыдущих статьях: это и логгирование и перехват ошибок. Это работало хорошо, и скрывало все «гадости» от конечного пользователя. Однако для того чтобы пользоваться этой системой, необходимо защищать каждый участок кода, который с ней соприкасается. И, делая это, я довольно быстро устал. Ведь представьте: для каждого метода, который вызывали во фреймворке или считывали значения свойств и полей классов, было необходимо провесети одинаковые проверки, и при этом не переборщить: иначе мы теряем в производительности.
Однако, хватит разглагольствовать о старых проектах, поговорим о деле: вы можете использовать PostSharp для управления транзакциями, независимо от того, строите вы свое приложение на доморощенных фреймворках или нет. Вот простой пример OnMethodBoudaryAspect, который даст вам пример основ типичной поддержки транзакций:
[Serializable]
public class TransactionScopeAttribute : OnMethodBoundaryAspect
{
[NonSerialized] private ICharityService _charityService;
private string _methodName;
private string _className;
public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
{
_className = method.DeclaringType.Name;
_methodName = method.Name;
}
public override void RuntimeInitialize(System.Reflection.MethodBase method)
{
// in practice, the begin/rollback/commit might be in a more general service
// but for convenience in this demo, they reside in CharityService alongside
// the normal repository methods
_charityService = new CharityService();
}
public override void OnEntry(MethodExecutionArgs args)
{
_charityService.BeginTransaction();
}
public override void OnException(MethodExecutionArgs args)
{
_charityService.RollbackTransaction();
var logMsg = string.Format("exception in {0}.{1}", _className, _methodName);
// do some logging
}
public override void OnSuccess(MethodExecutionArgs args)
{
_charityService.CommitTransaction();
}
}
Все действительно просто и здесь нет абсолютно ничего нового относительно моих более старых статей. Заметьте, что здесь было бы здорово перегенерировать исключение в случае отката транзакции, и именно это и делается по умолчанию (в коде, который вызывает onException). Все что вам надо сделать – пометить методы, в которых необходимо поддержать транзакции атрибутом [TransactionScope], и вы убережете себя от написания try/catch begin/commit/rollback кода все снова и снова одним простым аспектом. Также мы вынесли общий код в одно место, и можем таким же образом добавить логгирование или какой-либо специальный метод перехвата исключений.
Один из случаев, кода код выше не будет работать должным образом, это вложенные транзакции. В моем коде, изложенном выше, методы Begin/Commit/Rollback всего лишь пустышки, которые на самом деле ничего не делают. В конечном приложении, которое вы будете разрабатывать, менеджмент транзакий сильно зависит от метода доступа к данным. Если вы используете ADO.NET соединения, такие как SqlConnection, ODBC, Oracle, и проч., тогда вы скорее всего будете использовать TransactionScope (не перепутайте с названием аспекта) чтобы сделать внедрение транзакций простым и поддерживаемым. Однако если вы используете некоторую самостоятельную технологию, вы будете использовать совершенно иной API и вам потребуются дополнительные решения.
Теперь, когда у нас есть базовый аспект поддержки транзакций, давайте вернемся к моему изначальному примеру. Сервис, который я использую, постоянно ломается. Потому я напишу некий цикл, который будет пытаться сделать операцию до тех пор, пока операция не пройдет успешно. И, конечно, я не хочу чтобы попытки были постоянными и переросли в бесконечный цикл. Поэтому, введем ограничения на количество попыток. Также пусть мы точно знаем что признаком неудачной попытки служит исключение DataException, а все остальные исключения говорят нам о том что все дальнейшие попытки провести операцию предпринимать бесполезно. Поэтому мы залоггируем такие исключения и перестаним предпринимать попытки:
[Serializable]
public class TransactionScopeAttribute : MethodInterceptionAspect
{
[NonSerialized] private ICharityService _charityService;
[NonSerialized] private ILogService _logService;
private int _maxRetries;
private string _methodName;
private string _className;
public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
{
_methodName = method.Name;
_className = method.DeclaringType.Name;
}
public override void RuntimeInitialize(System.Reflection.MethodBase method)
{
_charityService = new CharityService();
_logService = new LogService();
_maxRetries = 4; // you could load this from XML instead
}
public override void OnInvoke(MethodInterceptionArgs args)
{
var retries = 1;
while (retries <= _maxRetries)
{
try
{
_charityService.BeginTransaction();
args.Proceed();
_charityService.CommitTransaction();
break;
}
catch (DataException)
{
_charityService.RollbackTransaction();
if (retries <= _maxRetries)
{
_logService.AddLogMessage(string.Format(
"[{3}] Retry #{2} in {0}.{1}",
_methodName, _className,
retries, DateTime.Now));
retries++;
}
else
{
_logService.AddLogMessage(string.Format(
"[{2}] Max retries exceeded in {0}.{1}",
_methodName, _className, DateTime.Now));
_logService.AddLogMessage("-------------------");
throw;
}
}
catch (Exception ex)
{
_charityService.RollbackTransaction();
_logService.AddLogMessage(string.Format(
"[{3}] {2} in {0}.{1}",
_methodName, _className,
ex.GetType().Name, DateTime.Now));
_logService.AddLogMessage("-------------------");
throw;
}
}
_logService.AddLogMessage("-------------------");
}
}
Да, это выглдит несколько пугающе! Но не беспокойтесь, давайте разберем все по кусочкам. CompileTimeInitialize и RuntimeInitialize вовсе не выглядят сильно отличными от тех, что описаны раньше: они всего лишь запоминают имена класса и метода и инициализируя сервисы, которые нам необходимы.
Для метода OnInvoke, давайте рассмотрим четыре возможных сценария работы:
- Если метод отрабатывает без исключения, то это значит что все отлично работает.
- Работа метода заканчивается неудачей и он выбрасывает исключение DataException, и мы еще не достигли ограничения в количестве повторов. Этот код находится в первой части оператора if/else первого блока catch{}
- Работа метода заканчивается неудачей и он выбрасывает исключение DataException, и мы достигли ограничения в количестве повторов. Этот код находится во второй части оператора if/else первого блока catch{}
- Метод выбрасывает другое исключение, сигнализируя о критической ошибке и невозможности повторов. За эту ситуацию отвечает код во втором блоке Catch{}
В моем простом приложении, я сделал сервис CharityService, который вылетает с ошибкой DataException в 50% случаев, а в оставшихся 50% — заканчивается удачно. Также примерно в 1 из 50 вызовов он возвращает не DataException, а какой-то другой. Если запустить пример, то сможете увидеть окно лога, в котором будет зафиксировано огромное количество попыток, и вызовов Begin, Rollback и Commit. Также пользователь увидит сообщение об ошибке в случае если программа сделала попыток больше чем разрешено либо если произошло исключение не явлюяющееся DataException.
Используя этот аспект, вы наделяете свое приложение некоторой роботизированностью в решении такого рода проблем. Код аспекта стал больше но он управляем. Если вы захотите получить что-то сложнее, тут будет иметь место рефакторинг: разделение на классы и методы, и прочие операции. Также можно использовать этот способ для того чтобы выстроить многопоточные обращения к методу в один поток.
Ссылки: