Understanding Contexts

Understanding Contexts

In Chapter 2, we introduced the concept of context. Now it s time to look at the role of contexts within the .NET Remoting infrastructure. A better understanding of context can help you design more efficient .NET Remoting applications. In this section, we ll look at how you can customize features of the context architecture to prevent the client from making a remote method call if it passes invalid parameters to a method, thus saving a round-trip to the remote object. We ll also look at how you can trace messages and log exceptions thrown across context boundaries.

Establishing a Context

The context architecture employed by .NET Remoting consists of context-bound objects, context attributes, context properties, and message sinks. Any type derived from System.ContextBoundObject is a context-bound type and indicates to the .NET Remoting infrastructure that it requires a special environment, or context, for its instances to execute within. During activation of a context-bound type, the runtime will perform the following sequence of actions:

  1. Invoke the IContextAttribute.IsContextOK method on each attribute for the type.

  2. If any attribute indicated that the context wasn t OK for activation, invoke the IContextAttribute.GetPropertiesForNewContext method on each attribute, passing the constructor call message for the type being activated.

  3. Construct the context.

  4. Notify each context property that the context is frozen by invoking the IContextProperty.Freeze method on each context property.

  5. Ask each context property whether the new context is OK by invoking the IContextProperty.IsNewContextOK method.

  6. Activate the object in the new context, passing the constructor call message to the proxy object s RealProxy.Invoke method.

Context Attributes and Properties

You define and establish a context by attributing the context-bound type with one or more attributes that implement the IContextAttribute interface. Table 6-2 shows the methods that IContextAttribute defines.

Table 6-2. Members of System.Runtime.Remoting.Contexts.IContextAttribute

Member

Description

IsContextOK

The runtime calls this method to determine whether the current context is OK for activation of the attributed type.

GetPropertiesForNewContext

The runtime calls this method after an attribute has indicated that the current context isn t OK for activation of the attributed type.

As an alternative, you can derive a type from System.ContextAttribute, which derives from System.Attribute and provides a default implementation of IContextAttribute. System.ContextAttribute also implements IContextProperty, which we ll discuss shortly.

A context attribute participates in the activation process and performs the following functions:

  • Indicates to the .NET Remoting infrastructure whether the current context meets the execution requirements of the attributed type.

  • Contributes to the context one or more properties that provide services and/or enforce execution requirements of the attributed type.

During activation of a type derived from ContextBoundObject, the run time invokes the IContextAttribute.IsContextOK method on each of the type s context attributes to determine whether the current context supports the execution requirements of that type. An attribute indicates that the current context is unacceptable by returning false from the IsContextOK method.

If any attribute on a type derived from ContextBoundObject returns false from IContextAttribute.IsContextOK, the runtime will create a new context for the object instance and call the IContextAttribute.GetPropertiesForNewContext method on each attribute. This allows the attribute to contribute one or more context properties that enforce the type s execution requirements. Context properties implement IContextProperty. Table 6-3 shows the members of IContextProperty.

Table 6-3. Members of System.Runtime.Remoting.Contexts.IContextProperty

Member

Member Type

Description

Name

Read-only property

The name of the property. Used as a key in the property collection for the context.

Freeze

Method

The .NET Remoting infrastructure calls this method after allowing all attributes to add properties to the context. After freezing the context, the .NET Remoting infrastructure disallows the addition of more context properties.

IsNewContextOK

Method

The runtime calls this method after freezing the context to give each context property a chance to abort creation of the context.

For example, suppose we want to create a context that provides a common logging facility to all its objects. The idea is that any code executing within the context could obtain a logging object from the context properties and call a method that logs a message. To implement a solution for the logging context, we need to implement a context attribute that contributes a context property providing the logging service. The following code defines a context attribute named ContextLogAttribute:

using System.Runtime.Remoting.Contexts; [AttributeUsage(AttributeTargets.Class)] class ContextLogAttribute : Attribute, IContextAttribute { public bool IsContextOK( System.Runtime.Remoting.Contexts.Context ctx, System.Runtime.Remoting.Activation.IConstructionCallMessage msg) { // Force new context. return false; } public void GetPropertiesForNewContext( System.Runtime.Remoting.Activation.IConstructionCallMessage msg) { // Add our property to the context. msg.ContextProperties.Add(new ContextLogProperty()); } }

During activation of a type derived from ContextBoundObject that s attributed with ContextLogAttribute, the .NET Remoting infrastructure calls GetPropertiesForNewContext, passing it the IConstructionCallMessage. At this point, the object hasn t yet been instantiated. Thus, the object s constructor method hasn t yet been called. GetPropertiesForNewContext adds a new instance of the ContextLogProperty class defined in the following code snippet to the ContextProperties member of the IConstructionCallMessage:

[AttributeUsage(AttributeTargets.Class)] class ContextLogProperty : Attribute, IContextProperty { public void Freeze( System.Runtime.Remoting.Contexts.Context newContext) { // At this point, no more properties will be added. } public bool IsNewContextOK( System.Runtime.Remoting.Contexts.Context newCtx) { // We could also inspect the other properties // for the context to make sure none conflict, // but for this example, we just report A-OK. return true; } public string Name { get { return "ContextLogProperty"; } } public void LogMessage(string msg) { Console.WriteLine(msg); } }

The ContextLogProperty class implements the logging functionality. Because it s a context property, any object instances within the context can obtain an instance of the property and use it to log messages:

[ContextLogAttribute()] class MyContextBoundObject : System.ContextBoundObject { public void Foo() { Context ctx = Thread.CurrentContext; ContextLogProperty lg = (ContextLogProperty)ctx.GetProperty("ContextLogProperty"); lg.LogMessage("In MyContextBoundObject.Foo()"); } }

Contexts and Remoting

Recall that the context forms a .NET Remoting boundary around object instances within it. Figure 6-1 illustrates how the .NET Remoting infrastructure isolates an object instance within a context from object instances outside the context by using a special type of channel known as a cross-context channel and four chains of message sinks that separate inbound message processing from outbound message processing.

figure 6-1 chains of message sinks isolate a contextboundobject instance from objects outside the context.

Figure 6-1. Chains of message sinks isolate a ContextBoundObject instance from objects outside the context.

All method calls entering the context enter as .NET Remoting messages, which flow through the cross-context channel, a contextwide server context sink chain, an object-specific server object sink chain, and the stackbuilder sink. All method calls made to objects outside the context exit as .NET Remoting messages, which flow through a proxy and its associated envoy sink chain, a contextwide client context sink chain, and the channel (either cross context or application domain). Each sink chain consists of zero or more custom message sinks followed by a special terminator message sink that the .NET Remoting infrastructure defines.

As shown in Table 6-4, the message sink chain to which you add your message sink largely depends on when and where you want to enforce the context behavior for method call messages entering and exiting the context.

Table 6-4. Context Sink Chains

Sink Chain

Intercepts

Possible Usage

Client context

Method calls made by any object within the context on objects located outside the context.

  • Logging all method call messages exiting the context

  • Synchronization

  • Security

  • Transactions

Server context

Method calls made on any objects within the context from objects located outside the context.

  • Logging all method call messages entering the context

  • Synchronization

  • Security

  • Transactions

Server object

Method calls made on a specific context-bound object.

  • Interception of method call methods specific to an object instance

  • Might operate in conjuction with envoy sink to convey information from client context to server context

Envoy

Calls made by a client of the context-bound object. The envoy sink chain executes in the context of the client.

  • Validating method arguments prior to sending the method call message from the client to the server

  • Other method optimizations that are better performed in the context of the client

Before we discuss context sink chains in detail, let s examine the other kind of sink that s applicable to context but doesn t participate in sink chains: the dynamic context sink.

Dynamic Context Sinks

The .NET Remoting infrastructure supports the ability to programmatically register object instances of types implementing the IDynamicMessageSink into the context s message processing architecture at run time. Dynamic sinks aren t true message sinks because they don t implement IMessageSink and they aren t linked into chains. Dynamic sinks allow you to intercept method call messages at various points during message processing.

Creating a Custom Dynamic Sink

To create a custom dynamic context sink, you need to perform the following tasks:

  • Define a dynamic message sink class that implements the IDynamicMessageSink interface.

  • Define a dynamic property class that implements the IDynamic Property and the IContributeDynamicSink interfaces.

  • In the implementation of the IContributeDynamicSink.GetDynamicSink method, return an instance of the dynamic message sink class.

  • Programmatically register and unregister the dynamic property by using the Context.RegisterDynamicProperty and Context.UnregisterDynamicProperty methods, respectively.

Table 6-5 shows how the parameters to the Context.RegisterDynamic Property method dictate interception behavior. For each registered dynamic sink at a given interception level, the runtime calls the sink s IDynamicMessage Sink.ProcessMessageStart method prior to processing a method call, passing it the IMessage representing the method call. Likewise, the .NET Remoting infrastructure calls the sink s IDynamicMessageSink.ProcessMessageFinish after a method call has returned, passing it the IMessage representing the response message of the method call.

Table 6-5. Context.RegisterDynamicProperty Interception Behavior

ContextBoundObject Parameter

Context Parameter

Interception Level

Reference to a proxy

null

Intercepts all method calls on the proxy

Reference to a real object

null

Intercepts all method calls on the real object

null

Reference to a context

Intercepts all method calls entering and leaving the context

null

null

Intercepts all method calls entering and leaving all contexts in the application domain

Client Context Sink Chain

As Figure 6-1 shows, the client context sink chain is the last sink chain through which messages flow on their way out of the context. However, the figure doesn t show that this chain is contextwide and that all outgoing method calls made by all objects within the context travel through this chain. You can implement behavior that applies to all outgoing method calls made by all object instances within the context by implementing a client context sink.

The .NET Remoting infrastructure inserts custom client context sinks at the front of the chain so that the last sink in the chain is the System.Runtime.Remoting.Contexts.ClientContextTerminatorSink. An application domain contains a single instance of this class that performs the following functions for all contexts within the application domain:

  1. Notifies dynamic sinks that a method call leaving the context is starting.

  2. Passes IConstructionCallMessage messages to any context properties implementing IContextActivatorProperty before calling the Activa tor.Activate method in the message to activate an instance of a remote object, and passes the IConstructionReturnMessage to any context properties implementing IContextActivatorProperty after Activator.Activate returns. Allows properties to modify the constructor call message and, on return, the constructor return message.

  3. Passes non-IConstructorCallMessage messages onto either the CrossContextChannel or a suitable channel leaving the application domain, such as HttpChannel or TcpChannel.

  4. Notifies any dynamic sinks that a method call leaving the context has finished. (For asynchronous calls, the AsyncReplySink handles this step when the call actually completes.)

Creating a Custom Client Context Sink

To create a custom client context sink, you need to perform the following tasks:

  • Implement a message sink that performs the context-specific logic for all method calls leaving the context.

  • Define a context property that implements the IContributeClientContextSink interface.

  • Define a context attribute that contributes the context property defined in the previous step to the context properties in the construction call message during the call to IContextAttribute.GetPropertiesFor NewContext.

  • Apply the context attribute to the class declaration.

Server Context Sink Chain

The counterpart to the client context sink chain is the server context sink chain. As Figure 6-1 shows, the server context sink chain is the first sink chain through which messages flow on their way into the context. Like the client context sink chain, this chain is contextwide, and incoming method calls made by all objects outside the context travel through this chain. You can implement behavior that applies to all incoming method calls made by objects outside the context by implementing a server context sink.

The .NET Remoting infrastructure builds the server context sink chain by enumerating over the context properties in reverse order relative to the building of the client context sink chain. This is allows and preserves a symmetry of the order of operations performed during message sink processing between the two chains in the event that a property contributes a sink to both chains. The .NET Remoting infrastructure inserts custom server context sinks at the front of the chain so that the last sink in the chain is the System.Runtime.Remoting.Contexts.Server ContextTerminatorSink. An application domain will contain a single instance of this class that performs the following functions for all contexts within the application domain:

  • Passes IConstructionCallMessage messages to any context properties implementing IContextActivatorProperty before calling the Activator.Activate method in the message to activate an instance of the server object, and passes the IConstructionReturnMessage to any context properties implementing IContextActivatorProperty after Activator.Activate returns. Allows properties to modify the constructor call message and, on return, the constructor return message.

  • Passes non-IConstructionCallMessage messages to the server object sink chain for the target server object specified in the message.

Creating a Custom Server Context Sink

To create a custom server context sink, you need to perform the following tasks:

  • Implement a message sink that performs the application-specific logic that you want to occur for all method calls coming into the context.

  • Define a context property that implements the IContributeServer ContextSink interface.

  • Define a context attribute that contributes the context property defined in the previous step to the context properties in the construction call message during the call to IContextAttribute.GetPropertiesForNewContext.

  • Apply the context attribute to the class declaration.

Example: Exception Logging Context

To demonstrate the use of both the server context sink chain and the client context sink chain, let s define an exception logging context that performs the following tasks:

  • Logs all exceptions thrown as a result of method calls made on objects within the context by objects outside the context.

  • Logs all exceptions thrown as a result of method calls made by objects within the context on objects outside the context.

Let s first define a message sink class that inspects all messages it processes to determine whether the message contains an exception:

public class ExceptionLoggingMessageSink : IMessageSink { IMessageSink _NextSink; static FileStream _stream; public IMessageSink NextSink { get { return _NextSink; } } public ExceptionLoggingMessageSink( IMessageSink next, string filename ) { Trace.WriteLine("ExceptionLoggingMessageSink ctor"); _NextSink = next; try { lock(this) { if ( _stream == null ) { _stream = new FileStream( filename, FileMode.Append ); } } } catch( System.IO.IOException e ) { Trace.WriteLine(e.Message); } }

As you can see, we ve started defining a class named ExceptionLoggingMessageSink. The constructor allows chaining an instance into an existing sink chain by accepting the next sink in the chain. The constructor also accepts the name of the file the message sink uses to open a FileStream when logging exceptions.

Next we need to implement the IMessageSink.SyncProcessMessage method, which will pass the message to the next sink in the chain and utilize the services of a helper function to inspect the return message:

public IMessage SyncProcessMessage(IMessage msg) { try { // Pass to next sink. IMessage retmsg = _NextSink.SyncProcessMessage(msg); // Inspect return message and log an exception if needed. InspectReturnMessageAndLogException(retmsg); return retmsg; } catch(System.Exception e) { return null; } } void InspectReturnMessageAndLogException(IMessage retmsg) { MethodReturnMessageWrapper mrm = new MethodReturnMessageWrapper((IMethodReturnMessage)retmsg); if ( mrm.Exception != null ) { lock(_stream) { Exception e = mrm.Exception; StreamWriter w = new StreamWriter(_stream, Encoding.ASCII); w.WriteLine(); w.WriteLine("========================"); w.WriteLine(); w.WriteLine(String.Format("Exception: {0}", DateTime.Now.ToString()) ); w.WriteLine(String.Format("Application Name: {0}", e.Source)); w.WriteLine(String.Format("Method Name: {0}", e.TargetSite.ToString())); w.WriteLine(String.Format("Description: {0}", e.Message)); w.WriteLine(String.Format("More Info: {0}", e.ToString())); w.Flush(); } } }

The InspectReturnMessageAndLogException method wraps the message in a System.Runtime.Remoting.Messaging.MethodReturnMessageWrapper instance, which maps properties in the return message to properties defined by the MethodReturnMessageWrapper class to facilitate coding. The code checks the Exception property of the message, which won t be null if the message contains an exception. Because we re using this sink in a contextwide sink chain and multiple threads might be executing this code concurrently, we lock the stream before writing to it.

Next we need to implement the IMessageSink.AsyncProcessMessage method:

 public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink ) { try { // // Set up our reply sink with a delegate // to our callback method. AsyncReplyHelperSink.AsyncReplyHelperSinkDelegate rsd = new SyncReplyHelperSink.AsyncReplyHelperSinkDelegate( this.AsyncProcessReplyMessage); // We want to trace the response when we get it, so add // a sink to the reply sink. replySink = (IMessageSink) new AsyncReplyHelperSink( replySink, rsd ); return _NextSink.AsyncProcessMessage( msg, replySink ); } catch(System.Exception e) { return null; } } // // Trace the reply message and return it. public IMessage AsyncProcessReplyMessage( IMessage msg ) { // Inspect reply message and log an exception if needed. InspectReturnMessageAndLogException(msg); return msg; } } // End class ExceptionLoggingMessageSink

The AsyncProcessMessage makes use of the AsyncReplyHelperSink that we defined earlier for handling asynchronous message processing. By instantiating an instance of AsyncReplyHelperSink with a delegate targeting the ExceptionLoggingMessageSink.AsyncProcessReplyMessage method and adding the helper sink to the reply sink chain, we can inspect the response message for asynchronous calls.

Now that we ve developed a sink, we need to plug instances of it into the client context sink chain and the server context sink chain. We do this by creating a context property that implements the IContributeClientContextSink and IContributeServerContextSink interfaces:

[Serializable] public class ExceptionLoggingProperty : IContextProperty, IContributeClientContextSink, IContributeServerContextSink { private string _Name; IMessageSink _ServerSink; IMessageSink _ClientSink; string _FileName; public ExceptionLoggingProperty(string name, string FileName) { _Name = name; _FileName = filename; } public void Freeze ( System.Runtime.Remoting.Contexts.Context newContext ) { // When this is called, we can't add any more properties // to the context. } public System.Boolean IsNewContextOK (Context newCtx ) { return true; } public string Name { get { return _Name; } } public IMessageSink GetClientContextSink (IMessageSink nextSink ) { Console.WriteLine("GetClientContextSink()"); lock(this) { if ( _ClientSink == null ) { _ClientSink = new ExceptionLoggingMessageSink(nextSink, _FileName); } } return _ClientSink; } public IMessageSink GetServerContextSink (IMessageSink nextSink ) { Console.WriteLine("GetServerContextSink()"); lock(this) { if ( _ServerSink == null ) { _ServerSink = new ExceptionLoggingMessageSink(nextSink, _FileName); } } return _ServerSink; } } // End class ExceptionLoggingProperty

The ExceptionLoggingProperty class implements the IContextProperty functionality that we saw in the beginning of this section. The interesting methods are IContributeServerContextSink.GetServerContextSink and IContributeClientContextSink.GetClientContextSink, each of which instantiate and return an instance of the ExceptionLoggingMessageSink we defined earlier. The runtime invokes the GetServerContextSink method during activation of an object instance within the context. Likewise, the runtime invokes the GetClientContextSink method upon the first occurrence of a method call out of the context.

WARNING
Each method returns a different instance of the ExceptionLoggingMessageSink. If you try to use the same sink instance in both the client context sink chain and the server context sink chain, you ll end up with a problem if a method call into the context also ends up calling out of the context. The problem occurs because the ExceptionLoggingMessageSink instance in the server context sink chain is also placed into the client context sink chain, creating a closed loop in the chain that prevents the message from leaving the context, resulting in an infinite recursion. Therefore, you need to design your sinks so that separate instances of the sink exist in each chain, as we ve done here.

NOTE
The client instantiates any context properties for a type in the context of the client during activation, prior to transmitting the constructor message to the remote object. This means that you should avoid placing code in context property constructors that depends on executing in the context of the remote object.

In our example, we could have created the FileStream in the ExceptionLoggingProperty and then passed the stream to the message sink constructor. This would be problematic in a remote application scenario in which the client and server objects exist in separate applications. In that case, the client application would have ownership of the exception log file because the ExceptionLoggingProperty would have opened a stream on that file. However, the message sinks would exist in the server application and might not be able to access it in a remote application.

Now we need to define a context attribute that adds the ExceptionLoggingProperty to the context:

[AttributeUsage(AttributeTargets.Class)] public class ExceptionLoggingContextAttribute : ContextAttribute { string _FileName; public ExceptionLoggingContextAttribute(string filepath) : base("ExceptionLoggingContextAttribute") { _FileName = filepath; } public override void GetPropertiesForNewContext ( IConstructionCallMessage msg ) { // Add our property to the context properties. msg.ContextProperties.Add( new ExceptionLoggingProperty( this.AttributeName, _FileName)); } public override System.Boolean IsContextOK ( Context ctx, IConstructionCallMessage msg ) { // Does the context already have // an instance of this property? return (ctx.GetProperty( this.AttributeName ) != null); } } // End class ExceptionLoggingContextAttribute

The ExceptionLoggingContextAttribute class follows the general pattern for context attributes by deriving from ContextAttribute and overriding the IsContextOK and GetPropertiesForNewContext methods, which check for the existence of and add an instance of the ExceptionLoggingContextProperty, respectively.

Now we can attribute any class derived from ContextBoundObject by using the ExceptionLoggingContextAttribute like this:

[ ExceptionLoggingContextAttribute(@"C:\exceptions.log") ] public class SomeCBO : ContextBoundObject {   }

If any method calls made by object instances outside the context on an instance of SomeCBO throw an exception, or any calls made by an instance of SomeCBO on objects outside the context throw an exception, the Exception LoggingMessageSink will log the exception information to the C:\exceptions.log file. Figure 6-2 shows an example of what the log might look like.

figure 6-2 output produced by the exceptionloggingmessagesink class

Figure 6-2. Output produced by the ExceptionLoggingMessageSink class

Server Object Sink Chain

As we mentioned earlier, both the server context sink chain and the client context sink chain intercept messages on a contextwide basis. If you want to implement context logic on a per-object-instance basis, you ll need to install a message sink into the server object sink chain, which allows you to intercept method call messages representing calls made on a context-bound object instance from objects outside the context. There s no corresponding per-object sink chain for intercepting calls made by an object instance within the context to objects outside the context.

The .NET Remoting infrastructure inserts custom server object sinks at the front of the chain so that the last sink in the chain is the System.Runtime.Remoting.Contexts.ServerObjectTerminatorSink. An application domain will contain a single instance of this class that performs the following functions for all contexts within the application domain:

  • Notifies any dynamic sinks associated with the object instance that a method call on the object is starting.

  • Passes the message to the StackBuilderSink, which ultimately invokes the method on the object instance.

  • Notifies any dynamic sinks that a method call on the object instance has finished. (For asynchronous calls, the AsyncReplySink handles this step when the call actually completes.)

Creating a Custom Server Object Sink

To create a custom server object sink, you need to perform the following tasks:

  • Implement a message sink that performs the context-specific logic for all method calls made on an object instance within the context.

  • Define a context property that implements the IContributeObjectSink interface.

  • Define a context attribute that contributes the context property defined in the previous step to the context properties in the construction call message during the call to IContextAttribute.GetPropertiesForNewContext.

  • Apply the context attribute to the class declaration.

Example: Tracing All Method Calls Made on an Object

Earlier in the section, we developed a context property that exposed method call message logging functionality to all object instances within a context containing that property. In that example, an object instance could get the property from the context and call its LogMessage method to log a message. One drawback to that approach is that it requires the addition of extra logging functionality to every method of the class. In contrast to the context property, the interception capability of context sinks affords you the ability to provide the method call logging functionality as a tracing service that applies to all method calls on an object instance without requiring each method implementation to explicitly use the tracing functionality. You can use context to provide method call tracing functionality more conveniently by creating a message sink that performs the tracing and adding the message sink to the server object sink chain.

First, we need to implement a message sink that performs the message tracing functionality. The following code listing defines a class that provides diagnostic output for all messages that it receives:

public class TraceMessageSink : IMessageSink { IMessageSink _NextSink; public IMessageSink NextSink { get { return _NextSink; } } public TraceMessageSink( IMessageSink next ) { _NextSink = next; } public virtual IMessage SyncProcessMessage ( IMessage msg ) { try { TraceMessage(msg); IMessage msgRet = _NextSink.SyncProcessMessage(msg); TraceMessage(msgRet); return msgRet; } catch(System.Exception e) { return new ReturnMessage(e, (IMethodCallMessage) msg); } }

We ve started defining a class named TraceMessageSink. The SyncProcessMessage method uses the following helper function to actually trace the message:

 void TraceMessage(IMessage msg) { Trace.WriteLine ("-------------------------------------------------------"); Trace.WriteLine( String.Format(  "{0} :: TraceMessage() --- {1}", DateTime.Now.Ticks.ToString(), msg.GetType().ToString()) ); Trace.WriteLine( String.Format("\tDomainID={0}, ContextID={1}", Thread.GetDomainID(), Thread.CurrentContext.ContextID.ToString())); IDictionaryEnumerator ie = msg.Properties.GetEnumerator(); while(ie.MoveNext()) { Trace.WriteLine( String.Format("\tMsg[{0}] = {1}", ie.Key, ie.Value)); if ( ie.Value == null ) { continue; } // Write out array elements if appropriate. if ( ! ie.Value.GetType().IsArray ) { continue; } object[] ar = (object[])ie.Value; for(int i = 0; i<ar.Length; ++i ) { Trace.WriteLine(.String.Format("\t\t[{0}] = {1}", i, ar.GetValue(i))); } } }

The TraceMessage method simply enumerates over the message properties, outputting each key-value pair by using Trace.WriteLine, which writes the output to the Output window of the debugger.

The following code completes the TraceMessageSink definition by defining the AsyncProcessMessage method:

 public virtual IMessageCtrl AsyncProcessMessage ( IMessage msg, IMessageSink replySink ) { try { // Trace the message before we send it down the chain. TraceMessage(msg); // We want to trace the response when we get it, // so add a sink to the reply sink helper. AsyncReplyHelperSink.AsyncReplyHelperSinkDelegate rsd = new AsyncReplyHelperSink.AsyncReplyHelperSinkDelegate (this.AsyncProcessReplyMessage); replySink = (IMessageSink)new AsyncReplyHelperSink( replySink, rsd ); // Pass to next sink. return _NextSink.AsyncProcessMessage( msg, replySink ); } catch(System.Exception e) { return null; } } public IMessage AsyncProcessReplyMessage( IMessage msg ) { // msg is the reply message; go ahead and trace it. TraceMessage(msg); return msg; } } // End class TraceMessageSink

The AsyncProcessMessage method traces the message before passing it to the next sink in the chain. Again, we use the AsyncReplyHelperSink defined earlier for handling asynchronous message processing. This time, we instantiate an instance of AsyncReplyHelperSink with a delegate that targets the TraceMessage Sink.AsyncProcessReplyMessage method so that we can intercept the response message for asynchronous calls.

Now that we have a sink, we need to plug it into the server object sink chain. You do this by creating a context property that implements the IContribute ObjectSink interface. Because the code implementing IContextProperty is largely boilerplate code, we ll show just the IContributeObjectSink implementation:

[Serializable] public class TraceMessageSinkProperty : IContextProperty, IContributeObjectSink { // IContextProperty implementation code removed for brevity. public IMessageSink GetObjectSink ( MarshalByRefObject obj, IMessageSink nextSink ) { return new TraceMessageSink(nextSink); } }

Nothing radical here: we just return an instance of TraceMessageSink from the GetObjectSink method. It s worth noting that one of the parameters to Get ObjectSink is a MarshalByRefObject. The .NET Remoting infrastructure associates the message sink chain returned by GetObjectSink with the object referenced by the obj parameter. Although we don t utilize the obj parameter here, you might use it for some other informational purpose.

All we need now is an attribute that adds the TraceMessageSinkProperty to the constructor call message s context properties in the GetPropertiesForNewContext method:

[AttributeUsage(AttributeTargets.Class)] public class TraceMessageSinkAttribute : ContextAttribute { public TraceMessageSinkAttribute() : base("TraceMessageSinkAttribute") { } public override void GetPropertiesForNewContext( IConstructionCallMessage msg ) { msg.ContextProperties.Add( new TraceMessageSinkProperty(this.AttributeName)); } public override System.Boolean IsContextOK ( Context ctx , IConstructionCallMessage msg ) { return (ctx.GetProperty(this.AttributeName) != null); } }

Now we can attribute any class derived from ContextBoundObject by using the TraceMessageSinkAttribute like this:

[TraceMessageSinkAttribute()] public class C : ContextBoundObject { void Foo(){ ... } }

The TraceMessageSink class will intercept all method calls made by object instances outside the context to any object instances of the C class. Here s an example of possible trace output resulting from an invocation of the C.Foo method:

------------------------------------------------------- 631580307740445920 :: TraceMessage() --- System.Runtime.Remoting.Messaging.MethodCall DomainID=1, ContextID=1 Msg[__Uri] = /fd062ced_1cc4_423b_949c_75acee5498bc/23509144_1.rem Msg[__MethodName] = Foo Msg[__MethodSignature] = System.Type[] Msg[__TypeName] = clr:CAO.C, Sinks Msg[__Args] = System.Object[] Msg[__CallContext] = System.Runtime.Remoting.Messaging.LogicalCallContext Msg[__ActivationTypeName] = CAO.C, Sinks, Version=1.0.876.29571, Culture=neutral, PublicKeyToken=null

Envoy Sink Chain

Each of the context sinks we ve discussed intercepts method call messages in the context of the server object instance. The envoy sink chain differs from the other context sink chains in that it executes in the context of the client object instance that s making method calls on the remote object. Figure 6-3 illustrates the relationship between a client object instance, proxy, and envoy sink chain in one application domain and a remote object instance.

figure 6-3 the envoy sink chain executes in the client context and intercepts method calls bound for the remote object instance.

Figure 6-3. The envoy sink chain executes in the client context and intercepts method calls bound for the remote object instance.

The .NET Remoting infrastructure builds the envoy sink chain by enumerating over the context properties in reverse order relative to the building of the server object sink chain. Again, this enumeration allows and preserves a symmetry of the order of operations performed during message sink processing between the two chains in the event that a property contributes a sink to both chains. The .NET Remoting infrastructure inserts custom envoy sinks at the front of the chain so that the last sink in the chain is the System.Runtime.Remoting.Contexts.EnvoyTerminatorSink. One envoy sink chain exists for each proxy to a remote object instance and contains at a minimum the application domain wide EnvoyTerminatorSink message sink instance.

After receiving the method call message from the transparent proxy, the real proxy (or a custom RealProxy derivative) passes the message on to the envoy sink chain. In Chapter 5, Messages and Proxies, we used the RemotingServices.GetEnvoyChainForProxy method to obtain a reference to the first sink in this chain.

Because envoy sinks execute in the context of the client, they re the perfect sinks to use for validating method call arguments or performing other client-side optimization of the method call. By validating the method arguments on the client side, you can prevent method call messages from leaving the context, thus saving a round-trip to the server when you know the method call will ultimately result in an error because of invalid arguments.

As with the other terminator sinks, an application domain will contain a single instance of the EnvoyTerminatorSink class that performs the following functions for all contexts within the application domain:

  • Validates the IMessage reference passed to SyncProcessMessage and AsyncProcessMessage, ensuring that it isn t null.

  • Forwards a valid message to the client context sink chain.

Creating a Custom Envoy Sink

To create a custom envoy sink, you need to perform the following tasks:

  • Implement a message sink that performs the application-specific logic that you want to occur in the client context whenever the client makes a method call on the server object.

  • Define a context property that implements the IContributeEnvoySink interface.

  • Define a context attribute that contributes the context property defined in the previous step to the context properties in the construction call message during the call to IContextAttribute.GetPropertiesForNewContext.

  • Apply the context attribute to the class declaration.

CAUTION
The ObjRef.EnvoyInfo property carries the envoy sink chain across the .NET Remoting boundary during marshalling of the ObjRef, which always occurs during activation of client-activated objects. However, for well-known objects, the .NET Remoting infrastructure doesn t marshal an ObjRef to the client during activation. Therefore, the client won t receive the envoy sink chain. You can get a reference to a well-known object and its associated envoy sink chain if you force the marshaling of an ObjRef for the well-known object by either returning a reference to the well-known object as a return result of a method or as an out parameter of a method.

Example: Validating Method Parameters

To demonstrate envoy sinks, we ll implement a message sink that validates message parameters. To make the example more extensible, we ve implemented a validation mechanism that allows the type implementer to create validation classes that implement the IParamValidator interface:

public interface IParamValidator { bool Validate(object[] o); }

The IParamValidator.Validate method takes an object array that represents the parameters passed to the method being validated. The first element in the array is parameter 1, the second is parameter 2, and so on. For example, the following method takes two parameters:

public void Foo(int x, int y) {   }

The corresponding IParamValidator.Validate method implementation for Foo would expect an array of objects with a Length of 2. The first array element (at index 0) is the value of x, and the second array element is the value of y.

The following code defines a message sink that uses the IParamValidator interface to validate method call parameters in the IMessage received in Sync ProcessMessage and AsyncProcessMessage:

[Serializable] public class ParamValidatorMessageSink : IMessageSink { Hashtable _htValidators; IMessageSink _NextSink; public IMessageSink NextSink { get { return _NextSink; } } // // The constructor accepts a Hashtable of IParamValidator // references keyed by the method name whose parameters // they validate. public ParamValidatorMessageSink( IMessageSink next, Hashtable htValidators) { Trace.WriteLine("ParamValidatorMessageSink ctor"); _htValidators = htValidators; _NextSink = next; } public IMessage SyncProcessMessage (IMessage msg ) { try { ValidateParams(msg); } catch ( System.Exception e ) { return new ReturnMessage(e, (IMethodCallMessage)msg); } return _NextSink.SyncProcessMessage(msg); }

Notice that the message sink is attributed with the Serializable attribute. This is so that the runtime can marshal the message sink instance during activation as part of the EnvoyInfo property of the ObjRef. Any message sink used in the envoy sink chain must be serializable so that it can flow across the .NET Remoting boundary into the client context. Also note that the constructor takes a Hashtable instance as a second parameter that hashes method names to the corresponding IParamValidator that validates the method parameters for the named method. The SyncProcessMessage method uses the following helper method to validate the parameters in the message:

 // // Validate the parameters in the message. void ValidateParams(IMessage msg) { // Make sure the msg is a method call message. if ( msg is IMethodCallMessage ) { // Wrap the message to facilitate coding. MethodCallMessageWrapper mcm = new MethodCallMessageWrapper((IMethodCallMessage)msg); // Look up the validator corresponding // to the called method. IParamValidator v = (IParamValidator)_htValidators[mcm.MethodName]; if ( v != null ) { // Use validator to validate the message parameters. if ( ! v.Validate( mcm.Args ) ) { throw new ArgumentException(  "IParamValidator.Validate() returned " +  "false indicating invalid parameter values"); } } } }

ValidateParams uses the method name contained in the message to look up the associated IParamValidator reference, which the method then uses to validate the message parameters by invoking IParamValidator.Validate. If the parameters are invalid, ValidateParams throws an exception.

The AsyncProcessMessage method also uses the ValidateParams method:

 public IMessageCtrl AsyncProcessMessage ( IMessage msg, IMessageSink replySink ) { try { ValidateParams(msg); return _NextSink.AsyncProcessMessage( msg, replySink ); } catch(System.Exception e) { replySink.SyncProcessMessage( new ReturnMessage( e, (IMethodCallMessage)msg) ); return null; } } } // End class ParamValidatorMessageSink

Because we don t care about the response message, we don t need to add a message sink to the reply sink chain. Instead, we just validate the message parameters and pass the message to the next sink in the chain. If we find the parameters invalid, we send a ReturnMessage instance, encapsulating the exception, to the first sink in the reply sink chain.

The following code defines a context property that implements IContribute EnvoySink:

[Serializable] public class ParameterValidatorProperty : IContextProperty, IContributeEnvoySink { private string _Name; Hashtable _htValidators; public string Name { get { return _Name; } } public ParameterValidatorProperty( string name, Hashtable htValidator ) { _Name = name; _htValidators = htValidators; } public IMessageSink GetEnvoySink( MarshalByRefObject obj, IMessageSink nextSink ) { return new ParamValidatorMessageSink(nextSink, _htValidators); } public void Freeze ( Context newContext ) { // When this is called, we can't add any more // properties to the context. } public Boolean IsNewContextOK ( Context newCtx ) { return true; } } // End class ParameterValidatorProperty

The ParameterValidatorProperty constructor takes a Hashtable parameter of IParamValidator references that the IContributeEnvoySink.GetEnvoySink method passes to the ParameterValidatorMessageSink constructor.

The following code defines an attribute that contributes the Parameter ValidatorProperty to the context:

[AttributeUsage(AttributeTargets.Class)] public class ParameterValidatorContextAttribute : ContextAttribute { string[] _methodNames; Hashtable _htValidators; public ParameterValidatorContextAttribute( string[] method_names, params Type[] validators) : base("ParameterValidatorContextAttribute") { if ( method_names.Length != validators.Length ) { throw new ArgumentException(  "ParameterValidatorContextAttribute ctor",  "Length of method_names and validators " +  "must be equal"); } _methodNames = method_names; _htValidators = new Hashtable(method_names.Length); int i = 0; foreach( Type t in validators ) { _htValidators.Add(method_names[i++], Activator.CreateInstance(t) ); } } public override void GetPropertiesForNewContext( IConstructionCallMessage msg ) { msg.ContextProperties.Add( new ParameterValidatorProperty( this.AttributeName, _htValidators)); } public override Boolean IsContextOK ( Context ctx , IConstructionCallMessage msg ) { return (ctx.GetProperty(this.AttributeName) != null); } }

The interesting code here is the ParameterValidatorContextAttribute constructor, which takes two parameters: an array of strings containing method names, and a variable-length params array of Type instances in which each Type is expected to implement the IParamValidator interface. The position of the elements in the arrays matches so that the method named by element 0 of the string array corresponds to the Type referenced by element 0 of the Type array. After verifying that the array lengths are the same, the constructor populates the Hashtable by mapping method names to a new instance of the matching Type.

We can use the ParameterValidatorContextAttribute like this:

[ ParameterValidatorContextAttribute( new string[] { "Bar", "Foo" }, typeof(BarValidator), typeof(FooValidator)) ] public class SomeObject : ContextBoundObject { public void Bar() { Console.WriteLine("ContextID={0}, SomeObject.Bar()", Thread.CurrentContext.ContextID); } public void Bar(int x) { Console.WriteLine("ContextID={0}, SomeObject.Bar(x={1})", Thread.CurrentContext.ContextID, x); return this; } public int Foo( int x, int y ) { return x + y; } }

Here s the listing for the BarValidator, which handles the fact that the Bar method is overloaded and limits input to an integer between 0 and 5000:

[Serializable] class BarValidator : IParamValidator { public bool Validate(object[] args) { // Bar is overloaded. We care about only the // version that has arguments. if ( args.Length != 0 ) { // First param is x. Limit to integer between 0 and 5000. int x = (int)args[0]; if (!( 0 <= x && x <= 5000 )) { string err = String.Format(  "BarValidator detected illegal 'x' parameter " +  "value of '{0}'.\nLegal values: 0 <= x <= 5000.", x); throw new ArgumentException(err); } } return true; } }

Figure 6-4 shows a screen shot of the exception message resulting from calling the Bar method and passing it the value 6500 for the x parameter.

figure 6-4 message box displayed as a result of passing invalid value for x to the someobject.bar method

Figure 6-4. Message box displayed as a result of passing invalid value for x to the SomeObject.Bar method

Here s the listing for the FooValidator, which validates both parameters passed to the SomeObject.Foo method:

[Serializable] class FooValidator : IParamValidator { public bool Validate(object[] args) { string e = ""; // First parameter is x. Limit it to // an integer between 0 and 9999. int x = (int)args[0]; if ( x > 9999 ) { e = "parameter 'x' exceeds maximum allowed value " +  "of '9999'"; } // Second parameter is y. Limit it to // an integer less than 1000. int y = (int)args[1]; if ( y > 1000 ) { e = "parameter 'y' exceeds maximum allowed value " +  "of '1000'"; } if ( e.Length != 0 ) { throw new ArgumentException ("FooValidator detected illegal parameter values\n\n" + e); } return true; } }

Note that we declare the validator classes with the Serializable attribute. This is because the message sink holds a reference to these classes in its _htValidators member. Because the runtime marshals the envoy sink chain to the client in the EnvoyInfo property of the ObjRef, any message sink members must also be Serializable. You ll also want to keep them lightweight to minimize the transmission cost during marshaling across the .NET Remoting boundary.



Microsoft. NET Remoting
Microsoft .NET Remoting (Pro-Developer)
ISBN: 0735617783
EAN: 2147483647
Year: 2005
Pages: 46

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net