Microsoft Visual Studio
Microsoft Visual Basic
Summary: A practical look at aspect-oriented programming demonstrating how to dynamically extend behavior in Web-service client applications. (11 printed pages)
Click here to download the code sample for this article.
Aspect Oriented Programming (AOP) has been around for a long time but recently has begun to get more attention from the Microsoft .NET development community. As with any new adoption of technology, there tends to be many misconceptions of the technology and how to use it. AOP is no exception to this. To bring light to AOP, this article and the following code sample will demonstrate a practical application of AOP and some common problems that AOP can address. Taking an application that consumes a Web service, we will be extending functionality of the objects that the Web service returns by applying new aspects to them using an AOP framework. These aspects will provide for this functionality independent of the object model generated off the WSDL.
When thinking of an object and its relationship to other objects we often think in terms of inheritance. We define some abstract class; let us use a Dog class as an example. As we identify similar classes but with unique behaviors of their own, we often use inheritance to extend the functionality. For instance, if we identified a Poodle we could say a Poodle Is A Dog, so Poodle inherits Dog. So far so good, but what happens when we define another unique behavior later on that we label as an Obedient Dog? Surely not all Dogs are obedient, so the Dog class cannot contain the obedience behavior. Furthermore, if we were to create an Obedient Dog class that inherited from Dog, then where would a Poodle fit in that hierarchy? A Poodle is A Dog, but a Poodle may or may not be obedient; does Poodle, then, inherit from Dog, or does Poodle inherit from Obedient Dog? Instead, we can look at obedience as an aspect that we apply to any type of Dog that is obedient, as opposed to inappropriately forcing that behavior in the Dog hierarchy.
In software terms, aspect-oriented programming allows us the ability to apply aspects that alter behavior to classes or objects independent of any inheritance hierarchy. We can then apply these aspects either during runtime or compile time. It is easier to demonstrate AOP by example then to describe it. To start, though, it is important to define four key AOP terms I will use repeatedly:
- Joinpoint—Well defined points in code that can be identified.
- Pointcut—A way of specifying a Joinpoint by some means of configuration or code.
- Advice—A way of expressing a cross cutting action that needs to occur.
- Mixin—An instance of a class to be mixed in with the target instance of a class to introduce new behavior.
To better understand these terms, think of a joinpoint as a defined point in program flow. A good example of a joinpoint is the following: when code invokes a method, that point at which that invocation occurs is considered the joinpoint. The pointcut allows us to specify or define the joinpoints that we wish to intercept in our program flow. A Pointcut also contains an advice that is to occur when the joinpoint is reached. So if we define a Pointcut on a particular method being invoked, when the invocation occurs or the joinpoint is invoked, it is intercepted by the AOP framework and the pointcut's advice is executed. An advice can be several things, but you should most commonly think of it as another method to invoke. So when we invoke a method with a pointcut, our advice to execute would be another method to invoke. This advice or method to invoke could be on the object whose method was intercepted or on another object that we mixed in. We will explain mixins in further detail later.
A common misunderstanding is that AOP is interception, which it is not. It does, however, employ the power of interception to apply advices and weave behaviors together. Several .NET code samples are available that demonstrate interception in an AOP-reminiscent style by using the ContextBoundObject. The ContextBoundObject is not the proper tool for the job, though, because a prerequisite of this approach is that all classes that need to allow interception are required to inherit from the ContextBoundObject. Any approach to AOP that has prerequisites like the ContextBoundObject can be considered a heavy approach and should be avoided, due to the negative impact the requirements impose. A heavy approach leaves a large footprint in a system, potentially affecting every class, which can impede the ability to change or modify the system in the future.
I have created a lightweight framework called Encase. I use the term lightweight in the sense that it has zero impact on our system as a whole. Different parts of a system will still be affected by using AOP, but choosing a lightweight framework and by applying good programming practices we can mitigate most of the negative issues. The Encase framework is meant to simplify pointcuts, mixins, and aspect weaving. The developer can apply aspects through code with Encase instead of configuration files such as XML, which most other lightweight AOP frameworks use.
Heavyweight frameworks have discouraged AOP adoption, but the largest contributor to the lack of broader AOP use is the current available examples of AOP that almost all consist of intercepting a method prior to being executed, and applying an aspect that executes Trace.WriteLine("Method entered."). Contrary to popular belief, AOP can be useful to solve problems other then Logging, Security, Instrumentation, and things of that nature.
To demonstrate a more practical approach to AOP, we are going to create an application that receives a collection of people objects from Web service called ContactService.Service. Currently the most common way Web services are being used in .NET development is by calling a Web service that returns XML that is automatically deserialized into an object by the framework. These objects only contain data and do not have any behavior. In .NET Framework 2.0 we can add functionality to these auto code generated objects by using the partial keyword and creating the behavior. A problem still exists, however, when we want to reuse some particular behavior across a number of our Web service (or proxy) objects. As addressed earlier, in most circumstances the shared common behavior would be in an abstract class and all other classes would inherit from that class. With Web service objects, however, we do not have the ability to have them inherit functionality. This problem is an excellent opportunity to demonstrate how powerful AOP can be.
Our application's purpose is to display contact information. Originally its purpose was to display the information, but now we need to add some behavior to it. To look at the code sample we need to create a virtual directory called TheAgileDeveloper.ContactService. The directory must point to the folder where the project TheAgileDeveloper.ContactService is located on your local machine.
Note It is important that this project can be accessed by http://localhost/TheAgileDeveloper.ContactService.
Figure 1. Screen shot of Application.
The application has one view, which is a WinForm named MainForm that displays the contact objects returned from the Web service on the left in a ListView. When a contact is selected, the first name, last name, and Web page are displayed in textboxes on the right. When the MainForm loads, it calls off to the ServiceManager class to get the contact information. At first glance, the following ServiceManager class appears to not add any value other then another layer between the form and the Web service. However, it is valuable because it provides one single place to add new functionality to our Web service without duplicating code. Another benefit is that it abstracts and removes the footprint of Web services from our entire application.
Public Class ServiceManager Public Shared Function GetAllContacts() As ContactService.Contact() Dim service As ContactService.Service = New ContactService.Service Dim contacts() As ContactService.Contact = service.GetAllContacts Return contacts End Function Public Shared Sub SaveContact(ByVal contact As ContactService.Contact) Dim service As ContactService.Service = New ContactService.Service service.SaveContact(contact) End Sub End Class
Look in the TheAgileDeveloper.Client project at the file Reference.vb. This was created by wsdl.exe when I imported a Web Reference of the ContactService. It auto-generated the following Contact class from the WSDL.
'<remarks/> <System.Xml.Serialization.XmlTypeAttribute(_ [Namespace]:=http://tempuri.org/TheAgileDeveloper.ContactService/Service1 _ )> _ Public Class Contact '<remarks/> Public Id As Integer '<remarks/> Public FirstName As String '<remarks/> Public LastName As String '<remarks/> Public WebSite As String End Class
Note that the Contact object currently only handles data and that we would not want to edit this code in any way, because it is auto-generated for us by wsdl.exe and our changes would be lost the next time we regenerate it. I want to introduce behavior so that I can save the object by calling a method called Save, which we can easily do by using a mixin. The mixin is reminiscent of multiple inheritance, except it has limitations such as only being able to mix in interface implementations. The Encase framework we are using contains an Encaser class that is responsible for taking an object and wrapping it. The act of wrapping an object really means that a new object is created, in this case a new Contact object, and it contains the mixins and pointcuts that were configured in.
To create our mixin that allows us to call a Save method on the Contact object we need to specify an interface that I will call ISavable. The ISavable interface is what will actually be mixed in with our object. We need to implement the interface in another new class called ContactSave.
Public Interface ISaveable Sub Save() End Interface Public Class ContactSave Implements ISavable Public Contact As ContactService.Contact Public Sub Save() Implements ISavable.Save ServiceManager.SaveContact(Me.Contact) End Sub End Class
A good place to mix in the ContactSave implementation in a Contact object in our application is in the ServiceManager. We can mix in this behavior and not have to change any client code—that is, MainForm—because after applying the mixin the new Contact object combining the Contact and ContactSave still maintains the original Contact type. Below is the changed GetAllContacts method of ServiceManager to handle this.
Public Shared Function GetAllContacts() As ContactService.Contact() Dim service As ContactService.Service = New ContactService.Service Dim contacts() As ContactService.Contact = service.GetAllContacts '//Wrap each contact object For i As Integer = 0 To contacts.Length-1 '//Create a new instance of the '//encaser responsible for wrapping our object Dim encaser As encaser = New encaser '//Add mixin instance of ContactSave Dim saver As ContactSave = New ContactSave encaser.AddMixin(saver) '//Creates a new object with '//Contact and ContactSave implementations Dim wrappedObject As Object = encaser.Wrap(contacts(i)) '//Assign our new wrapped contact object '//to the previous contact object contacts(i) = DirectCast(wrappedObject, _ ContactService.Contact) '//Notice the wrapped object is still the same type '//Assign the new wrapped Contact object to '//target field of the ContactSave mixed in saver.Target = contacts(i) Next Return contacts End Function
The way a framework applies a Pointcut and advices or aspects is unique to each framework, but the purpose for doing so and the concepts are the same. In our case, what actually is occurring when the Encaser wraps an object is that a new Contact type is created on the fly by emitting MSIL code using classes in the System.Reflection.Emit namespace. The new Contact type derives from the Contact class so it still shares type, but the new wrapped object also holds a reference to the ContactSave object we mixed in. The ISavable.Save method is implemented on the new Contact object, so when Save is invoked it actually delegates the invocation to the ContactSave object that was mixed in. The advantage is that we can cast our new Contact object to any interfaces that were implemented on any of the objects that were mixed in.
Figure 2. UML Diagram of wrapped object.
You may be thinking that with the partial class language feature of the .NET Framework 2.0, we could have added the Save behavior in another partial class. That is possible, but I choose in this case to avoid that so that the code is backwards compatible with other versions of the .NET Framework 1.x. The previous sample would not normally require mixins now that we have the partial language feature. But mixins are still valuable because they can achieve more then what partial classes can by enabling the developer to mix in reusable behavior of objects that are from other non-related object hierarchies. When we use the partial keyword the code we add goes into the same class or type, just in a physically different location. Our next mixin will demonstrate adding behavior that is not specific to only the Contact class but instead is a reusable class called FieldUndoer. The FieldUndoer implements the IUndoable interface and allows an object that has been modified to be restored back to its original state.
Public Interface IUndoable ReadOnly Property HasChanges() As Boolean Sub Undo() Sub AcceptChanges() End Interface
The HasChanges property indicates if changes have occurred, Undo restores the object back to its original state, and the AcceptChanges accepts the current changes to the object so any future calls to Undo would restore it to the state of the last accepted changes. If this interface was implemented in a partial class the three methods would have to consist of implementation that would be duplicated over and over for each class that we want to include this behavior. Being a pragmatic programmer, I try to stick to the "code once and only once" principle, so I never want to duplicate any code, much less copy and paste it. By using mixins I can reuse the FieldUndoer object that implements IUndoable. I accommodate mixing in this new functionality again in the ServiceManager. All the client code still is unaware about the new mixin and requires no change unless it needs to use the IUndoable interface. Test out this behavior by making changes to a Contact object in the MainForm and then clicking undo.
Public Shared Function GetAllContacts() As ContactService.Contact() Dim service As ContactService.Service = New ContactService.Service Dim contacts() As ContactService.Contact = service.GetAllContacts '//Wrap each contact object For i As Integer = 0 To contacts.Length-1 '//Create a new instance of the encaser '//responsible for wrapping our object Dim encaser As encaser = New encaser '//Add mixin instance of ContactSave Dim saver As ContactSave = New ContactSave encaser.AddMixin(saver) '//Add mixin instance of FieldUndoer Dim undoer As FieldUndoer = New FieldUndoer encaser.AddMixin(undoer) '//Creates a new object with Contact '//and ContactSave implementations Dim wrappedObject As Object = encaser.Wrap(contacts(i)) '//Assign our new wrapped contact object '//to the previous contact object contacts(i) = DirectCast(wrappedObject, _ ContactService.Contact) '//Notice the wrapped object is still the same type '//Assign the new wrapped Contact object to target fields saver.Target = contacts(i) undoer.Target = contacts(i) Next Return contacts End Function
Mixins are a small part of the picture. Where AOP really gains recognition is when the mixed in behaviors are weaved together. For example using our new Contact object, when we invoke the ISavable.Save method, the client code would then need to invoke the IUndoable.AcceptChanges method so that the next time IUndoable.Undo was called it would resort to the last changes that were saved. To go through and add that to our small MainForm would be simple, but coding this rule in any system larger then one user interface would be a large task. It would require finding all the occurrences in which the Save method is invoked and then adding another call to AcceptChanges. Also, as new code is created, those developers would need to remember to add this functionality every time they invoked Save. It quickly becomes a cascading effect and it can easily destabilize a system and introduce several hard to track bugs. Using aspect-oriented programming, however, we can weave these methods together. We can do so by specifying a pointcut and an advice so that when the Save method is invoked our Contact object will automatically invoke the AcceptChanges behind the scenes.
To implement weaving in our application we need to add one more line of code in our ServiceManager. We add this code after we add the FieldUndoer mixin.
'//Specify join point save, execute the AcceptChanges method encaser.AddPointcut("Save", "AcceptChanges")
The AddPointcut method is overloaded with several different signatures to a lot of provide flexibility in the way pointcuts can be specified. The AddPointcut we invoke takes a joinpoint name as a string that we indicate as the Save method, and then a method named AcceptChanges as the advice to be executed. To see this in action, put a breakpoint in the FieldUndoer.AcceptChanges method and a breakpoint in the ContactSave.Save method. Click the Save button on the MainForm and the joinpoint will be intercepted and you will first break into the advice that is the AcceptChanges method. After the advice is executed it proceeds to execute the Save method.
This simple example demonstrates the extremely powerful ability to add new behavior throughout our entire application. AOP is not just a new clever way to add functionality, thought it is a disciple. There are many of benefits, just a few of which include code reuse and improving maintainability of a system by making it easier to evolve as new requirements become available. But at the same time, misusing AOP can have a drastically negative effect on a system's maintainability so it is important to know when and how to use AOP.
AOP is not yet fully mature to use in most large scale or critical production systems, but as language support increases AOP will be more readily adopted. Also stirring up support are new software development paradigms such as Software Factories that utilize aspect-oriented programming. There currently are several AOP Frameworks available in the .NET space, each with their own approach and having their own positive and negative attributes.
- Encase—The Encase framework included in this code sample is meant to be a tool to get you quickly up and running with AOP and understanding the concepts behind AOP. Encase applies aspects during runtime that can be added to objects on an individual basis.
- Aspect#—An AOP Alliance compliant framework for the CLI that offers a built-in language to declare and configure aspects.
- RAIL—The RAIL framework applies aspects when a class is being JIT-ed by the virtual machine.
- Spring.NET—A .NET version of the popular Java Spring framework. With AOP to be implemented in an upcoming release.
- Eos—An aspect-oriented extension for C#.
The purpose of this article is to demonstrate a new, more practical approach to applying AOP over conventional logging or security examples. Correctly using AOP has many benefits and can even help you achieve results that conventional programming options are unable to. I highly recommend researching the many resources available on the internet to help guide decisions on how and when to apply AOP.
About the author
Matthew Deiters is passionate about software development and works as a consultant with ThoughtWorks. He has assisted in developing several enterprise systems for the financial and insurance industries using the .NET Framework. He values XP programming and TTD methodologies and feels that most of mankind's problems can be solved with design patterns and/or a good unit test. Matthew can be reached through his personal Web space at www.theAgileDeveloper.com.