
Aspect-oriented programming: learn and do it yourself!
- Transfer
The article was born out of the fact that I needed a convenient and simple interception mechanism for some tasks, which is easily implemented by AOP techniques. There are quite a few interceptors (Casle.net, Spring.net, LinFu, etc.) that require the implementation of dynamic child classes in IL-code at runtime and almost always lead to the same restrictions on intercepted classes: not static, non-sealed, methods and properties must be virtual, etc ...
Other interception mechanisms required a change in the assembly or license purchase process. I could not allow myself either ...
AOP - Aspect Oriented Programming. I think everyone is familiar with what OP means, so you need to figure out what the Aspect is . Do not worry about this in an article later.
I will try to write this article at the beginner 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 become bored or are tired, still look at the implementation part. You can always return to theory.
If you are already familiar with AOP, do not leave!
Let’s right here and now
I’ll reveal what’s in this article ... I’ll tell you about the interception technique that will allow you to intercept:
• Any class (including sealed, static, significant types)
• Constructors
• Type initializers
• Methods, properties and instance events (even if they are not marked as virtual)
• Static methods, properties, and events
Without:
• Redrawing your code and assemblies
• Embedding in IL code
• Dynamically creating something
• Changing target classes
• Weaver needs to implement something (for example Marshalby Ref)
In general, we will talk about clean managed code that can be run on .Net 1.1 (in general, I use a bit of Linq, but you can easily get rid of it) and intercept anything that comes to your mind.
We will clarify finally. Using this technique:
• You can intercept constructs such as System.DateTime.Now and System.IO.File!
• You will not have the usual limitations common to all popular interception libraries.
Still in doubt? Then read on!
Some may think that their way of learning Object-Oriented Programming is not yet complete, and does it make sense to switch from OOP to AOP, abandoning all the practices studied over the years of hard training? The answer is simple: no need to switch. There is no confrontation between OOP 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 will not be able to lose everything that you use in OOP.
In ancient times, when the Internet consisted of yellow pages, bulletin boards and user’s pages, it was best to read books to learn something (remark for the new generation: a book is a piece consisting of paper sheets sewn together containing text). And almost all of these books constantly reminded you of the following OOP rules:
• Rule number 1: data and code encapsulation is necessary
• Rule number 2: never break Rule number 1
Encapsulation was the main goal for which OOP appeared in 3rd generation languages ( 3GL).
From the wiki:
AOP generally asserts that sometimes a language construct is required to facilitate the integration of methods (or other functions) that work with encapsulated data without this data.
Did you catch it? Actually, that’s all you needed from theory. Excellent!
Now, let's move on to two questions that have arisen:
• When is it “sometimes”?
• Why is this needed?
You are a developer at the bank. The bank has a great system. The business is stable. The government issues a decree requiring banks to be more transparent. With any movement of funds to or from the bank, this action should be recorded. Also in the government they say that this is only the first step towards transparency, there will be more.
Your web application has been issued to the tester team. All functional tests passed, and the application broke 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 database queries that can be avoided by caching the results.
You have been building a domain model for two years in an ideal library containing 200+ classes. Later you will find out that a new front-end is being written for the application and it is necessary to associate all your objects with the UI. But to solve the problem, it is necessary that all classes implement INotifyPropertyChanged.
The above examples demonstrate where AOP can be a salvation. All of these scenarios are similar in the following:
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 receive data. Data caching is a non-functional requirement.
• Domain model classes implement the business logic of your company. Notifying the UI of a property change requires only the UI.
In general, we are talking about situations where you need to write code for various classes to solve problems that are not inherent in these classes. Speaking in the dialect of AOP - implemented actions are needed.
Understanding the actions being implemented is key to AOP. There are no implemented actions - there is no need for AOP.
Why is this needed?
Let's take a closer look at Scenario B.
The problem is that you have, for example, an average of 5 properties in each class. With 200+ classes, you will have to implement (copy-paste) more than 1000 template pieces of code to turn something like this:
In something like this:
Wow And this is only one property! By the way, are you up to date? The trainee quit a couple of days ago.
We need a "combination of methods (or other functions) that work 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 a domain model of type Customer.
If we can realize this, then the output will be:
• A clear separation of actions
• We will avoid repeating the code and speed up development
• We will not hide domain classes under tons of boilerplate code
Great. But how to do all this?
We have an implemented action that must be performed in several classes (hereinafter referred to as the goal). 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 repeat, because it is important: an action is the implementation of an implemented action) to any place we need. And we should be able to choose any of the following places for the purpose of implementing the action:
• Static initializers
• Constructors
• Reading and writing static properties
• Reading and writing properties of instances
• Static methods
• Instance methods
• Destructors
In an ideal AOP, we should be able to embed an action on any line of target 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) is called: pointcut. And the place where the code actually binds: the connection point.
Clear? Maybe not ... Here is a bit of pseudo code, which I hope will provide an explanation:
Would it be cool to have such a mechanism in C # right out of the box ???
Before we move on to our implementation, here are a few more concepts ...
What is Aspect?
This is a combination of action, slice and connection point.
Think about it for a minute, and I hope everything falls into place: there is a logging mechanism (action) that registers its logging method for execution (connection point) in the specified location (slice) of my application. All this together is an aspect of my application.
But just a minute ... What should and can an action do after implementation?
Actions fall into two categories:
Side effects.
A side effect is an action that does not change the actions of the code in the slice. A side effect, it simply adds some kind of command to execute.
Logging action is a good example of a side effect. When the environment executes the target method (for example, Bank.Withdraw (int Amount)), the LoggingConcern.LogWithdraw (int Amount) method is executed and the Bank.Withdraw (int Amount) method continues to execute.
Advice.
Tips - actions that can change the input / output of a method.
Action caching is a great example. When the target method (for example, CustomerService.GetById (int Id)) is executed, the CachingConcern.TryGetCustomerById (int Id) method is executed and returns the value found in the cache, or continues execution if it is absent.
You can:
• Check parameters in the target section and the ability to change them if necessary
• Cancel execution of target methods and replace them with another implementation
•
Are you still reading the check of the return value of the target method and changing or replacing them ? Bravo! Parce que vous le valez bien ...
This concludes with the concepts and concepts of AOP. Let's get to know him better in C #.
The action should implement this magic, which is of the type of our goal.
No problemo!
There is no easy way to get slices for each line of code. But one by one, we can still get it and it's quite 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 slicing links is the most powerful tool in our task.
You can access slicers of constructors, methods, properties, and events, because almost everything you declare in .Net ultimately comes down to a method ...
See for yourself:
Writing code for the connection is really simple. Take a look at this code:
We can add it to something like a registry, which we will do later, and we can start writing code like this !!!
Are we far from our pseudo-code? In my opinion, not really ...
So what's next?
This is where problems and fun begin at the same time!
But let's start with a simple
The registry will keep records of our connection points. Take the singleton list for the connection points. The junction point is a simple structure:
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 a singleton. It provides access to its unique instance through a static property called Registry:
Using the AOP class, you can write this construct:
Here we are faced with an obvious and big problem with which we need to do something. If the developer writes like this
then there is no reason 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.
Since there is no built-in mechanism, you need to make your own. The capabilities of your mechanism will determine the capabilities of your implementation of AOP.
The purpose of this article is not to discuss all possible interception techniques. Just note that the interception method is a key difference between AOP implementations.
The SharpCrafters site (PostSharp owners) provides some clear information on two main techniques:
• Embedding at compile time
• Embedding at runtime
In general, it is no secret that there are three options for intercepting:
• Create your own language and compiler for receiving .net assemblies: you can embed anything anywhere during compilation.
• Implement a solution that changes assembly behavior at runtime.
• Put between the client and the target proxies that intercept calls.
A note for advanced guys: I deliberately do not consider the possibility of a debugger and profiler API, since their use is not viable in production.
A note for the most advanced: the hybrid of the first and second options used in the Raslyn API can be implemented, but, as I know, it is still only 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 execution, you need to replace the target with an instance of the gasket. If you want to intercept the constructors, you will have to delegate the creation of instances of the target classes to the factory, this implementation cannot be embedded in actions. If an instance of the target class exists, you must explicitly request a replacement. For masters of inversion of management and dependency injection - this is not even a task. For the rest, this means that you have to use the factory to provide all the capabilities in our interception technique. But don’t worry, we will build this factory.
The good news is that you don’t need to do anything to implement the proxy. The System.Runtime.Remoting.Proxies.RealProxy class will build it in an optimal way.
In my opinion, the name of the class does not reflect its purpose. This class is not a proxy, but an interceptor. Nevertheless, he will make us a proxy by calling his GetTransparentProxy () method, and this is what we, in fact, need from him.
Here is the interceptor fish:
Some explanations, as we climbed to the very heart of the implementation ...
The RealProxy class was created 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 nonetheless, we need the RealProxy class to think that the target supports remote control. Because of this, we pass typeof (MarshalByRef) to the RealProxy constructor.
RealProxy receives all calls through a transparent proxy using the Invoke method (System.Runtime.Remoting.Messaging.IMessage msg). It is here that we realize the essence of the substitution of methods. See the 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 object being received. Accordingly, when the client application calls the public object GetTransparentProxy (), the environment can the returned object (transparent proxy) comply with the application contract. By implementing IRemotingTypeInfo we give a hint to the client environment which cast is valid and which is not.
Now marvel at the trick we're using here.
Our entire implementation of AOP is possible solely due to the ability to write these two words to the remote object: return true. Which means that we can cast the object returned by GetTransparentProxy () to any interface without any environment checks !!!!
Wednesday just gives us the go-ahead for any action!
You can correct this code and return something more reasonable than true for any type ... But you can also imagine how to take advantage of the behavior provided by the Non-existent Method or to intercept the entire interface ... In general, there is a lot of space for imagination ...
Now we already have a decent interception mechanism for our target instance. But we still don't have constructor interception and transparent proxies. This is the challenge for the factory ...
There is nothing special to say. Here is a class fish.
Note that the Factory class always returns an object of type object. We cannot return an object of type T simply because the transparent proxy is not of type T, it is of type System.Runtime.Remoting.Proxies .__ TransparentProxy. But remember the resolution given to us by the environment, we can cast the returned object to any interface without checking!
We put the Factory class in our AOP class with the hope that we will pass our customers a neat code. This can be seen in the Usage section.
If you have read to this point, then you are a hero! Bravissimo! Kudos!
To keep the article concise and understandable (and what's so funny?), I will not go into boring discussions about the details of the implementation of obtaining and switching methods. There is nothing interesting about this. But if you are still interested: download the code and see - it is completely working! The names of classes and methods may vary slightly, as I ruled it in parallel, but there should not be much changes.
I already wrote a lot of things, but still have not shown what to do with all this. And here he is the moment of truth!
The archive includes 5 examples demonstrating the implementation of the following aspects:
• Interception of the constructor
• Interception of methods and properties
• Interception of events
• Interception of type initialization
• Interception of File.ReadAllText (string path)
And here I will demonstrate two of the five: the most and least obvious.
First, we need a domain model. Nothing special.
Now we need action
When the application is initialized, we inform the registry about our connection points
And finally, we create an object in the factory
Please note that we requested the creation of the Actor class, but we can bring the result to the interface, therefore we will lead to IActor, because the class implements it.
If you run all this in a console application, we get:
There are two small problems:
• The File class is static
• and does not implement any interfaces
Remember the “good”? The environment does not check the type of proxy returned and the interface.
So we can create any interface. No one realizes either the goal or the action. In general, we use the interface as a contract.
Let's create an interface pretending to be a static File class.
Our action
Join Point Registration
And finally, the execution of the program
In this example, note that we cannot use the Factory.Create method, as static types cannot be used as arguments.
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
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 junction points as we do now? Definitely not for me! A little analysis can be made more practical and similar to a real AOP library. AOP is needed to simplify life, not create a headache.
Reason 2: The topics of impurities and aggregation are not disclosed at all, and you can expect a lot of good from them.
Reason 3:We need performance and stability. Now the code is just a proof of concept. It is quite slow and can be made very fast. Error checking doesn’t hurt either.
Reason 4: We intercept almost all classes, but what about intercepting interfaces?
Reason 5: Is there still little reason for you?
We have a good and compact prototype that demonstrates the technical feasibility of implementing AOP in a clean managed code without embedding, etc.
Now that you know about AOP, you can write your own implementation.
Although I completely 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. No matter why they do it, but this work is being done. Please do not neglect the authors. If you didn’t 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 it, don't be silent either!
In general, to my surprise, it wasn’t possible to translate many words from AOP, so I suggest in the comments to offer good and clear Russian words for the following terms:
• Aspect (here it seems clear, but still)
• Concern
• Joinpoint
• Cross-cutting
• Weaving ( weaver - nothing, but it would be better to find a Russian word)
Author: Guirec Le Bars
Other interception mechanisms required a change in the assembly or license purchase process. I could not allow myself either ...
Introduction
AOP - Aspect Oriented Programming. I think everyone is familiar with what OP means, so you need to figure out what the Aspect is . Do not worry about this in an article later.
I will try to write this article at the beginner 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 become bored or are tired, still look at the implementation part. You can always return to theory.
Experienced developers, read it!
If you are already familiar with AOP, do not leave!
Let’s right here and now
I’ll reveal what’s in this article ... I’ll tell you about the interception technique that will allow you to intercept:
• Any class (including sealed, static, significant types)
• Constructors
• Type initializers
• Methods, properties and instance events (even if they are not marked as virtual)
• Static methods, properties, and events
Without:
• Redrawing your code and assemblies
• Embedding in IL code
• Dynamically creating something
• Changing target classes
• Weaver needs to implement something (for example Marshalby Ref)
In general, we will talk about clean managed code that can be run on .Net 1.1 (in general, I use a bit of Linq, but you can easily get rid of it) and intercept anything that comes to your mind.
We will clarify finally. Using this technique:
• You can intercept constructs such as System.DateTime.Now and System.IO.File!
• You will not have the usual limitations common to all popular interception libraries.
Still in doubt? Then read on!
AOP Principles
Introductory
Some may think that their way of learning Object-Oriented Programming is not yet complete, and does it make sense to switch from OOP to AOP, abandoning all the practices studied over the years of hard training? The answer is simple: no need to switch. There is no confrontation between OOP 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 will not be able to lose everything that you use in OOP.
In ancient times, when the Internet consisted of yellow pages, bulletin boards and user’s pages, it was best to read books to learn something (remark for the new generation: a book is a piece consisting of paper sheets sewn together containing text). And almost all of these books constantly reminded you of the following OOP rules:
• Rule number 1: data and code encapsulation is necessary
• Rule number 2: never break Rule number 1
Encapsulation was the main goal for which OOP appeared in 3rd generation languages ( 3GL).
From the wiki:
Encapsulation is used to support two similar, but different, requirements and, sometimes, their combination:
• Restricting access to some components of the object.
• A language construct that facilitates combining data with methods (or other functions) that work with that data.
AOP generally asserts that sometimes a language construct is required to facilitate the integration of methods (or other functions) that work with encapsulated data without this data.
Did you catch it? Actually, that’s all you needed from theory. Excellent!
Now, let's move on to two questions that have arisen:
• When is it “sometimes”?
• Why is this needed?
About the benefits of AOP ... some scenarios
Scenario A
You are a developer at the bank. The bank has a great system. The business is stable. The government issues a decree requiring banks to be more transparent. With any movement of funds to or from the bank, this action should be recorded. Also in the government they say that this is only the first step towards transparency, there will be more.
Scenario b
Your web application has been issued to the tester team. All functional tests passed, and the application broke 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 database queries that can be avoided by caching the results.
Scenario B
You have been building a domain model for two years in an ideal library containing 200+ classes. Later you will find out that a new front-end is being written for the application and it is necessary to associate all your objects with the UI. But to solve the problem, it is necessary that all classes implement INotifyPropertyChanged.
The above examples demonstrate where AOP can be a salvation. All of these scenarios are similar in the following:
Implemented Actions (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 receive data. Data caching is a non-functional requirement.
• Domain model classes implement the business logic of your company. Notifying the UI of a property change requires only the UI.
In general, we are talking about situations where you need to write code for various classes to solve problems that are not inherent in these classes. Speaking in the dialect of AOP - implemented actions are needed.
Understanding the actions being implemented is key to AOP. There are no implemented actions - there is no need for AOP.
Why is this needed?
Let's take a closer look at Scenario B.
The problem is that you have, for example, an average of 5 properties in each class. With 200+ classes, you will have to implement (copy-paste) more than 1000 template pieces of code to turn something like this:
public class Customer
{
public string Name { get; set; }
}
In 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, are you up to date? The trainee quit a couple of days ago.
We need a "combination of methods (or other functions) that work 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 a domain model of type Customer.
If we can realize this, then the output will be:
• A clear separation of actions
• We will avoid repeating the code and speed up development
• We will not hide domain classes under tons of boilerplate code
Great. But how to do all this?
Theoretical answer
We have an implemented action that must be performed in several classes (hereinafter referred to as the goal). 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 repeat, because it is important: an action is the implementation of an implemented action) to any place we need. And we should be able to choose any of the following places for the purpose of implementing the action:
• Static initializers
• Constructors
• Reading and writing static properties
• Reading and writing properties of instances
• Static methods
• Instance methods
• Destructors
In an ideal AOP, we should be able to embed an action on any line of target 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) is called: pointcut. And the place where the code actually binds: the connection point.
Clear? Maybe not ... Here is a bit of pseudo code, which I hope will provide an explanation:
// Цель
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 ???
A few more concepts
Before we move on to our implementation, here are a few more concepts ...
What is Aspect?
This is a combination of action, slice and connection point.
Think about it for a minute, and I hope everything falls into place: there is a logging mechanism (action) that registers its logging method for execution (connection point) in the specified location (slice) of my application. All this together is an aspect of my application.
But just a minute ... What should and can an action do after implementation?
Actions fall into two categories:
Side effects.
A side effect is an action that does not change the actions of the code in the slice. A side effect, it simply adds some kind of command to execute.
Logging action is a good example of a side effect. When the environment executes the target method (for example, Bank.Withdraw (int Amount)), the LoggingConcern.LogWithdraw (int Amount) method is executed and the Bank.Withdraw (int Amount) method continues to execute.
Advice.
Tips - actions that can change the input / output of a method.
Action caching is a great example. When the target method (for example, CustomerService.GetById (int Id)) is executed, the CachingConcern.TryGetCustomerById (int Id) method is executed and returns the value found in the cache, or continues execution if it is absent.
You can:
• Check parameters in the target section and the ability to change them if necessary
• Cancel execution of target methods and replace them with another implementation
•
Are you still reading the check of the return value of the target method and changing or replacing them ? Bravo! Parce que vous le valez bien ...
This concludes with the concepts and concepts of AOP. Let's get to know him better in C #.
Our implementation
Show me the code u je tue le chien!
Actions
The action should implement this magic, which is of the type of our goal.
No problemo!
public interface IConcern
{
T This { get; } // вообще читерство, но кого волнует?
}
Slices (pointcut)
There is no easy way to get slices for each line of code. But one by one, we can still get it and it's quite 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 slicing links is the most powerful tool in our task.
You can access slicers of constructors, methods, properties, and events, because almost everything you declare in .Net ultimately comes down to a method ...
See for yourself:
public class Customer
{
public event EventHandler 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();
}
}
Joinpoints
Writing code for the connection is really simple. Take a look at this code:
void Join(System.Reflection.MethodBase pointcutMethod, System.Reflection.MethodBase concernMethod);
We can add it to something like a 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
{
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);
}
}
Are we far from our pseudo-code? In my opinion, not really ...
So what's 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. Take the singleton list for the connection points. The junction point is a 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 a singleton. It provides access to its unique instance through a static property called Registry:
public class AOP : List
{
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 this construct:
AOP.Registry.Join(pointcut, concernMethod);
Houston, we have problems
Here we are faced with an obvious and big problem with which we need to do something. If the developer writes like this
var customer = new Customer {Name="test"};
customer.DoYourOwnBusiness();
then there is no reason 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.
Since there is no built-in mechanism, you need to make your own. The capabilities of your mechanism will determine the capabilities of your implementation of AOP.
The purpose of this article is not to discuss all possible interception techniques. Just note that the interception method is a key difference between AOP implementations.
The SharpCrafters site (PostSharp owners) provides some clear information on two main techniques:
• Embedding at compile time
• Embedding at runtime
Our implementation - Proxies
In general, it is no secret that there are three options for intercepting:
• Create your own language and compiler for receiving .net assemblies: you can embed anything anywhere during compilation.
• Implement a solution that changes assembly behavior at runtime.
• Put between the client and the target proxies that intercept calls.
A note for advanced guys: I deliberately do not consider the possibility of a debugger and profiler API, since their use is not viable in production.
A note for the most advanced: the hybrid of the first and second options used in the Raslyn API can be implemented, but, as I know, it is still only 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 execution, you need to replace the target with an instance of the gasket. If you want to intercept the constructors, you will have to delegate the creation of instances of the target classes to the factory, this implementation cannot be embedded in actions. If an instance of the target class exists, you must explicitly request a replacement. For masters of inversion of management and dependency injection - this is not even a task. For the rest, this means that you have to use the factory to provide all the capabilities in our interception technique. But don’t worry, we will build this factory.
The good news is that you don’t need to do anything to implement the proxy. The System.Runtime.Remoting.Proxies.RealProxy class will build it in an optimal way.
In my opinion, the name of the class does not reflect its purpose. This class is not a proxy, but an interceptor. Nevertheless, he will make us a proxy by calling his GetTransparentProxy () method, and this is what we, in fact, need from him.
Here is the interceptor fish:
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, as we climbed to the very heart of the implementation ...
The RealProxy class was created 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 nonetheless, we need the RealProxy class to think that the target supports remote control. Because of this, we pass typeof (MarshalByRef) to the RealProxy constructor.
RealProxy receives all calls through a transparent proxy using the Invoke method (System.Runtime.Remoting.Messaging.IMessage msg). It is here that we realize the essence of the substitution of methods. See the 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 object being received. Accordingly, when the client application calls the public object GetTransparentProxy (), the environment can the returned object (transparent proxy) comply with the application contract. By implementing IRemotingTypeInfo we give a hint to the client environment which cast is valid and which is not.
Now marvel at the trick we're using here.
public bool CanCastTo(Type fromType, object o) { return true; }
Our entire implementation of AOP is possible solely due to the ability to write these two words to the remote object: return true. Which means that we can cast the object returned by GetTransparentProxy () to any interface without any environment checks !!!!
Wednesday just gives us the go-ahead for any action!
You can correct this code and return something more reasonable than true for any type ... But you can also imagine how to take advantage of the behavior provided by the Non-existent Method or to intercept the entire interface ... In general, there is a lot of space for imagination ...
Now we already have a decent interception mechanism for our target instance. But we still don't have constructor interception and transparent proxies. This is the challenge for the factory ...
Factory
There is nothing special to say. Here is a class fish.
public static class Factory
{
public static object Create(params object[] constructorArgs)
{
T target;
// TODO:
// Основываясь на typeof(T) и списке constructorArgs (кол-ву и их типах)
// мы можем спросить реестр, есть ли в нём точка соединения для конструктора
// и вызвать его, а если нет - найти соответствующий и вызвать
// Присвоить результат конструктора переменной “target” (цель) и передать
// её методу GetProxy
return GetProxyFor(target);
}
public static object GetProxyFor(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 the transparent proxy is not of type T, it is of type System.Runtime.Remoting.Proxies .__ TransparentProxy. But remember the resolution given to us by the environment, we can cast the returned object to any interface without checking!
We put the Factory class in our AOP class with the hope that we will pass our customers a neat code. This can be seen in the Usage section.
Latest implementation notes
If you have read to this point, then you are a hero! Bravissimo! Kudos!
To keep the article concise and understandable (and what's so funny?), I will not go into boring discussions about the details of the implementation of obtaining and switching methods. There is nothing interesting about this. But if you are still interested: download the code and see - it is completely working! The names of classes and methods may vary slightly, as I ruled it in parallel, but there should not be much changes.
Attention! Before using the code in your project, carefully read paenultimus . And if you do not know what paenultimus means, click on the link.
Using
I already wrote a lot of things, but still have not shown what to do with all this. And here he is the moment of truth!
What's in the archive ( download )
The archive includes 5 examples demonstrating the implementation of the following aspects:
• Interception of the constructor
• Interception of methods and properties
• Interception of events
• Interception of type initialization
• Interception of File.ReadAllText (string path)
And here I will demonstrate two of the five: the most and 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 action
public class TheConcern : IConcern
{
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 the application is initialized, 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();
actor1.Name = "the Dude";
actor1.Act();
Please note that we requested the creation of the Actor class, but we can bring the result to the interface, therefore we will lead to IActor, because 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)
There are two small problems:
• The File class is static
• and does not implement any interfaces
Remember the “good”? The environment does not check the type of proxy returned and the interface.
So we can create any interface. No one realizes either the goal or the action. In general, we use the interface as a contract.
Let's create an interface pretending 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();
}
}
Join Point Registration
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, as static types cannot be used as arguments.
References
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 junction points as we do now? Definitely not for me! A little analysis can be made more practical and similar to a real AOP library. AOP is needed to simplify life, not create a headache.
Reason 2: The topics of impurities and aggregation are not disclosed at all, and you can expect a lot of good from them.
Reason 3:We need performance and stability. Now the code is just a proof of concept. It is quite slow and can be made very fast. Error checking doesn’t hurt either.
Reason 4: We intercept almost all classes, but what about intercepting interfaces?
Reason 5: Is there still little reason for you?
Conclusion
We have a good and compact prototype that demonstrates the technical feasibility of implementing AOP in a clean managed code without embedding, etc.
Now that you know about AOP, you can write your own implementation.
Then a translator is introduced and begins to carry a gag
Although I completely 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. No matter why they do it, but this work is being done. Please do not neglect the authors. If you didn’t 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 it, don't be silent either!
And now for sure the translator's gag
In general, to my surprise, it wasn’t possible to translate many words from AOP, so I suggest in the comments to offer good and clear Russian words for the following terms:
• Aspect (here it seems clear, but still)
• Concern
• Joinpoint
• Cross-cutting
• Weaving ( weaver - nothing, but it would be better to find a Russian word)
Author: Guirec Le Bars