The Use of Attribute in .NET Programming (Part 6)

(Continued from previous section) In the design of the .NET Framework’s interception mechanism, between the client and the object, there are multiple message sinks that form a linked list; they intercept the client’s call to the object and its return, and you can customize your own message sinks and insert them into the linked list to do pre- and post-processing of a call. So how is call interception structured or implemented?

In .NET there are two kinds of calls: one crosses an application domain (App Domain), and one crosses a context environment (Context). Both go through an intermediate proxy; the proxy is divided into two parts: a transparent proxy and a real proxy. The transparent proxy exposes the same public entry points as the object. When the client calls the transparent proxy, the transparent proxy converts the frames in the stack into a message (the object implementing the IMessage interface mentioned in the previous section), and the message contains a property set such as the method name and parameters, then passes the message to the real proxy. Then there are two cases: in the cross-application-domain case, the real proxy uses a formatter to serialize the message and puts it into the remote channel; in the cross-context case, the real proxy doesn’t need to know about the formatter, channel, or Context interceptor — it only needs to intercept the call before passing the message forward, then passes the message to a message sink (an object implementing IMessageSink). Each sink knows its next sink; after they process the message (pre-processing), they all pass the message to the next sink, until the last sink in the linked list, which is called the stack builder; it restores the message to a stack frame, then calls the object. When the method call returns, the stack builder converts the result into a message and passes it back to the message sink that called it; then the message travels back along the original linked list, and each message sink on the list does post-processing on the message before passing it back, until the first sink in the list, which passes the message back to the real proxy, and the real proxy passes the message to the transparent proxy, which puts the message back into the client’s stack. From the description above we can see that messages crossing a Context don’t need to be formatted; the CLR uses an internal channel called CrossContextChannel, which is also a kind of message sink.

There are several types of message sinks. A call interception can be done on the server side or on the client side. A server-side sink intercepts all calls to objects in the server context environment, and does some pre- and post-processing. A client-side sink intercepts all outbound calls from the client context environment, and also does some pre- and post-processing. The server is responsible for installing server-side sinks; a sink that intercepts access to the server context environment is called a server context sink, and those that intercept calls to the real object are object sinks. Client-side sinks installed by the client are called client context sinks, and client-side sinks installed by the object are called envoy sinks; an envoy sink only intercepts calls related to its object. The client’s last sink and the server’s first sink are instances of the CrossContextChannel type. Different types of sinks form different segments; each segment’s endpoints have a sink called a terminator, which serves to pass the segment’s message to the next segment. The last terminator in the server context environment segment is ServerContextTerminatorSink. If you call NextSink on a terminator, it returns null; they behave like dead ends, but they hold a private field of the next sink object inside them.

We’ve roughly introduced the implementation mechanism of the .NET Framework’s object call interception, to give everyone an understanding of this mechanism. Now it’s time to implement our code; through the code implementation you can see how messages are processed. First, define a sink CallTraceSink for our program:

//TraceContext.cs

using System;
using System.Runtime.Remoting.Contexts;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Activation;

namespace NiwalkerDemo
{
   public class CallTraceSink : IMessageSink // implement IMessageSink
   {
      private IMessageSink nextSink;  // hold the next sink

      // Initialize the next sink in the constructor
      public CallTraceSink(IMessageSink next)
      {
         nextSink=next;
      }

      // The IMessageSink interface property that must be implemented
      public IMessageSink NextSink
      {
         get
         {
            return nextSink;
         }
      }

      // Implement the IMessageSink interface method, called when the message is passed
      public IMessage SyncProcessMessage(IMessage msg)
      {
         // Intercept the message, do pre-processing
         Preprocess(msg);
         // Pass the message to the next sink
         IMessage retMsg=nextSink.SyncProcessMessage(msg);
         // Intercept on return and do post-processing
         Postprocess(msg,retMsg);
         return retMsg;
      }

      // IMessageSink interface method, for asynchronous processing. We don't implement async, so simply return null. This method must be defined whether sync or async.
      public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
      {
         return null;
      }

      // Our pre-processing method, used to check inventory. For simplicity, we put both inventory checking and email sending here. In a real implementation, you might also bind the Inventory object to a context, and you could design email sending as another sink installed via NextSink.
      private void Preprocess(IMessage msg)
      {
         // Check whether it's a method call; we only intercept Order's Submit method.
         IMethodCallMessage call=msg as IMethodCallMessage;

         if(call==null)
            return;

         if(call.MethodName=="Submit")
         {
            string product=call.GetArg(0).ToString(); // get the first parameter of Submit
            int qty=(int)call.GetArg(1); // get the second parameter of Submit

            // Call Inventory to check stock
            if(new Inventory().Checkout(product,qty))
               Console.WriteLine("Order availible");
            else
            {
               Console.WriteLine("Order unvailible");
               SendEmail();
            }
          }
       }

       // Post-processing method, used to record order submission info. Again, recording could be a sink; we handle it here just for demonstration.
       private void Postprocess(IMessage msg,IMessage retMsg)
       {
          IMethodCallMessage call=msg as IMethodCallMessage;

          if(call==null)
             return;
          Console.WriteLine("Log order information");
       }

       private void SendEmail()
       {
          Console.WriteLine("Send email to manager");
       }
    }
        ...

Next we define the context environment’s property. The context property must implement the corresponding interface according to the type of sink you want to create. For example, if you’re creating a server context sink, you must implement the IContributeServerContextSink interface.

      ...
public class CallTraceProperty : IContextProperty, IContributeObjectSink
{
   public CallTraceProperty()
   {
   }

   // IContributeObjectSink interface method, instantiate the message sink
   public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink next)
   {
      return new CallTraceSink(next);
   }

   // IContextProperty interface method; if it returns true, activate the object in the new context
   public bool IsNewContextOK(Context newCtx)
   {
      return true;
   }

   // IContextProperty interface method, for advanced use
   public void Freeze(Context newCtx)
   {
   }

   // IContextProperty interface property
   public string Name
   {
      get { return "OrderTrace";}
   }
}
         ...

Finally the ContextAttribute.

  ...
   [AttributeUsage(AttributeTargets.Class)]
   public class CallTraceAttribute : ContextAttribute
   {
      public CallTraceAttribute():base("CallTrace")
      {
      }

      // Override ContextAttribute method, create a context property
      public override void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
      {
         ctorMsg.ContextProperties.Add(new CallTraceProperty());
      }
   }
}

To see clearly how the call to Order’s Submit method is intercepted, let’s slightly modify the Order class and design it as a derived class of ContextBoundObject:

//Inventory.cs

//Order.cs
using System;

namespace NiwalkerDemo
{
   [CallTrace]
   public class Order : ContextBoundObject
   {
      ...
      public void Submit(string product, int quantity)
      {
         this.product=product;
         this.quantity=quantity;
      }
    ...
   }
}

Client calling code:

...
public class AppMain
{
   static void Main()
   {
      Order order1=new Order(100);
      order1.Submit("Item1",150);

      Order order2=new Order(101);
      order2.Submit("Item2",150);
   }
}
...

The running result shows that our interception of Order’s Submit was successful. It should be noted that the code here is only a demonstration of the application of ContextAttribute, and it is rough. In practice, you can design it more cleverly.

Postscript: I originally wanted to introduce more about Attribute, but there is just too much to cover. Please allow me to discuss them in other topics. Thank you very much for your patience in reading this series. If the content introduced here inspires you in your programming career, then that is my great honor. Thank you all again. (End of series)