📜 ⬆️ ⬇️

PostSharp. Transaction management

image When I last consulted one company, we discussed an internal SOA framework that should interact with the enterprise databases. This framework was SOA only by name, completely homegrown, and what is most sad, it was a “favorite project” of the head of the IT department. It was not secure and built on dubious technologies and solutions. In general, it was made to solve some problem, which either does not exist or it could not be solved by simple means. My team was extremely disappointed with the structure of this framework. But, as a consultant, it is very often necessary first to think over ways to solve the problem, it is impossible to tell the customer that his product is “bad”, you must first build his trust in yourself, and only then solve more serious problems. One way or another, such a process can take years, and everything depends on the company in which such changes will take place. Then I thought about using aspect-oriented framework to solve this problem.

All source codes of the given programs are available at the link: GitHub

At the same time, we were doomed to fail with this framework, since it is absolutely not intended for multi-threaded applications (for example, for a web server). Because of this, accidents happen all the time, and sometimes it may just not work or run. We had to build workers and well-established applications around this mess and chaos in the first place because we want to build a certain level of trust from the client company. What we have already done for this company can be found in previous articles: this is both logging and error interception. It worked well and hid all the “nastiness” from the end user. However, in order to use this system, it is necessary to protect every part of the code that comes into contact with it. And while doing this, I got tired pretty quickly. After all, imagine: for each method that was called in the framework or read the values ​​of the properties and fields of the classes, it was necessary to carry out the same checks, and not to overdo it: otherwise we lose in performance.
However, stop talking about old projects, let's talk about business: you can use PostSharp to manage transactions, regardless of whether you are building your application on home-grown frameworks or not. Here is a simple OnMethodBoudaryAspect example that will give you an example of the basics of typical transaction support:
[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(); } } 


Everything is really simple and there is absolutely nothing new about my older articles. Note that it would be great to regenerate an exception in case of a rollback of a transaction, and this is what is done by default (in the code that throws an onException). All you have to do is mark the methods in which you need to support transactions with the [TransactionScope] attribute, and you will save yourself from writing the try / catch begin / commit / rollback code all over again with one simple aspect. We also brought the common code to one place, and we can add logging or some special method for catching exceptions in the same way.
One of the cases, the code code above will not work properly, it is nested transactions. In my code above, the Begin / Commit / Rollback methods are just dummies that actually do nothing. In the final application that you will develop, the transaction management is highly dependent on the data access method. If you use ADO.NET connections such as SqlConnection, ODBC, Oracle, etc., then you will most likely use TransactionScope (do not confuse with the name of the aspect) to make the implementation of transactions simple and supported. However, if you use some standalone technology, you will use a completely different API and you will need additional solutions.
Now that we have a basic aspect of transaction support, let's go back to my original example. The service that I use is constantly breaking down. Therefore, I will write a certain cycle that will attempt to perform an operation until the operation is successful. And, of course, I do not want the attempts to be constant and escalate into an infinite loop. Therefore, we introduce restrictions on the number of attempts. Also, let us know for sure that a DataException exception is a sign of an unsuccessful attempt, while all other exceptions tell us that it is useless to make all further attempts to perform an operation. Therefore, we pledge to pay such exceptions and stop attempting:
 [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("-------------------"); } } 


Yes, it looks a little scary! But do not worry, let's sort everything in pieces. CompileTimeInitialize and RuntimeInitialize do not look very different from those described earlier: they just remember the names of the class and method and initialize the services that we need.
For the OnInvoke method, let's consider four possible scenarios:

In my simple application, I made the CharityService service, which crashes with a DataException error 50% of the time, and in the remaining 50% it ends successfully. Also, in about 1 out of 50 calls it returns not a DataException, but some other. If you run the example, you can see the log window, in which a huge number of attempts will be recorded, and calls to Begin, Rollback and Commit. Also, the user will see an error message in case the program has made more attempts than allowed or if an exception is made that is not a DataException.

Using this aspect, you endow your application with some robotization in solving such problems. The aspect code has become bigger, but it is manageable. If you want to get something more complicated, refactoring will take place here: the division into classes and methods, and other operations. You can also use this method in order to build multi-threaded method calls into one stream.
')
References:

Source: https://habr.com/ru/post/126624/


All Articles