Programming .NET Components, 2nd Edition

11.3. Custom Component Services

The ability to install custom component services in .NET is a major advancement for software engineering and component-oriented programming. Custom component services allow you to fine-tune and optimize the way .NET services your 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; you can focus on implementing the business logic, rather than the service. Examples of custom services include application logging and tracing, performance counters, custom thread management, filtering of method calls, parameter checks, event subscriptions, and so on.

Custom component services are provided in the form of custom context attributes. Ordinary custom attributes (such as the ones discussed in Appendix C) have no use unless you provide the reflection code to look for these attributes, interpret their values, and act upon them. .NET is indifferent to such custom attributes. Unlike generic custom attributes, .NET is very much aware of custom context attributes when they are used on context-bound objects. Context attributes must derive from the class ContextAttribute, defined in the System.Runtime.Remoting.Contexts namespace. When creating a new context-bound object, .NET reflects the object's metadata and places it 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 can be used to install all four types of message sink interceptors. The next two sections demonstrate how to build custom context attributes and component services. First, you will see how to develop a custom context attribute and how it affects the activation context; then you'll look at how to install custom message sinks. Finally, you'll walk though the development of two real-life, useful custom component services.

Custom context attributes and custom message sinks are undocumented features of .NET. Microsoft is committed to supporting contexts in future versions of .NET, but not to extending the infrastructure and adding features.

11.3.1. Building a Custom Context Attribute

Each context has a set of properties associated with it. The properties are the component services the context supports. A context-bound object shares a context with its client only if the client's context has all the services the component requiresin other words, if the context has the required properties. If the client's context doesn't have one or more of the properties the object requires, .NET creates a new context and puts the object in it. In addition, a context property may require a new context regardless of the properties of the client's context. You use context attributes to specify the required services. The context attributes are those that decide whether or not the client's context is sufficient.

To understand how context attributes affect context activation, consider a custom context attribute that adds a color property to a context. The color is an enum of the 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 MyClass: ContextBoundObject {...}

Obviously, a color property isn't much of a service, but it's a good example. .NET creates objects of the class MyClass 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, .NET creates a new context, lets the attribute set its color property to ColorOption.Blue, and places the new object in the new context. ColorAttribute also has a default constructor, setting the context color to ColorOption.Red:

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

Example 11-2 shows the implementation of the ColorAttribute custom context attribute.

Example 11-2. The ColorAttribute 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) { IContextProperty 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 { 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 (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; } } }

ColorAttribute has a member called m_Color that contains 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 derives from ContextAttribute. The single constructor of ContextAttribute requires a string naming the new context attribute. This is provided by a call to the ContextAttribute constructor in the ColorAttribute constructor:

public ColorAttribute(ColorOption color) : base("ColorAttribute") {...}

ContextAttribute derives from and provides a virtual implementation of the IContextAttribute interface, defined as:

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

The IsContextOK( ) method lets the context attribute examine the creating client's context, which is provided in the ctx parameter. If the client's context is adequate, no further action is required, and .NET activates the new object in the creating client's context. If the context attribute returns false from IsContextOK( ), .NET creates a new context and calls GetPropertiesForNewContext( ), letting the context attribute add new properties to the new context. Because a single object can have more than one context attribute, .NET can optimize its queries of the attributes. .NET starts iterating over the attribute list, calling IsContextOK( ) on each one. As soon as it finds an attribute in the list that returns false, .NET 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 that implement the IContextProperty interface:

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

Each context property is identified by name via 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 it doesn't, 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 an object of type IConstructionCallMessage, providing a collection of properties for the new context via the ContextProperties property. ColorAttribute creates an object of type ColorProperty, 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's possible that some will conflict with others. To handle such an eventuality, after adding all the properties to the new context, .NET calls IsNewContextOK( ) on each property. If a property returns false, .NET aborts creating the new object and throws an exception of type RemotingException. 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 available for advanced use only.

Figure 11-7 is a UML activity diagram summarizing the process flow when using a custom context attribute and a context property. The diagram shows the order in which the various methods take place and the resulting activation logic.

Figure 11-7. Custom context attribute and property activity diagram

11.3.2. Installing a 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 either a server context sink, a client context sink, an envoy sink, or a server object sink. Commonly, a custom context attribute installs only a server context sink. The other sinks are intended for advanced cases, but you can install one if the need arises. For each type of custom sink you wish to contribute to the interception chain, the custom context property must implement a matching interface.

11.3.2.1 Providing a server context sink

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

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

In its implementation of GetServerContextSink( ), the context property creates a sink object and concatenates it to the next sink in the chain, which is provided as the method parameter. GetServerContextSink( ) should return the new sink it created so that .NET can add it to the interception chain. For example, here is how to install GenericSink (presented in Example 11-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. .NET calls GetServerContextSink( ) after its call to IContextProperty.IsNewContextOK( ) and before creating the object, allowing the context property to provide the sink. A server context sink can intercept construction calls.

11.3.2.2 Providing a client context sink

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

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

A client context sink affects the context-bound object only when it's the client of another object outside the context; it intercepts all calls exiting the context. .NET calls GetClientContextSink( ) only when the object makes its first call outside the context. The information in the message object passed to the sink pertains to the target object, not the client.

11.3.2.3 Providing an envoy sink

The context property can also implement the IContributeEnvoySink interface, defined as:

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

In this case, when a proxy to an object on the client's side is set up, the proxy has an envoy sink as part of the interception chain leading to that object. The envoy sink intercepts all calls going from the client to the object. Other objects accessed by the client aren't affected. Every time a new client in a different context connects to the object, .NET installs an envoy sink in that client's context. .NET calls GetEnvoySink( ) after creating the new object but before returning control to the client. You can't intercept construction calls with an envoy sink.

11.3.2.4 Providing an object sink

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

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

The object sink is installed on an object-by-object basis, which means it intercepts calls only to the object whose reference is provided in the GetObjectSink( ) call. Other calls into the context aren't affected. .NET calls GetObjectSink( ) before the first method call is forwarded to the object. As a result, you can't intercept construction calls with an object sink.

11.3.2.5 Processing messages

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

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 was called, and the object's location. You can use that information in your pre-call message-processing logic. After the last sinkthe stack builderinvokes 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:

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 provides additional information about the method's returned value, the values of any outgoing parameters, and any exceptions. The fact that exception information is captured is of particular interest. If the 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 re-throws it on the calling client's side.

11.3.3. The Logbook Service

It's time to put all the knowledge and intricacies described so far to good use, with a comprehensive and useful real-life example. One of the most beneficial steps you can take to achieve a robust application and faster time to market is to add a logging capability to your application. This section presents you with the logbooka simple custom component service that allows you to automatically log method calls and exceptions. The logbook is your product's flight recorder, and in a distributed environment, it's worth its weight in gold; with it, you can analyze why something didn't work the way it was supposed to. Examining the logbook entries, you can analyze what took place across machines and applications, and the source of the problem is usually almost immediately evident. The logbook is also useful for troubleshooting customer problems in post-deployment scenarios. The logbook intercepts incoming calls to your context-bound objects and logs most of the information in the messages. As you will see shortly, you can use the same logbook to record method invocations 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: machine name, app domain name, thread ID and name, and context ID

  • The caller's identity (username)

  • Information about the target object: its assembly, its type, and the member being accessed (constructor, method, property, indexer, or event)

  • The invocation date and time

  • Error information, if an exception was thrown: the type of the exception and the exception message

11.3.3.1 Using the logbook

A key requirement in designing the logbook was that it should require no explicit participation on behalf of the object. The object should focus on implementing its business logic, and the logbook should do the logging. To add logging support for a

Contexts, AOP, and Indigo

The next generation of Microsoft distributed application technology is code named Indigo. Indigo unifies the three remote-call technologies available in .NET: Enterprise Services, remoting, and web services. This new technology is designed to connect publicly exposed services. A service in Indigo terminology is something that exposes logical functionality across boundaries (e.g., machine or technology boundaries). Inside an Indigo service, you will still use conventional .NET programming. Indigo provides Enterprise Services-like services to the services it connects, such as propagation of transactions and security call contexts, thread synchronization, event publishing and subscription, and so on. Indigo is based on the exchange of messages. Indigo services are implemented using message interception and pre- or post-call processing. Indigo has its own notion of contexts, and it also provides for interception-based custom services.

Although .NET contexts will not be able to map into Indigo contexts, using them today has the potential of benefiting your Indigo applications in the future. The reason is that the programming model itself is reusable. Using interception-based component services takes the weight of developing the plumbing and infrastructure off the developers' shoulders and lets them be more productive by focusing on the business logic. This is, of course, the idea behind aspect-oriented programming (AOP): allowing you to add aspects to the business logic and leave the logic free of plumbing.

While AOP as a generic mechanism is impossible to implement due to the aspect-ordering problem, the ability to add a custom service to your application is very valuable indeed. When you implement a service in an aspect-oriented application, there are two things you need to do: you need to implement the decorating aspect, and you need to apply it. The aspect implementation is always specific to the technology used and is not trivial to portit is tied to the particulars of your supporting technology. For example, the context sink interceptor implementation is not reusable and does not easily transfer to other technologies. You will have to reinvest in implementing your aspects and services in Indigo. However, aspects provide you with a valuable programming model on the using side by extracting from the application the code used for security, logging, and so on. This programming model is transferable across technologiesthat is, if you use a .NET context attribute for logging, and the component code itself does not contain any logging logic, this programming model will be transferable to Indigo. Simply replace the context logging attribute with the equivalent Indigo logging attribute, and you get Indigo-based logging, without affecting the component code. Because there can be many components using any given .NET context-based service, the cost of implementing the service is amortized over many components; the real savings is in maintaining the programming model and its benefits on the component side.

context-bound object, add the LogbookAttribute custom context attribute, defined in the ContextLogger namespace:

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

The logbook service allows you to choose what information to log. Sometimes, it's necessary to record everything that takes place (method calls and errors). In other situations, it's 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 only unhandled exceptions, write:

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

The parameter-less constructor of LogbookAttribute defaults to LogOption. MethodCalls, so these two declarations are equivalent and can log both method calls and errors:

[Logbook] [Logbook(LogOption.MethodCalls)]

11.3.3.2 The logbook service architecture

When you apply LogbookAttribute to a context-bound class, it requires private contexts for each instance to support logging of all calls coming into the object. If it were possible for two objects using the LogbookAttribute to share a context, cross-context calls would be logged, but intra-context calls made on one another wouldn't. The LogbookAttribute adds to the new context a property called LogContextProperty, which contributes a server-context sink called LogSink. LogSink intercepts all calls to the object but doesn't log them itself; instead, it uses the Logbook component, which encapsulates the actual logging mechanism. The implementation provided here logs to a SQL Server database, but you can replace that with any other repository. The Logbook is a remote singleton residing in an EXE host. As a result, all objects using the logbook service actually log to the same repository, in order. This is a key feature that allows you to trace the execution of a distributed application, because the host can be on a dedicated machine used by all other machines.

Figure 11-8 depicts the logbook architecture. In addition, a logbook viewer application is provided; it displays in a grid control the content of the logbook database entries table. The logbook viewer allows you to filter the grid to display methods and errors, or just errors. The application has another feature, too: you can export the logbook entries to a logfile, as well as display the content of an existing logfile. The logbook viewer doesn't connect to the database directly. Instead, it too connects to the singleton Logbook. The viewer doesn't directly connect to the database for two reasons. The first is that if it did, you would couple the viewer to the repository used and would have to modify the viewer each time you switched repositories. The Logbook provides the necessary indirection. Second, to avoid database synchronization issues, the Logbook is the single data access component, and it provides the synchronization.

Figure 11-8. The logbook service architecture supports logging in a distributed environment

The source files accompanying this book include the Logbook solution. The solution comprises 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 that can access an SQL Server database. (You will need to create a database called Logbook with an Entries table by running the included Logbook.sql script file.) The LogbookHost project is a simple Windows Forms EXE that hosts Logbook. The LogbookHost configuration file exposes the Logbook type as a server-activated singleton object. The TestClient project is a Windows Forms application that 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 that 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, which lets you browse the logbook entries or clear the table (see Figure 11-9). The LogbookViewer application registers the Logbook component as a remote server as well.

You can extend and modify the logbook to suit your particular needs: you can log parameter types and values, and you can use other repositories besides SQL Server.

Figure 11-9. The logbook viewer application

11.3.3.3 Implementing the logbook

The LogbookAttribute class isn't that different from the ColorAttribute class presented in Example 11-2. It refuses the client's context in its IsContextOK( ) method and installs the LogContextProperty property in its GetPropertiesForNewContext( ) method. Example 11-3 shows the implementation of LogbookAttribute.

Example 11-3. Implementation of the 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) { IContextProperty 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; } }

LogContextProperty implements the IContributeServerContextSink interface, installing the LogSink server context sink:

public class LogContextProperty : IContextProperty,IContributeServerContextSink { 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 the LogSink class. LogSink implements the IMessageSink interface. In its implementation of IMessageSink, LogSink processes the message object and 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 the object's location and execution scope. LogSink then passes the LogbookEntry object to the Logbook component. Because the logbook is accessed as a remote component, LogbookEntry is marshaled by value using the Serializable attribute. Example 11-4 contains the code for LogbookEntry.

Example 11-4. The LogbookEntry structure

[Serializable] public struct LogbookEntry { public LogbookEntry(string assemblyName,string typeName,string methodName, string eventDescription): this(assemblyName,typeName,methodName,String.Empty,String.Empty) { Event = eventDescription; } public LogbookEntry(string assemblyName,string typeName,string methodName): this(assemblyName,typeName,methodName,String.Empty,String.Empty) {} public LogbookEntry(string assemblyName,string typeName,string methodName, string exceptionName,string exceptionMessage) { AssemblyName = assemblyName; TypeName = typeName; MemberAccessed = methodName; ExceptionName = exceptionName; ExceptionMessage = exceptionMessage; Event = String.Empty; MachineName = Environment.MachineName; AppDomainName = AppDomain.CurrentDomain.FriendlyName; ThreadID = Thread.CurrentThread.ManagedThreadId( ); 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; //Event public readonly string Event; }

Example 11-5 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 a few helper parsing methods. LogSink then forwards the call to the next sink down the chain, to eventually call 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 done, LogSink constructs a LogbookEntry object and adds it to the logbook using the Logbook object.

Example 11-5. The 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 = msg as IMethodMessage; Debug.Assert(methodMessage != null); string assemblyName = GetAssemblyName(methodMessage); string typeName = GetTypeName(methodMessage); string methodName = GetMethodName(methodMessage); IMethodReturnMessage returnedMessage; returnedMessage = m_NextSink.SyncProcessMessage(msg) as IMethodReturnMessage; Debug.Assert(returnedMessage != null); 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 != String.Empty) { 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 2005 to generate much of the ADO.NET data connectivity classes and code. However, 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:

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. Logbook is a thread-safe component that locks itself in every method call to synchronize concurrent access.

Although this functionality is unrelated to contexts and interception, I thought it would be handy if any object (even a non-context-bound object using the service) could explicitly log information to the logbook. This is done using the static method AddEvent( ) of Logbook:

public class MyClass { public void SomeMethod( ) { Logbook.AddEvent("Some event took place"); } }

The AddEvent( ) implementation captures the same information as LogSink, and it uses the same LogbookEntry struct. The big difference is that AddEvent( ) doesn't use interception. Instead, it uses the StackFrame class, defined in the System.Diagnostics namespace:

public static void AddEvent(string description) { StackFrame frame = new StackFrame(1);//Get the frame of the caller string typeName = frame.GetMethod().DeclaringType.ToString( ); string methodName = frame.GetMethod( ).Name; string assemblyName = Assembly.GetCallingAssembly().GetName( ).Name; LogbookEntry logbookEntry = new LogbookEntry(assemblyName, typeName,methodName,description); Logbook logbook = new Logbook( ); logbook.AddEntry(logbookEntry); }

StackFrame provides access to information on every caller up the call chain. In this case, AddEvent( ) simply extracts the information on the method that called it.

11.3.4. The Transaction Management Service

.NET 2.0 introduces an innovative transaction management service in the System.Transactions namespace. A transaction managed by System.Transactions is stored in the thread local storage and is called the ambient transaction. System.Transactions-enabled resource managers (such as SQL Server 2005) detect the ambient transaction and automatically enlist in the transaction, similar to the auto-enlistment of Enterprise Services resource managers. This for the most part eliminates the need to manage the transaction yourself. The main feature of System.Transactions is its ability to automatically promote the transaction across transaction managers, from the lightweight transaction manager (LTM) used with a single object and a single durable resource to the OleTx transaction manager used to manage a distributed transaction. For more information about System.Transactions, see my whitepaper "Introducing System.Transactions in the Microsoft .NET Framework Version 2" (MSDN, April 2005).

When not using Enterprise Services (and until the release of Indigo), System.Transactions supports only an explicit programming model. You typically interact with an object of type transactionScope, defined as:

public class TransactionScope : IDisposable { public TransactionScope( ); public TransactionScope(TransactionScopeOption scopeOptions); //Additional constructors public void Complete( ); public void Dispose( ); }

As the name implies, the transactionScope class is used to scope a code section with a transaction, as demonstrated in Example 11-6. Internally in its constructor, the TRansactionScope object creates a transaction (an LTM transaction, by default) and assigns it as the ambient transaction. transactionScope is a disposable objectthe transaction will end once the Dispose( ) method is called (the end of the using statement in Example 11-6).

Example 11-6. Using the TransactionScope class

TransactionScope scope = new TransactionScope( ); using(scope) { /* Perform transactional work here */ //No errors - commit transaction scope.Complete( ); }

The transactionScope object has no way of knowing whether the transaction should commit or abort. To address this, transactionScope internally maintains a consistency bit, which is set by default to false. You can set the consistency bit to TRue by calling the Complete( ) method. If the consistency bit is set to false when the transaction ends, the transaction will abort; otherwise, it will try to commit. Note that once you call Complete( ), there is no way to set the consistency bit back to false.

11.3.4.1 Transaction flow management

Transaction scopes can nest both directly and indirectly. A direct scope nesting is simply one scope nested inside another. An indirect scope nesting occurs when you call a method that uses a transactionScope object from within a method that uses its own scope. You can also have multiple scope nesting, involving both direct and indirect nesting. The topmost scope is referred to as the root scope. The question is, of course, what is the relation between the root scope and all the nested scopes? How will nesting a scope affect the ambient transaction? To address these questions, the transactionScope class provides several overloaded constructors that accept an enum of the type transactionScopeOption, defined as:

public enum TransactionScopeOption { Required, RequiresNew, Suppress }

The value of transactionScopeOption lets you control whether the scope takes part in a transaction, and, if so, whether it will join the ambient transaction or be the root scope of a new transaction. For example, here is how you specify the value of the transactionScopeOption in the scope's constructor:

TransactionScope scope; scope = new TransactionScope(TransactionScopeOption.Required); using(scope) {...}

The default value for the scope option is transactionScopeOption.Required. The TRansactionScope object determines which transaction to belong to when it is constructed. Once determined, the scope will always belong to that transaction. TRansactionScope bases its decision on two factors: whether an ambient transaction is present and the value of the TRansactionScopeOption parameter.

A transactionScope object has three options:

  • Join the ambient transaction.

  • Be a new scope root; that is, start a new transaction and have that transaction be the new ambient transaction inside its own scope.

  • Not take part in a transaction at all.

If the scope is configured with TRansactionScopeOption.Suppress, it will never be part of a transaction, regardless of whether an ambient transaction is present.

If the scope is configured with transactionScopeOption.Required, and an ambient transaction is present, the scope will join that transaction. If, on the other hand, there is no ambient transaction, the scope will create a new transaction and become the root scope.

If the scope is configured with TRansactionScopeOption.RequiresNew, it will always be the root scope. It will start a new transaction, and its transaction will be the new ambient transaction inside the scope.

The way the values of TRansactionScopeOption affect the flow of the transaction is analogous to the way the integer constants provided to the Synchronization attribute control the flow of the synchronization domain, as discussed in Chapter 8.

11.3.4.2 Declarative transaction support

You can use context-bound objects and call interception to provide declarative support for System.Transactions. You will need to install a server context sink that wraps the call to the next sink down the chain in a transactionScope. Example 11-7 shows the transactionAttribute class. Obviously, you will also need a context attribute that adds a context property that installs the sink. These two classes (transactionAttribute and transactionalProperty, respectively) are very similar to LogbookAttribute and LogContextProperty. The transactionAttribute's constructor accepts an enum of the type transactionScopeOption, indicating how the transaction should flow through this context-bound object. The default constructor uses transactionScopeOption.Required.

Example 11-7. The TransactionAttribute class

using System.Transactions; [AttributeUsage(AttributeTargets.Class)] public class TransactionAttribute : ContextAttribute { TransactionScopeOption m_TransactionOption; public TransactionAttribute( ) : this(TransactionScopeOption.Required) {} public TransactionAttribute(TransactionScopeOption transactionOption) : base("TransactionAttribute") { m_TransactionOption = transactionOption; } //Add a new transaction property to the new context public override void GetPropertiesForNewContext(IConstructionCallMessage ctor) { IContextProperty transactional; transactional = new TransactionalProperty(m_TransactionOption); ctor.ContextProperties.Add(transactional); } //Provides a private context public override bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg) { return false; } }

The transactionalProperty class installs the transactionSink class as a server context sink, providing it with the transaction scope option:

public class TransactionalProperty: IContextProperty, IContributeServerContextSink { TransactionScopeOption m_TransactionOption; public IMessageSink GetServerContextSink(IMessageSink nextSink) { IMessageSink transactionSink; transactionSink = new TransactionSink(nextSink,m_TransactionOption); return transactionSink; } //Rest of the implementation }

The interesting work, of course, is done by transactionSink, shown in Example 11-8.

Example 11-8. The TransactionSink class provides a transactional context

public class TransactionSink : IMessageSink { IMessageSink m_NextSink; TransactionScopeOption m_TransactionOption; public TransactionSink(IMessageSink nextSink, TransactionScopeOption transactionOption) { m_TransactionOption = transactionOption; m_NextSink = nextSink; } public IMessageSink NextSink { get { return m_NextSink; } } public IMessage SyncProcessMessage(IMessage msg) { IMethodReturnMessage returnedMessage = null; Exception exception; TransactionScope scope = new TransactionScope(m_TransactionOption); using(scope) { try { returnedMessage = (IMethodReturnMessage)m_NextSink. SyncProcessMessage(msg); exception = returnedMessage.Exception; } catch(Exception sinkException) { exception = sinkException; } if(exception == null) { scope.Complete( ); } return returnedMessage; } } public IMessageCtrl AsyncProcessMessage(IMessage msg,IMessageSink replySink) { string message = "Transactional calls must be synchronous" throw new InvalidOperationException(message); } }

In SyncProcessMessage( ), transactionSink constructs a new transactionScope object, passing its constructor the original value of the transactionScopeOption enum passed to the transaction attribute. A using statement with the scope object wraps the call to SyncProcessMessage( ) on the next sink down the chain. If no exception has occurred, SyncProcessMessage( ) calls Complete( ) on the scope and returns.

Example 11-9 demonstrates the use of the TRansaction attribute.

Example 11-9. Using the Transaction attribute

[Transaction] public class RootClass : ContextBoundObject { public void CreateObjects( ) { Class1 obj1 = new Class1( ); Class2 obj2 = new Class2( ); Class3 obj3 = new Class3( ); } } [Transaction] public class Class1 : ContextBoundObject {} [Transaction(TransactionScopeOption.Suppress)] public class Class2 : ContextBoundObject {} [Transaction(TransactionScopeOption.RequiresNew)] public class Class3 : ContextBoundObject {}

Figure 11-10 depicts the resulting transactions after executing this transactionDemo( ) method:

//Non-transactional client class MyClient { public void TransactionDemo( ) { RootClass root = new RootClass( ); root.CreateObjects( ); } }

The RootClass class is configured to require a transaction. Since it is being called by a non-transactional client (MyClient is not even context-bound), there is no ambient transaction. RootClass therefore starts a new transaction and becomes its root. When the client calls CreateObjects( ) on the RootClass object, the object creates three other context-bound objects, each configured with a different transactionScopeOption value. Class1 is configured to require a transaction, so it will join the transaction of the RootClass object. Class2 suppresses any transaction flow, so it will execute without an ambient transaction. Class3 requires a new transaction, so it will be placed in a new transaction.

Figure 11-10. Transactions flow across transactional contexts

You can add granularity to the transaction attribute and, instead of being object-based (that is, all calls are transactional), make it method-based. Define a method-level attribute and apply it to the methods you wish to be called transitionally. Have SyncProcessMessage( ) reflect the target method to see if it has the attribute, and if so, wrap the method call with a transactionScope.

Категории