📜 ⬆️ ⬇️

Aspect-oriented programming: study and do it yourself!

The article was born from the fact that I needed a convenient and simple interception mechanism for some tasks, which is easily implemented by AOP technicians. There are quite a few interceptors (Casle.net, Spring.net, LinFu, etc.) that require you to embed dynamic child classes in the IL code at run time and almost always lead to the same restrictions imposed on the classes being intercepted: not static, not sealed, methods and properties must be virtual, etc ...

Other interception mechanisms required changes to the build process or license purchase. I could not afford either of them ...

Introduction


AOP - Aspect-Oriented Programming. I think everyone is familiar with what OP means, so you need to figure out what an Aspect is . Do not worry about this in the article later.

I will try to write this article at novice level. The only requirement for further reading is knowledge of the concept of Object Oriented Programming.
')
It seems to me that understanding the concept will allow you to better understand the implementation. But I also understand that the article is quite large, because if you get bored or tired, then still see the part with the implementation. You can always go back to theory.

Experienced developers, read!


If you're already familiar with AOP, don't leave!
Let me tell you right here and now what is in this article ...

I will talk about the interception technique, which will allow to intercept:
• Any class (including sealed, static, meaningful type)
• Constructors
• Type Initializers
• Methods, properties, and event instances (even if they are not marked as virtual)
• Static methods, properties and events

Without:
• Redrawing your code and assemblies
• Embedding in IL-code
• Dynamic creation of something
• Target class changes
• The need for a weaver to implement something (for example, MarshalByRef)

In general, the talk will be about pure managed code that can be run on .Net 1.1 (in general, I use a little Linq, but you can easily get rid of it) and intercept everything that comes into your head.

Clarify finally. With the help of this technique:
• You can intercept constructs such as System.DateTime.Now and System.IO.File!
• You will not have the usual restrictions specific to all popular interception libraries.
Still in doubt? Then read on!

Principles of AOP


Introductory


Some may think that their path of knowledge of Object Oriented Programming is not yet complete, and does it make sense to switch from OOP to AOP, abandoning all the practices studied during the years of hard learning? The answer is simple: do not switch. No opposition to the PLO and AOP! AOP is a concept whose name, in my opinion, is misleading. Understanding the principles of AOP requires you to work with classes, objects, inheritance, polymorphism, abstractions, etc., because you can’t manage to lose everything that you use in OOP.

In ancient times, when the Internet consisted of yellow pages, bulletin boards, and juznet, it was best to read books to study something (a note for a new generation: a book is a thing consisting of stitched together paper sheets containing text). And almost all of these books constantly reminded you of the following OOP rules:
• Rule # 1: Encapsulation of data and code is necessary.
• Rule number 2: never break Rule number 1

Encapsulation was the main purpose for which OOP appeared in 3rd generation languages ​​(3GL).

From wiki:
Encapsulation is used to support two similar, but different, requirements and, sometimes, to their combination:
• Restriction of access to some components of the object.
• A language construct that makes it easy to combine data with methods (or other functions) that work with this data.


AOP, in general, argues that sometimes a language construct is required that facilitates combining methods (or other functions) that work with encapsulated data without this data.

Got it? Actually, this is all you need from theory. Fine!

Now, let's move on to the two questions that have appeared:
• When is it “sometimes”?
• Why is this needed?

About the benefits of AOP ... some scenarios


Scenario A

You are a developer in a bank. The bank works fine system. Business is stable. The government issues a decree requiring more transparency from banks. With any movement of funds to or from the bank, this action must be recorded. Also in the government they say that this is only the first step on the path to transparency, there will be more.

Scenario B

Your web application is issued to the tester team. All functional tests passed, and the application broke down on the load test. There is a non-functional requirement saying that no page can be processed on the server for more than 500 ms. After analyzing, you found dozens of queries to the database, which can be avoided by caching the results.

Scenario B

You have been building a domain model for two years into an ideal library containing 200+ classes. Later, you will learn that a new front-end is being written for the application and you need to associate all your objects with the UI. But to solve the problem, it is necessary that all classes implement INotifyPropertyChanged.

These examples demonstrate where AOP can be a salvation. All of these scenarios are similar in the following:

Cross-cutting concern

When is it "sometimes"?

Some classes (banking system, data access services, domain-model) need to get functionality, which, in general, is not “their business”.

• The business of the banking system is to transfer money. Logging operations wants the government.
• A data access service is needed to get data. Data caching is a non-functional requirement.
• Classes of the domain model implement the business logic of your company. Notifying the UI of a property change is required only by the UI

In general, we are talking about situations when you need to write code for various classes to solve problems not related to these classes. Speaking in the AOP dialect, action is needed.

Understanding the actions being implemented is key to AOP. There are no actions to implement - there is no need for AOP.

Why do you need it?
Let's take a closer look at Scenario B.

The problem is that you have, say, an average of 5 properties in each class. Having 200+ classes, you have to implement (copy) more than 1000 sample pieces of code to turn something like this:
public class Customer { public string Name { get; set; } } 

Something like this:
 public class Customer : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string _Name; public string Name { get { return _Name; } set { if (value != _Name) { _Name = value; SignalPropertyChanged("Name"); } } } void SignalPropertyChanged(string propertyName) { var pcEvent = this.PropertyChanged; if (pcEvent != null) pcEvent(this, new PropertyChangedEventArgs(propertyName)); } } 


Wow! And this is only one property! By the way, do you know? Intern retired a couple of days ago

It is necessary “to combine methods (or other functions) working with encapsulated data, without this data”. In other words: implement the implementation of INotifyPropertyChanged actions without changing or with minimal changes to the classes of the domain model of the type Customer.

If we can do this, then we will get:
• Clear separation of actions
• Avoid code repetition and speed up development
• We will not hide domain classes under tons of sample code.

Great. But how to do all this?

Theoretical answer

We have an injected action that must be performed in several classes (hereinafter referred to as the goals). An implementation (code that implements logging, caching, or something else) is called an AOP action.

Next, we need to attach (embed, embed, select your word) action (I will repeat, because it is important: the action is the implementation of the action being implemented) at any place we want the goal. And we should be able to choose any of the following target locations for the implementation of the action:
• Static initializers
• Constructors
• Reading and writing static properties
• Reading and writing instance properties
• Static methods
• instance methods
• Destructors

In ideal AOP, we should be able to inject an action into any line of the goal code.
Great, but if we need to attach an action, do we need an interceptor in the target? Yes, CEP!

In AOP, the description of the interceptor (the place where the action will be performed) has the name: pointcut. And the place where the code actually binds: the junction point.

Clear? Maybe not ... Here is a bit of pseudocode, which I hope will explain:
 //  class BankAccount { public string AccountNumber {get;} public int Balance {get; set;} void Withdraw(int AmountToWithdraw) { :public pointcut1; //  ( ,   -    ) Balance -= AmountToWithdraw; } } //   concern LoggingConcern { void LogWithdraw(int AmountToWithdraw) { //   ,     //  'this' –    BankAccount. Console.WriteLine(this.AccountNumber + " withdrawal on-going..."); } } class Program { void Main() { //        pointcut = typeof(Bank).GetPointcut("pointcut1"); //     LoggingConcern.Join(cutpoint, LogWithdraw); //        , //    LoggingConcern   pointcut1  } } 

Would it be cool to have such a mechanism in C # right out of the box ???

Some more concepts

Before we go further to our implementation, we will give a few more concepts ...

What is an Aspect?
This is a combination of action, cut and point of connection.

Ponder this for a minute, and, I hope, everything will fall into place: there is a logging mechanism (action) that registers its logging method for execution (connection point) in the specified location (section) of my application. All this together is an aspect of my application.

But just a minute ... What should and can the action do after implementation?

Actions are divided into two categories:

Side effects.
A side effect is an action that does not change the action of the code in the slice. A side effect is simply adding a certain command to execute.
The logging action is a good side effect. When the environment executes the target method (for example, Bank.Withdraw (int Amount)), the LoggingConcern.LogWithdraw (int Amount) method will be executed and the Bank.Withdraw (int Amount) method will continue its execution.

Tips.
Tips - actions that can change the method input / output.
Action caching is a great example. When the target method is executed (for example, CustomerService.GetById (int Id)), the method CachingConcern.TryGetCustomerById (int Id) is executed and returns the value found in the cache, or continues execution if it is not present.

Advice can be:
• Check the parameters in the cut of the goal and the ability to change them if necessary
• Cancel target methods and replace them with a different implementation.
• Check the return value of the target method and change or replace them.

Are you still reading? Bravo! Parce que vous le valez bien ...

This concludes with the concepts and concepts of AOP. Let's take a closer look at C #.

Our implementation


Show me the code u je tue le chien!

Actions (concern)

The action must implement the magic of this, which refers to the type of our goal.
No problemo!
 public interface IConcern<T> { T This { get; } //  ,   ? } 


Pointcuts

There is no easy way to get cuts for each line of code. But one by one, we can still get it and it's pretty simple using the System.Reflection.MethodBase class. MSDN is not verbose about it: Provides information about methods and constructors.

Between us, using MethodBase to get links to slices is the most powerful possible tool in our task.
You can get access to slices of constructors, methods, properties and events, because practically everything that you declare in .Net ultimately comes down to the method ...

See for yourself:
 public class Customer { public event EventHandler<EventArgs> NameChanged; public string Name { get; private set; } public void ChangeName(string newName) { Name = newName; NameChanged(this, EventArgs.Empty); } } class Program { static void Main(string[] args) { var t = typeof(Customer); //  (  ) var pointcut1 = t.GetConstructor(new Type[] { }); //  ChangeName var pointcut2 = t.GetMethod("ChangeName"); //  Name var nameProperty = t.GetProperty("Name"); var pointcut3 = nameProperty.GetGetMethod(); var pointcut4 = nameProperty.GetSetMethod(); //     NameChanged var NameChangedEvent = t.GetEvent("NameChanged"); var pointcut5 = NameChangedEvent.GetRaiseMethod(); var pointcut6 = NameChangedEvent.GetAddMethod(); var pointcut7 = NameChangedEvent.GetRemoveMethod(); } } 


Join points

Writing the connection code is really simple. Look at this code:
 void Join(System.Reflection.MethodBase pointcutMethod, System.Reflection.MethodBase concernMethod); 

We can add it to something like the registry, which we will do later, and we can start writing code like this !!!
 public class Customer { public string Name { get; set;} public void DoYourOwnBusiness() { System.Diagnostics.Trace.WriteLine(Name + "   "); } } public class LoggingConcern : IConcern<Customer> { public Customer This { get; set; } public void DoSomething() { System.Diagnostics.Trace.WriteLine(This.Name + "    "); This.DoYourOwnBusiness(); System.Diagnostics.Trace.WriteLine(This.Name + "    "); } } class Program { static void Main(string[] args)h { //    Customer.DoSomething(); var pointcut1 = typeof(Customer).GetMethod("DoSomething"); var concernMethod = typeof(LoggingConcern).GetMethod("DoSomething"); //   AOP.Registry.Join(pointcut1, concernMethod); } } 

How far have we got from our pseudocode? In my opinion, not very ...
So what next?

Putting it all together ...

This is where problems and fun begin at the same time!
But let's start with a simple

Registry

The registry will keep records of our connection points. We take the list-singleton for connection points. Connection point - simple structure:
 public struct Joinpoint { internal MethodBase PointcutMethod; internal MethodBase ConcernMethod; private Joinpoint(MethodBase pointcutMethod, MethodBase concernMethod) { PointcutMethod = pointcutMethod; ConcernMethod = concernMethod; } //       public static Joinpoint Create(MethodBase pointcutMethod, MethodBase concernMethod) { return new Joinpoint (pointcutMethod, concernMethod); } } 

Nothing special ... He also needs to implement IEquatable, but to make the code shorter, I removed it.

And the registry. The class is called AOP and is singleton. It provides access to its unique instance through a static property called Registry:
 public class AOP : List<Joinpoint> { static readonly AOP _registry; static AOP() { _registry = new AOP(); } private AOP() { } public static AOP Registry { get { return _registry; } } [MethodImpl(MethodImplOptions.Synchronized)] public void Join(MethodBase pointcutMethod, MethodBase concernMethod) { var joinPoint = Joinpoint.Create(pointcutMethod, concernMethod); if (!this.Contains(joinPoint)) this.Add(joinPoint); } } 


Using the AOP class, you can write the following structure:
 AOP.Registry.Join(pointcut, concernMethod); 


Houston, we have problems

Here we are faced with an obvious and big problem with which to do something. If the developer writes like this
 var customer = new Customer {Name="test"}; customer.DoYourOwnBusiness(); 

there is no reason why you need to access our registry, and our LoggingConcern.DoSomething () method will not start.

The trouble is that .Net does not provide us with an easy way to intercept such calls.
Once there is no built-in mechanism, you need to make your own. The capabilities of your mechanism will determine the capabilities of your AOP implementation.
The purpose of this article is not to discuss all possible interception techniques. Just note that the interception method is the key difference between AOP implementations.

The SharpCrafters site (PostSharp owners) provide some clear information on two main techniques:
• Embedding at compile time
• Embedding at runtime

Our implementation - Proxy

In general, it is no secret that there are three options for intercepting:
• Create your own language and compiler to get .net assemblies: when compiling, you can embed anything anywhere.
• Implement a solution that changes the behavior of assemblies during execution.
• Put between the client and the proxy target that intercepts calls.

Note for advanced guys: I deliberately do not consider the features of the debugger and profiler API, as their use is not viable in production.

A note for the most advanced: the hybrid of the first and second variants, used in the Raslyn API, can be implemented, but, as I know, is still being done. A bon entendeur ...

Moreover, if you do not need to be able to cut in any line of code, the first two options are too complicated.

Let's move on to the third option. There are two news about the proxy: good and bad.
Bad - during the execution you need to substitute the target for a copy of the gasket. If you want to intercept constructors, then you have to delegate the creation of instances of target classes to the factory, this implementation cannot embed such actions. If there is an instance of the target class, you must explicitly request a replacement. For control inversion and dependency injection masters, this is not even a task. For the rest, this means that you have to use the factory to provide all the possibilities in our interception technique. But do not worry, we will build this factory.

The good news is that you don't need to do anything to implement a proxy. The System.Runtime.Remoting.Proxies.RealProxy class builds it in an optimal way.
In my opinion, the class name does not reflect its purpose. This class is not a proxy, but an interceptor. Nevertheless, he will make us a proxy by calling his method GetTransparentProxy (), and this is what we actually need from him.

Here is the fish interceptor:
 public class Interceptor : RealProxy, IRemotingTypeInfo { object theTarget { get; set; } public Interceptor(object target) : base(typeof(MarshalByRefObject)) { theTarget = target; } public override System.Runtime.Remoting.Messaging.IMessage Invoke(System.Runtime.Remoting.Messaging.IMessage msg) { IMethodCallMessage methodMessage = (IMethodCallMessage) msg; MethodBase method = methodMessage.MethodBase; object[] arguments = methodMessage.Args; object returnValue = null; // TODO: //     ,  AOP.Registry //     MethodBase,   "method"... //      ,     //   "theTarget"   ... ;-) return new ReturnMessage(returnValue, methodMessage.Args, methodMessage.ArgCount, methodMessage.LogicalCallContext, methodMessage); } #region IRemotingTypeInfo public string TypeName { get; set; } public bool CanCastTo(Type fromType, object o) { return true; } #endregion } 

Some explanations, because we got to the very heart of realization ...

The RealProxy class is designed to intercept calls from remote objects and organize target objects. Remote should be understood as truly remote: objects from another application, another application domain, another server, etc.). Without going into details, there are two ways to organize remote objects in the .Net infrastructure: by reference and by value. Therefore, you can order deleted objects only if they inherit MarshalByRef or implement ISerializable. We are not going to use the capabilities of remote objects, but nevertheless we need the RealProxy class to think that the target supports remote control. Because of this, we pass typeof (MarshalByRef) to the constructor RealProxy.

RealProxy receives all calls through a transparent proxy using the Invoke method (System.Runtime.Remoting.Messaging.IMessage msg). It is here that we implement the essence of the substitution of methods. See comments in the code above.

Regarding the implementation of IRemotingTypeInfo: in a real remote environment, the client will request an object from the server. The client application may not know anything about the type of the object being received. Accordingly, when a client application calls a public object GetTransparentProxy (), the environment can the returned object (transparent proxy) match the application's contract. By implementing IRemotingTypeInfo, we give a hint to the client environment which cast is permissible and which is not.

Now marvel at the trick we use here.
 public bool CanCastTo(Type fromType, object o) { return true; } 

All our implementation of AOP is possible solely due to the possibility to write these two words for a remote object: return true. Which means that we can bring the object returned by GetTransparentProxy () to any interface without any check by the environment !!!!

The environment just gives us a go-ahead for any actions!
You can fix this code and return something more reasonable than true for any type ... But you can also imagine how to benefit from the behavior provided by the Non-Existent Method or intercept the entire interface ... In general, there is quite a lot of space for fantasy ...

Now we already have a decent interception mechanism for our target instance. But we still have no interception of constructors and transparent proxies. This is a task for the factory ...

Factory

To say nothing special. Here is a fish class.
 public static class Factory { public static object Create<T>(params object[] constructorArgs) { T target; // TODO: //   typeof(T)   constructorArgs (-   ) //    ,         //   ,    -     //     “target” ()   //   GetProxy return GetProxyFor<T>(target); } public static object GetProxyFor<T>(object target = null) { //         // (    ,  ) //        return new Interceptor(target).GetTransparentProxy(); } } 

Note that the Factory class always returns an object of type object. We cannot return an object of type T simply because a transparent proxy is not of type T, it is of type System.Runtime.Remoting.Proxies .__ TransparentProxy. But remember the resolution of the given environment, we can bring the returned object to any interface without checking!

We will plant the Factory class into our AOP class with the hope that we will pass on a neat code to our customers. You can see this in the Usage section.

Last notes on implementation

If you've read this far, then you are a hero! Bravissimo! Kudos!

To keep the article concise and understandable (and what's funny?), I will not go into boring arguments about the details of the implementation of the get and switch methods. This is nothing interesting. But if you are still interested: download the code and see - it is fully working! The names of classes and methods may differ slightly, because I ruled it in parallel, but there should be no special changes.

Attention! Before using the code in your project, carefully read the paenultimus . And if you do not know what paenultimus means, click on the link.


Using

I have already written a lot of things, but still have not shown what to do with all this. And here it is the moment of truth!

What is in the archive ( download )

The archive includes 5 examples demonstrating the implementation of the following aspects:
• Interception Designer
• Interception of methods and properties
• Interception of events
• Interception of initialization of types
• Interception File.ReadAllText (string path)

And here I will demonstrate two of the five: the most and the least obvious.

Interception of methods and properties

First, we need a domain model. Nothing special.
 public interface IActor { string Name { get; set; } void Act(); } public class Actor : IActor { public string Name { get; set; } public void Act() { Console.WriteLine("My name is '{0}'. I am such a good actor!", Name); } } 


Now we need an action
 public class TheConcern : IConcern<Actor> { public Actor This { get; set; } public string Name { set { This.Name = value + ". Hi, " + value + " you've been hacked"; } } public void Act() { This.Act(); Console.WriteLine("You think so...!"); } } 


When we initialize the application, we inform the registry about our connection points.
 // Weave the Name property setter AOP.Registry.Join ( typeof(Actor).GetProperty("Name").GetSetMethod(), typeof(TheConcern).GetProperty("Name").GetSetMethod() ); // Weave the Act method AOP.Registry.Join ( typeof(Actor).GetMethod("Act"), typeof(TheConcern).GetMethod("Act") ); 


And finally, we create an object in the factory.
 var actor1 = (IActor) AOP.Factory.Create<Actor>(); actor1.Name = "the Dude"; actor1.Act(); 

Please note that we have requested the creation of the class Actor, but we can bring the result to the interface, therefore we will lead to the IActor, since the class implements it.

If you run all this in a console application, we get:
My name is 'the Dude. Hi, the Dude you've been hacked'. I am such a good actor!
You think so...!


Interception File.ReadAllText (string path)

Here are two minor problems:
• File class static
• and does not implement any interfaces

Remember the "good"? The medium does not check the type returned by the proxy and its correspondence to the interface.
So we can create any interface. No one realizes it neither the goal nor the action. In general, we use the interface as a contract.
Let's create an interface that pretends to be a static File class.
 public interface IFile { string[] ReadAllLines(string path); } 


Our action
 public class TheConcern { public static string[] ReadAllLines(string path) { return File.ReadAllLines(path).Select(x => x + " hacked...").ToArray(); } } 


Registration of connection points
 AOP.Registry.Join ( typeof(File).GetMethods().Where(x => x.Name == "ReadAllLines" && x.GetParameters().Count() == 1).First(), typeof(TheConcern).GetMethod("ReadAllLines") ); 


And finally, the execution of the program
 var path = Path.Combine(Environment.CurrentDirectory, "Examples", "data.txt"); var file = (IFile) AOP.Factory.Create(typeof(File)); foreach (string s in file.ReadAllLines(path)) Console.WriteLine(s); 


In this example, note that we cannot use the Factory.Create method, since static types cannot be used as arguments.

Links


Without any special order.
• Tutorials from Qi4j
• Extending RealProxy
• Dynamic Proxy Tutorial (Castle.net)
• LinFu.DynamicProxy: A Lightweight Proxy Generator
• Add aspects using Dynamic Decorator
• Implementing a CLR Profiler to act as an AOP interceptor
• Intercepting the .Net
• AspectF Fluent Way To Add Aspects for Cleaner Maintainable Code

What's next?


We were able to achieve the main goal of AOP: to realize the aspect and register it for execution. TinyAOP was born. But your path in the lands of AOP is not finished yet, and you may want to go deeper.

Reason 1: Who wants to register connection points the way we do now? Not to me for sure! It is a little analysis and you can make it more practical and similar to a real AOP library. AOP is needed to simplify life, not create a headache.

Reason 2: The themes of impurities and aggregation are not disclosed at all, and from them you can expect a lot of good things.

Reason 3: We need performance and stability.Now the code is just a proof of the concept's performance. It is rather slow and can be made very fast. Error checking also does not hurt.

Reason 4: We intercept almost all classes, but what about intercepting interfaces?

Reason 5: You still have few reasons?

Conclusion


We have a good and compact prototype that demonstrates the technical feasibility of implementing AOP in a clean, managed code without stitching, etc.

Now you know about AOP, you can write your own implementation.

Here the translator is introduced and begins to carry gag


Although I fully agree with the author about this section.
Sites like habahabr.ru, codeproject.com and the like exist only because different people publish articles on them. It doesn't matter why they do it, but this work is being done. Please do not neglect the authors. If you do not like the article, explain in the comments what is wrong. If you just minus, no one will know how to do better. If you like, also do not be silent!

And now for sure the gag of the translator


In general, to my surprise, it was impossible to translate many words from AOP, so I propose in the comments to propose good and understandable Russian words for the following terms:
• Aspect (it’s understandable here, but still)
• Concern
• Joinpoint
• Cross-cutting
• Weaving ( viver - nothing, but it would be better to find the Russian word)

Author: Guirec Le Bars

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


All Articles