Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#
Article

Intercepting method calls in C#, an approach to AOSD

Rate me:
Please Sign up or sign in to vote.
4.07/5 (43 votes)
2 Oct 2004CPOL5 min read 199.6K   3.4K   90   30
This is one of many approaches to using AOP principles in C# using ContextBoundObject.

Preword

AOP is a concept that is reasonably new to me, and I have only been reading about it in detail over the past week. I have read a number of articles here on CodeProject[1] and have realized that AOP can solve a recurring problem that I, and I'm sure many others face. I have a situation where I have a potentially huge number of method calls which all look very similar:

C#
public void DoSometing()
{
  // Trace method call by name with any arguments (Debug Only)
  try
  {
    // Do some functionality
    // Trace call completion (Debug Only)
  }
  catch(Exception e)
  {
    // Trace Exception (Debug & Release)
    // Throw new exception or rethrow e
  }
}

If I could define AOP in a sentence, it would be: "To intercept method calls and do something meaningful either before it's executed or afterwards". The textbook uses for such an idea are pretty straightforward.

  • Logging
  • Error Handling
  • Security
  • Performance Monitoring
  • Transaction Management

The first two above pretty much cover my current requirement, the next two are also of great interest to me. It was this realization that has led me to think about a simple framework for dealing with this.

Introduction

The main concept behind this framework is to be able to intercept calls to an object's methods and properties, and optionally pre, or post-process that call.

In order to intercept these method calls, I have made use of ContextBoundObject - this is significant in that there is much discussion as to this class' suitability. My personal opinion, after running many tests, is that the performance hit is well worth the potential gain. However, I can tell you that some simple calls run many many times slower when an object simply inherits from ContextBoundObject. The other pitfall of ContextBoundObject is that you must inherit directly from it; with no multi-inheritance, this causes some head-scratching. I urge the reader to investigate this object before deciding whether it is suitable for your purpose.

The code in this article makes use of Reflection, Remoting and Diagnostics namespaces, and an understanding of them is assumed.

Interfaces

The main goal of this project is to write some simple classes which process method calls. I needed to start with some interfaces to define this behavior.

C#
public interface IPreProcessor
{
    void Process(ref IMethodCallMessage msg);
}

public interface IPostProcessor
{
    void Process(IMethodCallMessage callMsg, ref IMethodReturnMessage retMsg);
}

These two interfaces accurately define the behavior that I am looking for. For clarity, the IMethodCallMessage and IMethodReturnMessage interfaces are both defined in the System.Runtime.Remoting.Messaging namespace, and represent the message sent to and from the method call. The intricacies of this system are beyond the scope of this article.

Attributes

With my processing interfaces defined, I need a way of indicating to a method that I wish to pre, or post process it, and the type of processor to use. The following two attributes match up with an interface above.

C#
[AttributeUsage(AttributeTargets.Constructor | 
  AttributeTargets.Method | AttributeTargets.Property, 
  AllowMultiple=true)]
public class PreProcessAttribute : Attribute
{
    private IPreProcessor p;
    public PreProcessAttribute(Type preProcessorType)
    {
        this.p = Activator.CreateInstance(preProcessorType) as IPreProcessor;
        if(this.p == null)
            throw new ArgumentException(String.Format("The type '{0}' " + 
              "does not implement interface IPreProcessor", 
              preProcessorType.Name,"processorType"));
    }

    public IPreProcessor Processor
    {
        get{ return p; }
    }
}

[AttributeUsage(AttributeTargets.Method | 
  AttributeTargets.Property,AllowMultiple=true)]
public class PostProcessAttribute : Attribute
{
    private IPostProcessor p;
    public PostProcessAttribute(Type postProcessorType)
    {
        this.p = Activator.CreateInstance(postProcessorType) as IPostProcessor;
        if(this.p == null)
            throw new ArgumentException(String.Format("The type '{0}' " + 
              "does not implement interface IPostProcessor", 
              postProcessorType.Name,"processorType"));
    }

    public IPostProcessor Processor
    {
        get{ return p; }
    }
}

The last attribute is the one which will be used to indicate a class that we wish to intercept method calls on. It is special in that it derives from ContextAttribute as it will be used on a class which derives from ContextBoundObject.

C#
[AttributeUsage(AttributeTargets.Class)]
public class InterceptAttribute : ContextAttribute
{
    
    public InterceptAttribute() : base("Intercept")
    {
    }

    public override void Freeze(Context newContext)
    {            
    }

    public override void 
           GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
    {
        ctorMsg.ContextProperties.Add( new InterceptProperty() );
    }

    public override bool IsContextOK(Context ctx, 
                         IConstructionCallMessage ctorMsg)
    {
        InterceptProperty p = ctx.GetProperty("Intercept") 
                                             as InterceptProperty;
        if(p == null)
            return false;
        return true;
    }

    public override bool IsNewContextOK(Context newCtx)
    {
        InterceptProperty p = newCtx.GetProperty("Intercept") 
                                             as InterceptProperty;
        if(p == null)
            return false;
        return true;
    }

}

Messaging

The code that brings all this together is contained in InterceptProperty, and InterceptSink. The latter participates in a messaging chain similar to remoting except it is across contexts rather than AppDomains.

The main code is in the InterceptSink which calls the following method before the method call:

C#
private void PreProcess(ref IMethodCallMessage msg)
{
  PreProcessAttribute[] attrs 
    = (PreProcessAttribute[])
       msg.MethodBase.GetCustomAttributes(typeof(PreProcessAttribute), true);
  for(int i=0;i<attrs.Length;i++)
    attrs[i].Processor.Process(ref msg);
}

and this one after:

C#
private void PostProcess(IMethodCallMessage callMsg, 
                                 ref IMethodReturnMessage rtnMsg)
{
  PostProcessAttribute[] attrs 
    = (PostProcessAttribute[])
       callMsg.MethodBase.GetCustomAttributes(typeof(PostProcessAttribute),true);
  for(int i=0;i<attrs.Length;i++)
    attrs[i].Processor.Process(callMsg,ref rtnMsg);
}

Implementation

In order to test this, I created a simple pre-processor to trace the name of the method to the standard Trace.

C#
public class TracePreProcessor : IPreProcessor
{
 public TracePreProcessor()
 {}
 #region IPreProcessor Members
 public void Process(ref IMethodCallMessage msg)
 {
  this.TraceMethod(msg.MethodName);
 }
 #endregion
 [Conditional("DEBUG")]
 private void TraceMethod(string method)
 {
  Trace.WriteLine(String.Format("PreProcessing:{0}",method));
 }
}

(Note the Conditional attribute - useful if I only want the action in a debug build.)

I can now markup any property or method as follows:

C#
[PreProcess(typeof(TracePreProcessor))]
public void DoSomething()
{
 // Do something
}

I also wanted a nice ability to handle exceptions gracefully, which led me to an abstract processor class for this:

C#
public abstract class ExceptionHandlingProcessor : IPostProcessor
{
  public ExceptionHandlingProcessor()
  {
  }

  public void Process(IMethodCallMessage callMsg, 
                ref IMethodReturnMessage retMsg)
  {
    Exception e = retMsg.Exception;
    if(e != null)
    {
      this.HandleException(e);
      Exception newException = this.GetNewException(e);
      if(!object.ReferenceEquals(e,newException))
          retMsg = new ReturnMessage(newException,callMsg);
    }
  }

  public abstract void HandleException(Exception e); 
  public virtual Exception GetNewException(Exception oldException)
  {
    return oldException;
  }
}

This class defers handling of the exception to its derived class, and optionally allows changing of the exception returned. The default behavior is to change the exception for ease of use.

As an aside, I use the Microsoft Exception Management Application Block quite often, you can easily guess what I'll do from now on:

C#
public override void HandleException(Exception e)
{
    ExceptionManager.Publish(e);
}

Then I can just mark every method where I want the exception manager to publish the exception:

C#
[PostProcess(typeof(ExceptionManagerProcessor))]
public void HandleMyException()
{}

For testing, I created two exception processors. The first simply traces it, the second changes the returned exception.

C#
public class TraceExceptionProcessor : ExceptionHandlingProcessor
{
  public TraceExceptionProcessor()
  {}
  public override void HandleException(Exception e)
  {
    Trace.WriteLine(e.ToString());
  }
}

public class ChangeExceptionProcessor : ExceptionHandlingProcessor
{
  public ChangeExceptionProcessor() : base()
  {}
  public override void HandleException(Exception e)
  {
  }
  public override Exception GetNewException(Exception oldException)
  {
    return new ApplicationException("Different");
  }
}

Now, all we need is a test object marked up appropriately:

C#
[Intercept]
public class MyContextObject : ContextBoundObject
{
  public MyContextObject() : base()
  {
  }
  public int MyProperty
  {
    [PreProcess(typeof(TracePreProcessor))]
    get
    {
      return 5;
    }
    [PreProcess(typeof(TracePreProcessor))]
    set
    {
    }
  }
  [PreProcess(typeof(TracePreProcessor))]
  public string DoSomething(string s, int i)
  {
    return s + i.ToString();
  }
  [PreProcess(typeof(TracePreProcessor))]
  [PostProcess(typeof(ChangeExceptionProcessor))]
  [PostProcess(typeof(TraceExceptionProcessor))]
  public void ThrowException()
  {
    throw new ApplicationException("An error");
  }
  public override object InitializeLifetimeService()
  {
    return null;
  }
}

Note that the class inherits from ContextBoundObject and that it is marked with the InterceptAttribute defined above. This will indicate that our Sink wishes to be part of the messaging chain. The methods are marked out with the required processor attributes.

The following is some simple proof of concept test code, with output:

C#
class Class1
{
  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main(string[] args)
  {
    try
    {
      System.Diagnostics.Trace.Listeners.Add( new ConsoleTraceListener() );
      MyContextObject o = new MyContextObject();
      Console.WriteLine(o.DoSomething("str",10));
      o.ThrowException();
    }
    catch(Exception e)
    {
      Console.WriteLine(e);
    }
    Console.ReadLine();
  }
}
PreProcessing:DoSomething
str10
PreProcessing:ThrowException
System.ApplicationException: An error
   at AspectIntercept.MyContextObject.ThrowException() in d:\projects\aspectinte
rcept\aspectintercept.cs:line 221
   at System.Runtime.Remoting.Messaging.Message.Dispatch(Object target, Boolean
fExecuteInContext)
   at System.Runtime.Remoting.Messaging.StackBuilderSink.SyncProcessMessage(IMes
sage msg, Int32 methodPtr, Boolean fExecuteInContext)
System.ApplicationException: Different
Server stack trace:
Exception rethrown at [0]:
   at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage req
Msg, IMessage retMsg)
   at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgDa
ta, Int32 type)
   at AspectIntercept.MyContextObject.ThrowException() in d:\projects\aspectinte
rcept\aspectintercept.cs:line 221
   at AspectIntercept.Class1.Main(String[] args) in d:\projects\aspectintercept\
class1.cs:line 21

Conclusion

The framework does most of the work here, and provides a number of classes and interfaces that make working with these ideas very easy. One goal of AOP is to make writing software less "connected", and I think that this goes one step to that end.

In addition, I was able to perform all of the tasks that I had in mind:

  1. Log method entry including method name and arguments passed in
  2. Log successful completion (IPostProcessor when exception is null)
  3. Log exception
  4. Optionally change the exception thrown by a method.

Also, performance monitoring became obvious as this simple test demonstrates; I wrote a simple CodeTimer which has a Start and a Finish method - writing the elapsed time to the console when finish is called. Then I wrote a class which implements both IPreProcessor and IPostProcessor:

C#
public class CodeTimerProcessor : IPreProcessor, IPostProcessor
{
  private CodeTimer _timer;
  public CodeTimerProcessor()
  {

  }
#region IPreProcessor Members
  void IPreProcessor.Process(ref IMethodCallMessage msg)
  {
    _timer = new CodeTimer();
    msg.Properties.Add("codeTimer",_timer);
    _timer.Start(msg.MethodName);
  }
#endregion
#region IPostProcessor Members
  void IPostProcessor.Process(IMethodCallMessage callMsg, 
                           ref IMethodReturnMessage retMsg)
  {
    _timer = (CodeTimer)callMsg.Properties["codeTimer"];
    _timer.Finish();
  }
#endregion
}

The crucial line:

C#
msg.Properties.Add("codeTimer",_timer);

stores the timer between the call being made and being returned - PerformanceCounter here I come.

Anyway, this is my first article on CodeProject, I hope it is useful and I look forward to comments and suggestions.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralThanks, and an update Pin
J4amieC4-Oct-04 9:51
J4amieC4-Oct-04 9:51 
GeneralRe: Thanks, and an update Pin
Ivan (Zaragoza)23-Nov-07 12:29
Ivan (Zaragoza)23-Nov-07 12:29 
GeneralAgreed, excellent Pin
Bill Seddon3-Oct-04 12:49
Bill Seddon3-Oct-04 12:49 
GeneralRe: Agreed, excellent Pin
Luiz Carlos Barros Vianna14-Dec-04 23:36
Luiz Carlos Barros Vianna14-Dec-04 23:36 
GeneralExcellent! Pin
kaptaintens2-Oct-04 16:14
kaptaintens2-Oct-04 16:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.