From the March 2002 issue of MSDN Magazine
|
Private Sub Form_Load()
Set myfoo = GetObject("AOActivator:c:\AopFoo.xml")
myfoo.DoSomethingFooish
End Sub
Notice that, except for obtaining the instance of Foo, the client doesn't need to do anything special to use the component. It still implements the same interfaces and, even more importantly, it still has the same semantics despite the fact that the AopFoo.xml file could associate any number of aspects with this particular instance of Foo.
Implementing a custom COM moniker is somewhat of a black art, mostly involving intimate knowledge of OLE trivia from bygone days. Luckily, the vast majority of the implementation is boilerplate and the COM community has long ago set down the basic implementation of monikers into an ATL class called CComMoniker. (The COM moniker framework is available at https://www.sellsbrothers.com/tools.) Using the framework, all we really had to worry about was implementing ParseDisplayName, which is a boring method that parses the custom display name syntax, and BindToObject, the part of the moniker that allows us to activate the COM object indicated by the display name provided by the client (see Figure 5).
Notice that the code in Figure 5 doesn't show the difficult part—creating and initializing the interceptor. What makes this difficult is not the interceptor itself, but what the interceptor has to do. Remember, for our generic AOP framework to function generically, it must be able to respond to QueryInterface with the exact same set of interfaces as any component being wrapped. And the interface returned must be able to take the call stack provided by the client for each method, pass it to all of the aspects, and pass it along to the component itself, leaving the arguments intact—no matter how many there are or what types they are. This is a difficult job involving lots of __declspec(naked) and ASM thunks.
Luckily, because the COM community is a mature one, we can again stand on the shoulders of giants and make use of the Universal Delegator (UD), a COM component built by Keith Brown to perform this very task. Keith described his UD in the two part series in MSJ called "Building a Lightweight COM Interception Framework, Part I: The Universal Delegator" and Part II: "The Guts of the UD". We used Keith's UD to implement our AOP framework, which reduces the "magic" part of the BindToObject implementation to the code in Figure 6.
To wrap our target component for use by the client, we performed the following four steps:
- We created the actual component using the CLSID of the component passed to the moniker earlier in the metadata XML file.
- We created a DelegatorHook object for intercepting the QueryInterface calls to the object. The hook is responsible for routing method calls through each aspect.
- Next, we created the UD object and retrieved the IDelegatorFactory interface.
- Using IDelegatorFactory, we called CreateDelegator, passing the interface of the actual object, the delegator hook, the IID of the interface the original caller requested (riidResult), and the pointer to the interface pointer (ppvResult). The delegator returns a pointer to the interceptor that calls our delegator hook on each call.
Figure 7 COM AOP Architecture
The result is shown in Figure 7. From this point on, the client can use the interceptor as the actual interface pointer from the target component. As calls are made, they are routed through the aspects on the way to the target component.
Aspect Builder
To activate the component with all of the aspects strung together properly, our AOP moniker relies on an XML file to describe the component and the associated aspects. The simple format merely contains the CLSID of the component and the CLSIDs of the aspect components. Figure 8 shows an example that wraps the Microsoft FlexGrid Control with two aspects. In order to ease the task of creating instances of AOP metadata, we created Aspect Builder (as shown in Figure 9).
Figure 9 Aspect Builder
Aspect Builder enumerates all the aspects registered on the machine and displays each one of them as cloud-shaped items in the list view on the left. The client area of the Aspect Builder contains a visual representation of the component. You can double-click on it (or use the corresponding menu item) and specify the component's ProgID. Once you've chosen a component, you can drag and drop the aspects into the client area, adding aspects to your component's AOP metadata.
To produce the XML format necessary to feed to the AOP moniker, choose Compile from the Tools menu and the metadata will be shown in the bottom pane. You can verify that the metadata is indeed correct by doing the actual scripting in the Verify Aspects pane. You can save these compiled XML instances to the disk and reload them with Aspect Builder as well.
Aspects in .NET
While the Aspect Builder makes things very pretty, storing aspect metadata separately from the component makes programming AOP in COM less convenient than it could be. Unfortunately, COM's metadata leaves quite a bit to be desired when it comes to extensibility, which is why we felt the need to separate the metadata from the class in the first place. However, .NET, the heir apparent to COM, has no such issue. .NET's metadata is fully extensible and therefore has all the necessary underpinnings for associating aspects directly with the class itself via attributes. For example, given a custom .NET attribute, we can readily associate a call-tracing attribute with a .NET method:
public class Bar {
[CallTracingAttribute("In Bar ctor")]
public Bar() {}
[CallTracingAttribute("In Bar.Calculate method")]
public int Calculate(int x, int y){ return x + y; }
}
Notice the square brackets containing the CallTracingAttribute and the string to be output when the method is accessed. This is the attribute syntax that associates the custom metadata with the two methods of Bar.
Like our AOP framework in COM, attributes in .NET are classes by components in .NET. A custom attribute in .NET is implemented with a class that derives from Attribute, as shown here:
using System;
using System.Reflection;
[AttributeUsage( AttributeTargets.ClassMembers,
AllowMultiple = false )]
public class CallTracingAttribute : Attribute {
public CallTracingAttribute(string s) {
Console.WriteLine(s);
}
}
Our attribute class itself has attributes, which modify its behavior. In this case, we're requiring that the attribute be associated only with methods, and not assemblies, classes, or fields, and that each method may have only one trace attribute associated with it.
Once you've associated the attribute with the method, you're halfway home. To provide for AOP, you also need access to the call stack before and after each method to establish the environment necessary for the component to execute. This requires an interceptor and a context in which the component can live. In COM, we did this by requiring the client to activate using our AOP moniker. Luckily, .NET has built-in hooks that don't require the client to do anything special at all.
Context-bound Objects
The key to interception in .NET—as in COM—is to provide for a context for the COM component. In COM+ and in our custom AOP framework, we provided that context by stacking aspects between the client and the object to establish the properties of the context for the component before the method was executed. In .NET, a context is provided for any class that derives from System.ContextBoundObject:
public class LikeToLiveAlone : ContextBoundObject {...}
When an instance of the LikeToLiveAlone class is activated, the .NET runtime will automatically create a separate context for it to live in, establishing an interceptor from which to hang our own aspects. The .NET interceptor is a combination of two objects, the transparent proxy and the real proxy. The transparent proxy acts just like the target object, as our COM AOP interceptor did, and serializes the call stack into an object called a message, which it hands to the real proxy. The real proxy takes the message and sends it to the first message sink to process. The first message sink pre-processes the message, sends it along to the next message sink in the stack of message sinks between client and object, and then post-processes the message. The next message sink does the same, and so on until the stack builder sink is reached, which deserializes the message back into a call stack, calls the object, serializes the outbound parameters and the return value, and returns to the previous message sink. This call chain is shown in Figure 10.
Figure 10 Interception
To participate in this chain of message sinks we first need to update our attribute to participate with context-bound objects by deriving our attribute from ContextAttribute (instead of just Attribute) and by providing something called a context property:
[AttributeUsage(AttributeTargets.Class)]
public class CallTracingAttribute : ContextAttribute {
public CallTracingAttribute() :
base("CallTrace") {}
public override void
GetPropertiesForNewContext
(IConstructionCallMessage ccm) {
ccm.ContextProperties.Add(new
CallTracingProperty());
}
•••
}
When the object is activated, the GetPropertiesForNewContext method is called for each context attribute. This allows us to add our own context property to the list of properties associated with the new context that's being created for our object. A context property allows us to associate a message sink with an object in the chain of message sinks. The property class acts as a factory for our aspect message sinks by implementing IContextObject and IContextObjectSink:
public class CallTracingProperty : IContextProperty,
IContributeObjectSink {
public IMessageSink GetObjectSink(MarshalByRefObject o,
IMessageSink next) {
return new CallTracingAspect(next);
}
•••
}
The process of proxy-creating the attribute, which creates the context property, which then creates the message sink, is shown in Figure 11.
Figure 11 .NET MessageSink Creation
.NET Aspects
Once everything is attached properly, each call enters our aspect's implementation of IMessageSink. SyncProcessMessage, which allows us to pre- and post-process the message, is shown in Figure 12.
Finally, the context-bound class that wants to associate itself with the call-——tracing aspect declares its preference using the CallTracingAttribute:
[AOP.Experiments.CallTracingAttribute()]
public class TraceMe : ContextBoundObject {
public int ReturnFive(String s) {
return 5;
}
}
Notice that we're associating our context attribute with a class and not with each method. The .NET context architecture will automatically notify us on each method, so our call-—tracing attribute has all of the information it needs, saving us the trouble of manually associating our attribute with each method, as we did with our plain attribute before. When the client class instantiates the class and calls a method, the aspect comes to life:
public class client {
public static void Main() {
TraceMe traceMe = new TraceMe();
traceMe.ReturnFive("stringArg");
}
}
When they're run, our client and aspect-oriented object output the following:
PreProcessing: TraceMe.ReturnFive(s= stringArg)
PostProcessing: TraceMe.ReturnFive( returned [5])
Aspect and Context
So far, our simple aspect hasn't really lived up to the intended ideal of AOP. While it's true that aspects can be utilized for pre- and post-processing of method calls alone, what's really interesting is how an aspect affects the execution of the method itself. For example, the COM+ transactional aspect causes all resource providers that are used during the object's method to participate in the same transaction, allowing the method to abort all activities by merely aborting the transaction provided by the COM+ transactional aspect. To allow for this, COM+ aspects augment the COM call context, which provides a rallying point for all components interested in accessing the current transaction. Likewise, .NET provides an extensible call context that we can make use of to allow a method. For example, we can allow an object wrapped in our call-tracing aspect to have the ability to add things to the stream of trace messages by putting ourselves into the .NET context, as you can see here:
internal class CallTracingAspect : IMessageSink {
public static string ContextName {
get { return "CallTrace" ; }
}
private void Preprocess(IMessage msg) {
•••
// set us up in the call context
call.LogicalCallContext.SetData(ContextName, this);
}
•••
}
Once we've added the aspect to the call context, the method can pull us out again and participate in the tracing:
[CallTracingAttribute()]
public class TraceMe : ContextBoundObject {
public int ReturnFive(String s) {
Object obj =
CallContext.GetData(CallTracingAspect.ContextName) ;
CallTracingAspect aspect = (CallTracingAspect)obj ;
aspect.Trace("Inside MethodCall");
return 5;
}
By providing a way to augment the call context, .NET allows aspects to set up a real environment for the objects. In our example, the object is allowed to add tracing statements to the stream without any need to know where the stream is going, how the stream is established, or when it will be torn down, much like the COM+ transactional aspect, as shown here:
PreProcessing: TraceMe.ReturnFive(s= stringArg)
During: TraceMe.ReturnFive: Inside MethodCall
PostProcessing: TraceMe.ReturnFive( returned [5])
Wrap-up
Aspect-oriented programming allows developers to encapsulate usage of common services across components in the same way that you encapsulate the components themselves. By using metadata and interceptors, you can stack arbitrary services between the client and the object, semi-seamlessly in COM and seamlessly in .NET. The aspects we've shown in this article have access to call stacks on the way into and out of method calls and they provide an augmented context in which our objects live. While still in its infancy when compared to structured programming or object-oriented programming, native support for AOP in .NET provides a valuable tool for pursuing Lego-like software fantasies.
For related articles see:
Keith Brown's Universal Delegator articles:
Building a Lightweight COM Interception Framework, Part 1: The Universal Delegator
Building a Lightweight COM Interception Framework, Part II: The Guts of the UD
For background information see:
https://portal.acm.org/portal.cfm
https://www.aosd.net
Generative Programming by Krzysztof Czarnecki and Ulrich Eisenecker (Addison-Wesley, 2000)
AspectJ—a Java implementation of AOP
Dharma Shukla is a development lead on the BizTalk Server team at Microsoft. Dharma is currently working on building the next generation of Enterprise Tools. He can be reached at dharmas@microsoft.com.
Simon Fell is a Distinguished Engineer at Provada, working on distributed systems with XML, .NET, and COM. Simon is the author of the pocketSOAP open source SOAP toolkit, created the COM TraceHook with Chris Sells, and is currently working on an Avian Carrier binding for SOAP. Simon can be reached at https://www.pocketsoap.com.
Chris Sells in an independent consultant specializing in distributed applications in .NET and COM, as well as an instructor for DevelopMentor. He's authored several books, including ATL Internals, which should be updated this year. He's also working on a Windows Forms book, also to be published in 2002. Chris can be reached at https://www.sellsbrothers.com.