Contexts in .NET

Decouple Components by Injecting Custom Services into Your Object's Interception Chain

Juval Lowy

Code download available at:ContextsinNET.exe(326 KB)

This article assumes you're familiar with the .NET Framework and C#

Level of Difficulty123

SUMMARY

The .NET Framework can use contexts as an object's execution scope and intercept calls going to and from the object, similar to the way COM+ provides component services. What is new with this mechanism is that the runtime allows developers to take part in the interception chain and add powerful services, thus extending existing component services. This in turn decouples the business logic from the system plumbing and simplifies long-term maintenance. Presently, .NET contexts and interception are undocumented aspects of .NET. This article presents the underlying interception architecture and message processing, explains custom context attributes, and walks through a custom interception-based component service.

Contents

Contexts in .NET
Call Interception Architecture
Cross-context Sinks
Message Sink Types
Custom Component Services
Custom Context Attribute
Custom Message Sink
Providing Four Types of Sinks
Processing Messages
The Logbook Service
Using the Logbook
The Logbook Service Architecture
Implementing the Logbook
Conclusion

O ne of the most important aspects of the Microsoft® .NET Framework as a component technology is its use of contexts to facilitate component services. The core design pattern is interception: intercepting the call from the client to the object, performing some pre-call processing, forwarding the call to the object, and doing some post-call processing before returning control back to the client. Objects indicate to the Framework which component services they rely on, and using interception, the Framework ensures that they get the runtime environment they require.

In addition, the .NET Framework enables developers to provide their own custom component services. This is a major advancement for software engineering and component-oriented programming because now developers can fine-tune the way the .NET Framework supports their particular application and business logic. Custom component services decouple clients from objects because they don't need to coordinate the execution of the custom service and can instead focus on the business logic. Examples of custom services are: application logging and tracing, performance counters, custom thread management, method calls filtering, parameter checks, security access checks, and event subscriptions.

In this article I'll explain the interception architecture and how to build custom context attributes. Then I'll walk you through developing a real-life example of these techniques. The example will be a logbook services that logs method calls and exceptions.

Contexts in .NET

The .NET Framework provides component services through call interception. To intercept the call, a proxy is injected between the client and the object. In the same app domain, by default, the client gets a direct reference to an object; no proxies are involved. The common language runtime (CLR) can optionally subdivide an app domain into contexts. When making a cross-context call, there is always a proxy between the client and the object, even in the same app domain. If you want to take advantage of interception-based services, you need to derive your class from ContextBoundObject, as shown here:

public class SomeClass : ContextBoundObject {...}

As the name implies, a context-bound object always executes in the same context, and all calls into the context are intercepted.

Objects indicate which services they require by using special context attributes. Out of the box, the .NET Framework provides just one such attribute, SynchronizationAttribute, defined in the System.Runtime.Remoting.Contexts namespace:

using System.Runtime.Remoting.Contexts; [Synchronization] public class SomeClass: ContextBoundObject {...}

When a context-bound object uses the Synchronization attribute, the CLR ensures that only one thread at a time is allowed to access the object by associating the object with a lock. If the object is accessed by another thread, the calling thread is blocked until the concurrent call returns. The Synchronization attribute also provides ways for sharing the lock between objects in order to reduce the chances of a deadlock.

Call Interception Architecture

The cross-context interception architecture is very similar to the one used when performing remote calls across app domain boundaries. In the .NET Framework, the proxy has 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, it converts the stack frame to a message and passes the message to the real proxy. The message is an object implementing the IMessage interface, as shown here:

public interface IMessage { IDictionary Properties{ get; } }

The message comprises a collection of properties, such as a method's name and its arguments. In the case of a call across app domains, the real proxy needs to serialize the message using a formatter and pass it to the channel. In the case of a cross-context call, the real proxy needs to perform interception steps before forwarding the call to the object. It turns out that an elegant design solution allows the same real proxy to be used in both cases. The real proxy does not know about formatters, channels, or context interceptors. The real proxy passes the message to a message sink, which is an object that implements the IMessageSink interface that is defined in the System.Runtime.Remoting.Messaging namespace:

public interface IMessageSink { IMessageSink NextSink{ get; } IMessageCtrl AsyncProcessMessage(IMessage msg,IMessageSink replySink); IMessage SyncProcessMessage(IMessage msg); }

The common language runtime groups message sinks together in a linked list (see Figure 1). Each message sink knows about the next sink in the list and it passes the message to it when it has finished processing it. You can obtain the next sink through the NextSink property of IMessageSink.

Figure 1 Interception Architecture

Figure 1** Interception Architecture **

The last sink in the chain is called the stack builder sink because it converts the message back to a stack frame and then calls the method on the object. When the call returns to the stack builder, it constructs a message with the method results and returns that message to the sink that called it. That sink performs some post-call processing and returns the message to the calling sink, and so on. The first sink in the chain returns control to the real proxy, providing it with the returned message from the object. The real proxy returns the message to the transparent proxy, which puts it back on the calling client's stack.

To pass the message through the sinks, the real proxy calls the SyncProcessMessage method of the first sink, allowing it to process the message. After processing the message, the first sink calls SyncProcessMessage on the next sink, as do the rest of the sinks. With cross-app-domain calls, the first sink on the client side is the message formatter. When the SyncProcessMessage method returns to the proxy, it returns the message returned from the object. The IMessageSink interface also provides the AsyncProcessMessage method that is used to intercept asynchronous calls. Unfortunately, the discussion of the interception of asynchronous calls is beyond the scope of this article.

Cross-context Sinks

In the case of a cross-context call, there is no need for a formatter, and the CLR uses an internal channel called CrossContextChannel which is also a message sink. It is up to the sinks to compensate for differences in component services configurations between the client and the object. To be able to accomplish that, the .NET Framework installs all the neccessary message sinks between the client and the object.

Figure 2 shows a generic sink implementation. The sink's constructor accepts the next sink in the chain. When the SyncProcessMessage method is called, the sink performs some pre-call processing and then calls SyncProcessMessage on the next sink. The call will advance down the sink chain, call the object, and return. The generic sink now has a chance to examine the returned message and do some post-call processing before returning control to the sink that called it.

Figure 2 Generic Message Sink Implementation

public class GenericSink : IMessageSink { IMessageSink m_NextSink; public GenericSink(IMessageSink nextSink) { m_NextSink = nextSink; } public IMessageSink NextSink { get { return m_NextSink; } } public IMessage SyncProcessMessage(IMessage msg) { PreCallProcessing(msg); //This calls the object: IMessage returnedMessage = m_NextSink.SyncProcessMessage(msg); PostCalm"rocessing(returnedMessage); return returnedMessage; } void PreCallProcessing(IMessage msg) { /*Do some pre call processing*/ } void PostCallProcessing(IMessage msg) { /*Do some post call processing*/ } public IMessageCtrl AsyncProcessMessage(IMessage msg,IMessageSink replySink) { /*Handle the asynchronous call, then:*/ return m_NextSink.AsyncProcessMessage(msg,replySink); } }

Message Sink Types

The call interception can occur in two places. Server-side sinks can intercept calls coming into the context as well as do some pre- and post-call processing, such as managing a thread lock. Client-side sinks intercept calls going out of the context and do some pre- and post-call processing. For example, the Synchronization attribute can optionally track calls outside the context and unlock the lock to allow other threads access while an outgoing call is in progress. This is done using a client-side sink. Server-side sinks intercepting all calls into the context are referred to as server context sinks, while those intercepting calls to a particular object are server object sinks. The server is responsible for installing server-side sinks. Client-side sinks installed by the client are called client context sinks, and they affect all calls going out of the client's context. Client-side sinks installed by the object are called envoy sinks. An envoy sink intercepts calls only to the particular object with which it is associated. The last sink on the client side and the first sink on the server side are instances of the CrossContextChannel type. The resulting sink chain is comprised of segments, with each segment made of a different type of sink, as shown in Figure 3.

Figure 3 Client-side and Server-side Sink Chains

The .NET Framework installs a terminator at the end of each segment, which is a sink of the segment's type. The terminator is responsible for performing the final processing for the segment and forwarding the message to the next segment. For example, the last message sink in the server context sink segment is called the ServerContextTerminatorSink. The terminators behave like a true dead end: if you call IMessageSink.NextSink on a terminator, a null reference will be returned. The real next sink (the first in the next segment) is a private member of the terminator. As a result, there is no way to iterate using IMessageSink.NextSink on the entire length of the interception chain.

Custom Component Services

Custom component services are provided in the form of custom context attributes. Generic custom attributes have no use unless the developer provides the reflection code to look for these attributes, interpret their values, and act upon it. The .NET Framework is indifferent to such custom attributes.

Unlike generic custom attributes, the Framework is aware of custom context attributes when used on context-bound objects. They must derive from the ContextAttribute class. When creating a new context-bound object, the the object's metadata is reflected and placed in the appropriate context based on the behavior of the attributes. Custom context attributes can affect the context in which the object is activated and running in, as well as install all four types of message sink interceptors.

Custom Context Attribute

Each context has a set of properties associated with it. The properties are the component services that a context supports. A context-bound object will share a context with its client only if the client's context has the services that the component requires; in other words, the context has the required properties. If the client's context does not have at least one of the properties the object requires, the CLR will create a new context and put the object in it. In addition, a context property may require a new context irrespective of the client's context. Developers use context attributes to specify the required services. The context attributes are the ones deciding whether the client's context is sufficient.

To understand how context attributes affect a context's activation, consider one that adds a color property to a context. The color is an enum of type ColorOption:

public enum ColorOption{Red,Green,Blue};

You use ColorAttribute as a class attribute on a class derived from ContextBoundObject:

[Color(ColorOption.Blue)] public class SomeClass: ContextBoundObject {...}

The CLR will create objects of the SomeClass class in the client's context only if the creating client's context has a color property and if its value is set to ColorOption.Blue. Otherwise, a new context will be created, the attribute will be allowed to set its color property to ColorOption.Blue, and the new object will be created in the new context. The ColorAttribute also has a default constructor, setting the context color to ColorOption.Red:

[Color]//Default is ColorOption.Red public class SomeClass: ContextBoundObject {...}

Figure 4 shows the implementation of the custom context attribute ColorAttribute. ColorAttribute has a member called m_Color containing the required context color. The color is specified during the attribute construction either explicitly or by using the default constructor. As a custom context attribute, it must derive from the class ContextAttribute defined in the System.Run- time.Remoting.Contexts namespace. The single constructor of ContextAttribute requires a string naming the new context attribute. ContextAttribute derives and provides virtual implementation of the IContextAttribute interface, defined as:

public interface IContextAttribute { void GetPropertiesForNewContext(IConstructionCallMessage msg); bool IsContextOK(Context ctx,IConstructionCallMessage msg); }

Figure 4 Custom Context Attribute

using System.Runtime.Remoting.Contexts; using System.Runtime.Remoting.Activation; public enum ColorOption {Red,Green,Blue}; [AttributeUsage(AttributeTargets.Class)] public class ColorAttribute : ContextAttribute { ColorOption m_Color; public ColorAttribute():this(ColorOption.Red)//Default color is red {} public ColorAttribute(ColorOption color):base("ColorAttribute") { m_Color = color; } //Add a new color property to the new context public override void GetPropertiesForNewContext (IConstructionCallMessage ctor) { ColorProperty colorProperty = new ColorProperty(m_Color); ctor.ContextProperties.Add(colorProperty); } //ctx is the creating client's context public override bool IsContextOK(Context ctx,IConstructionCallMessage ctorMsg) { ColorProperty contextColorProperty = null; //Find out if the creating context has a color property. If not, //reject it contextColorProperty = ctx.GetProperty("Color") as ColorProperty; if(contextColorProperty == null) { return false; } //It does have a color property. Verify color match return (m_Color == contextColorProperty.Color); } } //The ColorProperty is added to the context properties collection by the //ColorAttribute class public class ColorProperty : IContextProperty { protected ColorOption m_Color; public ColorProperty(ColorOption ContextColor) { Color = ContextColor; } public string Name { get { return "Color"; } } //IsNewContextOK called by the runtime in the new context public bool IsNewContextOK(Context ctx) { ColorProperty newContextColorProperty = null; //Find out if the new context has a color property. If not, reject it newContextColorProperty = ctx.GetProperty("Color") as ColorProperty; if(newContextColorProperty == null) { return false; } //It does have color property. Verify color match return (this.Color == newContextColorProperty.Color); } public void Freeze(Context ctx) {} //Color needs to be public so that the attribute class can access it public ColorOption Color { get { return m_Color; } set { m_Color = value; } } }

The IsContextOK method lets the context attribute examine the creating client's context provided in the ctx parameter. Every context has an object of the Context type associated with it. The Context object provides easy access to the context properties. If the client's context is adequate, then no further action is required and the runtime activates the new object in the creating client's context. If the content attribute returns false from IsContextOK, then a new context is created and GetPropertiesForNewContext is called, allowing the context attribute to add new properties to the new context.

In fact, because there can be more than one context attribute on a single object, the .NET Framework can optimize its queries of the attributes when examining the client's context. It starts iterating over the attribute list, calling IsContextOK on each attribute in the process. As soon as it finds one attribute in the list that returns false, it aborts the iteration and creates a new context. It then calls GetPropertiesForNewContext on each context attribute, letting it add its properties to the new context. ColorAttribute needs to override both methods of IContextAttribute and manage its single context property. Context properties are objects implementing the IContextProperty interface:

public interface IContextProperty { string Name{ get; } void Freeze(Context newContext); bool IsNewContextOK(Context newCtx); }

Each context property is identified by name through the Name property of IContextProperty. ColorAttribute uses a helper class called ColorProperty to implement IContextProperty. ColorProperty names itself as "Color." ColorProperty also provides the Color public property of type ColorOption. This allows for type-safe checking of the color value.

In its implementation of IsContextOK, ColorAttribute checks whether the client's context has a property called "Color." If not, IsContextOK returns false. If the client's context has a color property, ColorAttribute verifies that there is a color match by comparing the value of the Color property with its own color.

The implementation of GetPropertiesForNewContext is straightforward as well. The single parameter is of type IConstructionCallMessage, providing a collection of properties for the new context through the ContextProperties property. ColorAttribute creates an object of the ColorProperty type, initializes it with the required color, and adds it to the collection of properties for the new context.

Because a single context-bound object can have multiple context attributes, it is possible that some of them conflict. To handle this, IsNewContextOK is called on each property after all the properties are added to the new context. If any one of the properties returns false, then the instantiation of the new object is aborted and an exception of the RemotingException type is thrown. In IsNewContextOK, ColorAttribute simply verifies that the new context has the correct color. The Freeze method lets a context property know that the final location of the context is established, and is available for advanced use only.

Figure 5 Custom Context Attribute Flow

Figure 5** Custom Context Attribute Flow **

Figure 5 is an activity diagram describing the activation flow when using a custom context attribute and a context property.

Custom Message Sink

To provide a useful component service, the custom context attribute must install at least one custom message sink. The message sink can be a server context sink, a server object sink, a client context sink, or an envoy sink. Commonly, a custom context attribute will install only a server context sink. The other sinks are intended for advanced scenarios. For each type of custom sink you want to contribute to the interception chain, the custom context property must implement a matching interface.

Providing Four Types of Sinks

To contribute a server context sink, the custom context property needs to implement the IContributeServerContextSink interface, which is defined as:

public interface IContributeServerContextSink { IMessageSink GetServerContextSink(IMessageSink nextSink); }

In its implementation of GetServerContextSink, the context property creates a sink object and concatenates it with the next sink in the chain provided as the method parameter. GetServerContextSink should return the new sink it created so that it will be added to the interception chain. For example, here is how to install the GenericSink presented in Figure 2 as a server context sink:

public IMessageSink GetServerContextSink(IMessageSink nextSink) { IMessageSink sink = new GenericSink(nextSink); return sink; }

The server context sink intercepts all calls coming into the context. GetServerContextSink is called after its call to IContextProperty.IsNewContextOK but before creating the object, allowing the context property to provide the sink. A server context sink can intercept construction calls.

To install a client context sink, the context property needs to implement the IContributeClientContextSink, defined as:

public interface IContributeClientContextSink { IMessageSink GetClientContextSink(IMessageSink nextSink); }

A client context sink affects the context-bound object only when it is the client of another object outside the context. A client context sink will intercept all calls exiting the context. GetClientContextSink is called only when the object makes its first call outside the context. The information in the message object passed to the sink will pertain to the target object, not the client.

If the context property implements the IContributeEnvoySink interface, shown in the following code snippet, then when a proxy is set up on the client's side to the object, the proxy will have the envoy sink as part of the interception chain leading to the particular object in question:

public interface IContributeEnvoySink { IMessageSink GetEnvoySink(MarshalByRefObject obj, IMessageSink nextSink); }

The envoy sink will intercept all calls going from the client to the object. Other objects accessed by the client are not affected. Every time a new client in a different context connects to the object, the runtime installs an envoy sink in the client's context. GetEnvoySink is called after the new object is created, but before returning control to the client. When using an envoy sink, it is not possible to intercept construction calls.

To install an object sink, the context property needs to implement the IContributeObjectSink interface, defined as:

public interface IContributeObjectSink { IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink nextSink); }

The envoy sink is installed on an per object basis, meaning it will only intercept calls to an object whose reference is provided in the GetObjectSink call. Other calls to the context are not affected. GetObjectSink is called before the first method call is forwarded. As a result of this, when using an object sink, it is not possible to intercept construction calls.

Processing Messages

The IMessage interface presented previously is a collection of information about the method being intercepted. Although you could retrieve that information from the dictionary, there is another way. When intercepting an incoming call, the different message objects (used for synchronous methods, asynchronous methods, and constructor calls) all support the IMethodMessage interface, as defined in Figure 6.

Figure 6 IMethodMessage Interface

public interface IMethodMessage : IMessage { int ArgCount{ get; } object[] Args{ get; } bool HasVarArgs{ get; } LogicalCallContext LogicalCallContext { get; } MethodBase MethodBase { get; } string MethodName{ get; } object MethodSignature{ get; } string TypeName{ get; } string Uri{ get; } object GetArg(int argNum); string GetArgName(int index); }

IMethodMessage provides information about the method name, its arguments, the type on which the method is called, and the object's location. You can use that information in your pre-call message processing logic. After the last sink (the stack builder) invokes the call on the object, it returns a different message object. Again, there are several types of returned method objects, but they are all polymorphic with the IMethodReturnMessage interface, defined as the following:

public interface IMethodReturnMessage : IMethodMessage { Exception Exception { get; } int OutArgCount { get; } object[] OutArgs { get; } object ReturnValue { get; } object GetOutArg(int argNum); string GetOutArgName(int index); }

IMethodReturnMessage derives from IMethodMessage and it provides additional information about the method returned value, the value of outgoing parameters, and exception information. The fact that exception information is captured is of particular interest. If an object throws an exception, the stack builder sink silently catches it, and saves it in the returned message object. This allows all the sinks up the call chain to examine the exception object. When control returns to the proxy, if exception information is present, the proxy rethrows it on the calling client's side.

The Logbook Service

It's time to put all the knowledge and intricacies I've described so far to use with a real-life example. One of the most beneficial steps you can take to achieve robustness in an application and to decrease time to market is to add a logbook capability to your application. The logbook is a simple custom component service that enables you to log method calls and exceptions. It intercepts incoming calls to your context-bound objects and logs most of the information in the message. As you will see shortly, you can use the same logbook to record method invocation from multiple machines and have the various entries interleaved in order.

Each logbook entry contains the following information, captured automatically by the logbook:

  • The location where the method was invoked, including the machine name, app domain name, thread ID and name, and the context ID
  • The caller's identity (user name)
  • Information about the target object: its assembly, its type, and the member being accessed (constructor, method, property, indexer, or event)
  • Invocation date and time
  • Error information if an exception was thrown; the type of the exception and the exception message

Using the Logbook

A key requirement in designing the logbook was that it should require no participation by the object. The object should focus on implementing its business logic, and the logbook should do the logging. To add logging support for a context-bound object, add the LogbookAttribute custom context attribute defined in the ContextLogger namespace:

using ContextLogger; [Logbook] public class SomeClass : ContextBoundObject {...}

The logbook service enables developers to choose what specific information to log. Sometimes, it's necessary to record everything that takes place: method calls and errors. In other situations, it is sufficient to log only errors and exceptions. To that end, the logbook provides the enum LogOption, defined as:

public enum LogOption { MethodCalls, Errors }

You can provide the constructor of LogbookAttribute with the appropriate enum value. For example, to log errors only, you would write:

[Logbook(LogOption.Errors)] public class SomeClass : ContextBoundObject {...}

The parameterless constructor of LogbookAttribute defaults to LogOption.MethodCalls so these two declarations are equivalent and will log both method calls and errors:

[Logbook] [Logbook(LogOption.MethodCalls)]

The Logbook Service Architecture

When you apply LogbookAttribute to a context-bound class, it will insist on having private contexts for each instance. This is required to support logging all calls coming into the object. If it were possible for two objects using the LogbookAttribute to share a context, then cross-context calls would have been logged, but the intra-context calls they made on each other would not have been logged. The LogbookAttribute adds a property called LogContextProperty to the new context, which itself adds a server context sink called LogSink. LogSink intercepts all calls to the object but does not log them itself. Instead, it uses the Logbook component. Logbook encapsulates the actual logging mechanism. The implementation provided here records the information into a SQL Server™ database, but you can replace that with a storage method or device of your choosing.

Also note that the Logbook is a remote singleton residing in an EXE host. As a result, all objects using the logbook service will actually log to the same repository, in order. This is a key feature, allowing you to trace the execution of a distributed application because the host can actually be on a dedicated machine that is used by all other machines.

Figure 7 Logbook Service Architecture

Figure 7** Logbook Service Architecture **

Figure 7 depicts the logbook architecture. In addition, a logbook viewer application is provided, displaying in a grid control the content of the logbook database entries table. The logbook viewer does not connect to the database directly, but instead connects to the singleton object Logbook. If the logbook viewer connected directly to the database, it would couple the viewer to the repository being used (a database, in this case). As a result, you would have to modify the viewer every time you switched repositories, for example if you wanted to store the logbook entries in a flat file instead of a database. The Logbook provides the necessary degree of separation between them.

The source files accompanying this article provide the Logbook solution (see the link at the top of this article). The solution has the following projects: the ContextLogger class library contains the LogbookAttribute, the LogContextProperty, the LogSink, and the Logbook component itself. Logbook is an ADO.NET component, accessing a SQL Server database. Run the included Logbook.sql script file to create a database called Logbook with the Entries table. The LogbookHost project is a simple Windows® Forms EXE which hosts Logbook. The LogbookHost configuration file exposes the Logbook type as a server-activated singleton object.

The TestClient project is a Windows Forms application which has a test class and the test client. The test class is a context-bound class that uses the LogbookAttribute. The test client is a form which is able to exercise various calls on the test object. The configuration file of the TestClient application registers the Logbook component as a remote server-activated object whose URL connects to the LogbookHost application. The LogbookViewer project contains the logbook viewer, letting you browse the logbook entries or clear the table (see Figure 8). The LogbookViewer application registers the Logbook component as a remote server as well.

Figure 8 Logbook Viewer

Implementing the Logbook

The LogbookAttribute class is not that much different from the ColorAttribute class shown in Figure 4. It refuses the client's context in its IsContextOK and installs the LogContextProperty in its GetPropertiesForNewContext method. Figure 9 shows the implementation of LogbookAttribute.

Figure 9 LogbookAttribute Class

[AttributeUsage(AttributeTargets.Class)] public class LogbookAttribute : ContextAttribute { LogOption m_LogOption; public LogbookAttribute(): this(LogOption.MethodCalls) {} public LogbookAttribute(LogOption logOption):base("LogbookAttribute") { m_LogOption = logOption; } // Add a new logbook property to the new context public override void GetPropertiesForNewContext(IConstructionCallMessage ctor) { LogContextProperty logProperty = new LogContextProperty(m_LogOption); ctor.ContextProperties.Add(logProperty); } //Called by the runtime in the creating client's context public override bool IsContextOK(Context ctx,IConstructionCallMessage ctorMsg) { return false; } } /*The LogContextProperty implements the IContributeServerContextSink interface, installing the LogSink server context sink:*/ public class LogContextProperty : IContextProperty,IContributeServerContextSink { protected LogOption m_LogOption; public IMessageSink GetServerContextSink(IMessageSink nextSink) { IMessageSink logSink = new LogSink(nextSink,m_LogOption); return logSink; } /* Rest of the implementation */ }

The interesting part of the logbook service is, of course, the LogSink class. In its implementation of IMessageSink, LogSink processes the message object, constructs an instance of the LogbookEntry structure, providing it with the information extracted from the message. LogbookEntry stores the information provided as construction parameters and captures additional information such as object location and execution scope (see Figure 10). LogSink then passes the LogbookEntry object to the Logbook component. Because it's accessed as a remote component, LogbookEntry is marshaled by value using the Serializable attribute.

Figure 10 LogbookEntry Structure

[Serializable] public struct LogbookEntry { public LogbookEntry(string assemblyName,string typeName, string methodName): this(assemblyName, methodName,typeName,"","") {} public LogbookEntry(string assemblyName,string typeName,string methodName, string exceptionName,string exceptionMessage) { AssemblyName = assemblyName; TypeName = typeName; MemberAccessed = methodName; ExceptionName = exceptionName; ExceptionMessage = exceptionMessage; MachineName = Environment.MachineName; AppDomainName = AppDomain.CurrentDomain.FriendlyName; ThreadID = Thread.CurrentThread.GetHashCode(); ThreadName = Thread.CurrentThread.Name; ContextID = Thread.CurrentContext.ContextID; Date = DateTime.Now.ToShortDateString(); Time = DateTime.Now.ToLongTimeString(); if(Thread.CurrentPrincipal.Identity.IsAuthenticated) { UserName = Thread.CurrentPrincipal.Identity.Name; } else { UserName = "Unauthenticated"; } } //Location public readonly string MachineName; public readonly string AppDomainName; public readonly int ThreadID; public readonly string ThreadName; public readonly int ContextID; //Identity public readonly string UserName; //Object info public readonly string AssemblyName; public readonly string TypeName; public readonly string MemberAccessed; public readonly string Date; public readonly string Time; //Exception public readonly string ExceptionName; public readonly string ExceptionMessage; }

Figure 11 contains most of the implementation of LogSink. The constructor saves the logging filer (methods or errors), as well as the next message sink in the chain, and creates a new Logbook object (a proxy to the remote singleton). In SyncProcessMessage, LogSink downcasts the message object to IMethodMessage and passes it to several helper methods. Then, it forwards the call to the next sink, eventually calling the object. When the call returns, LogSink downcasts the returned message to IMethodReturnMessage and uses other helper methods to get the exception information (if an exception took place). When the processing is finished, LogSink constructs a LogbookEntry object and adds it to the logbook using the Logbook object.

Figure 11 LogSink Class

public class LogSink : IMessageSink { IMessageSink m_NextSink; LogOption m_LogOption; Logbook m_Logbook; public LogSink(IMessageSink nextSink,LogOption logOption) { m_LogOption = logOption; m_NextSink = nextSink; m_Logbook = new Logbook(); } public IMessageSink NextSink { get {return m_NextSink;} } public IMessage SyncProcessMessage(IMessage msg) { IMethodMessage methodMessage = (IMethodMessage)msg; string assemblyName = GetAssemblyName(methodMessage); string typeName = GetTypeName(methodMessage); string methodName = GetMethodName(methodMessage); IMethodReturnMessage returnedMessage; returnedMessage = (IMethodReturnMessage)m_NextSink.SyncProcessMessage(msg); string exceptionName = GetExceptionName(returnedMessage); string exceptionMessage = GetExceptionMessage(returnedMessage); LogbookEntry logbookEntry = new LogbookEntry(assemblyName, typeName,methodName, exceptionName,exceptionMessage); DoLogging(logbookEntry); return returnedMessage; } public IMessageCtrl AsyncProcessMessage(IMessage msg,IMessageSink replySink) { /* Processing of the message, similar to SyncProcessMessage() */ } void DoLogging(LogbookEntry logbookEntry) { if(m_LogOption == LogOption.MethodCalls) { LogCall(logbookEntry); } if(m_LogOption == LogOption.Errors) { if(logbookEntry.ExceptionName != "") { LogCall(logbookEntry); } } } void LogCall(LogbookEntry logbookEntry) { m_Logbook.AddEntry(logbookEntry); } static string GetMethodName(IMethodMessage methodMessage) { /* Processes methodMessage.MethodName */ } static string GetTypeName(IMethodMessage methodMessage) { /* Processes methodMessage.TypeName */ } static string GetAssemblyName(IMethodMessage methodMessage) { /* Processes methodMessage.TypeName */ } static string GetExceptionName(IMethodReturnMessage returnedMessage) { /* Processes returnedMessage.Exception */ } static string GetExceptionMessage(IMethodReturnMessage returnedMessage) { /* Processes returnedMessage.Exception.Message */ } }

The Logbook component derives from the Component class defined in the System.ComponentModel namespace. Deriving from Component allows you to use Visual Studio .NET to generate much of the ADO.NET data connectivity classes and code. However, the Logbook must be available for remoting. Fortunately, Component derives from MarshalByRefObject. The Logbook component overrides InitializeLifetimeService and provides a null lease. This is required to maintain the singleton semantics so that the object will in fact live indefinitely:

public class Logbook : Component { //Logbook should be used as a singleton public override object InitializeLifetimeService() { return null; } /* Rest of the implementation */ }

The Logbook component uses ADO.NET to connect to the Logbook database and store or retrieve the entries using a DataSet. It's is a thread-safe component that locks itself in every method call to synchronize concurrent access.

Conclusion

Interception-based services are not new to component developers, and previous component technologies from Microsoft (notably COM and COM+) used interception to provide for component services. With the .NET Framework, for the first time developers can define custom component services, which are as useful, productive, and powerful as any service provided by Windows operating systems. It is possible that Microsoft will provide additional services for context-bound objects in future releases of the Framework. Even though context attributes and custom message sinks are presently undocumented features of the .NET Framework, I believe that using them is a risk worth taking in order to unleash the power of custom component services.

For related articles see:
House of COM: Is COM Dead?
AOP: Aspect-Oriented Programming Enables Better Code Encapsulation and Reuse

For background information see:
Programming .NET Components by Juval Lowy (O'Reilly, March 2003)

Juval Lowyis a software architect providing consultation and training on .NET design and .NET migration. He is also the Microsoft Regional Director for the Silicon Valley. This article is adapted from his latest book, Programming .NET Components (O'Reilly, March 2003). Contact Juval at http://www.idesign.net.