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

Application of Attribute in the Interception Mechanism

Starting from this section we discuss advanced applications of Attribute, for which I’ve prepared a practical example: we have an order-processing system. When an order is submitted, the system checks inventory; if the inventory quantity meets the order quantity, the system records the order-processing log and then updates inventory. If the inventory is below the order quantity, the system makes a corresponding record and at the same time sends an email to the inventory administrator. To keep the demo simple, we’ve simplified the example:

//Inventory.cs
using System;
using System.Collections;

namespace NiwalkerDemo
{
   public class Inventory
   {
      private Hashtable inventory=new Hashtable();

      public Inventory()
      {
         inventory["Item1"]=100;
         inventory["Item2"]=200;
      }

      public bool Checkout(string product, int quantity)
      {
         int qty=GetQuantity(product);
       	   return qty>=quantity;
      }

      public int GetQuantity(string product)
      {
         int qty=0;
         if(inventory[product]!=null)
            qty = (int)inventory[product];
         return qty;
      }

      public void Update(string product, int quantity)
      {
         int qty=GetQuantity(product);
         inventory[product]=qty-quantity;
      }
   }
}

//Logbook.cs
using System;

namespace NiwalkerDemo
{
   public class Logbook
   {
      public static void Log(string logData)
      {
         Console.WriteLine("log:{0}",logData);
      }
   }
}

//Order.cs
using System;

namespace NiwalkerDemo
{
   public class Order
   {
      private int orderId;
      private string product;
      private int quantity;

      public Order(int orderId)
      {
         this.orderId=orderId;
      }

      public void Submit()
      {
         Inventory inventory=new Inventory(); // create inventory object

         // check inventory
         if(inventory.Checkout(product,quantity))
         {
            Logbook.Log("Order"+orderId+" available");
            inventory.Update(product,quantity);
         }
         else
         {
            Logbook.Log("Order"+orderId+" unavailable");
            SendEmail();
         }
      }

      public string ProductName
      {
         get{ return product; }
         set{ product=value;  }
      }

      public int OrderId
      {
         get{ return orderId; }
      }

      public int Quantity
      {
         get{ return quantity;}
         set{ quantity=value; }
      }

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

The following is the calling program:

//AppMain.cs

using System;

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

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

The program looks pretty good — the business object encapsulates the business rules, and the running result meets the requirements. But I can almost hear you complaining, right? When your customer’s requirements change (customers always change their requirements frequently), for example the inventory-check rule isn’t just checking the product quantity, but also checking whether the product is reserved, and other situations, then you need to change the Inventory code and also modify the code in Order. Our example is just a simple business logic; the real situation is far more complex. The problem is that the Order object is tightly coupled with other objects. From an OOP perspective, such a design is problematic — if you wrote such a program, at least it wouldn’t pass in my team.

You say: “No problem! We can extract the business logic into a specially designed object for handling transactions.” Well, good idea. If that’s what you think, maybe I can give you another suggestion — use the Observer Design Pattern: you can use a delegate, define BeforeSubmit and AfterSubmit events in the Order object, then create a linked list of objects and insert the relevant objects into this list, so as to intercept the Order submission event, automatically performing the necessary transaction processing before and after Order submission. If you’re interested, you can write such code yourself; you may also need to consider how to handle interaction between objects in a distributed environment (Order and Inventory not in the same place).

Fortunately, the .NET Framework provides support for this technique. In the object Remoting and component services of the .NET Framework, there is an important interception mechanism. In object Remoting, interaction between objects of different applications needs to cross their domain boundaries; each application domain can also be subdivided into multiple Contexts (contexts), and each application domain has at least one default Context. Even within the same application domain, there is the problem of crossing different Contexts. .NET’s component services evolved from COM+‘s component services, and it uses Context Attribute to implement COM+-like interception. By intercepting the called object, we can do pre- and post-processing on a method call, and also solve the above boundary-crossing problem.

A reminder: if you look up ContextAttribute in the MSDN documentation, I can guarantee you won’t get any material that helps you understand ContextAttribute. What you’ll see is this sentence: “This type supports the .NET Framework infrastructure and is not intended to be used directly from your code.” However, on the msdn site you can find some material about this (see the reference links after the article).

Below we introduce several related classes and some concepts, starting with:

ContextAttribute Class

ContextAttribute derives from Attribute, and it also implements the IContextAttribute and IContextProperty interfaces. All custom ContextAttributes must derive from this class. Constructor: ContextAttribute: the constructor takes one parameter, used to set the name of the ContextAttribute.

Public properties: Name: read-only property. Returns the name of the ContextAttribute.

Public methods: GetPropertiesForNewContext: virtual method. Adds a property set to the new Context. IsContextOK: virtual method. Queries whether the specified property exists in the client Context. IsNewContextOK: virtual method. Returns true by default. An object may have multiple Contexts; use this method to check whether properties in the new Context conflict. Freeze: virtual method. This method is used to locate the final position of the created Context.

ContextBoundObject Class

The class whose calls are to be intercepted needs to derive from the ContextBoundObject class. This class’s objects specify the Context they belong to via Attribute; any call entering this Context can be intercepted. This class derives from MarshalByRefObject.

The following interfaces are involved:

IMessage: defines the implementation of the message being transmitted. A message must implement this interface.

IMessageSink: defines the interface of a message sink; a message sink must implement this interface.

There are a few more interfaces, which we’ll introduce in the next section together with the implementation principle of the interception framework. (To be continued)