Introduction
The modern architecture model requires publishing its business process
behavior in the loosely coupled design pattern. The COM+ Event System can
accomplish this task with a minimum coding. This article describes how the LCE
can be incorporated in the .Net application using the C# language. In my sample
I used a simple Log Message publisher to notify a subscriber in the �fire &
forgot� manner. Based on that, it can be built a more sophisticated
publisher/subscriber for post-processing purposes such as analyzing, modeling,
tuning, animation, test cases, debugging, tracing, logging, auditing,
monitoring, etc. Notice that the about features incorporated into an application
model core will be very helpful in your incrementally development phase, QA and
production. Before looking at closely for each component of that mechanism from
the application point of view, I�d like to describe how the .Net would
interoperate with COM+ Event System.
COM+ Event System.
The COM+ Event System is an unmanaged code � model with COM interfaces
packaged into the es.dll component. To enable this code to use in the
.Net world we have to generate some metadata for it. The interop layer (RCW) is
using this metadata in the run-time to handle an actual COM object (activation,
marshaling requirements, etc). MS offers a very useful tool (TlbImp.exe)
to generate a metadata from the typelib of unmanaged code, which can be added
into the assembly. In my case I choose another way � the reengineering. Based on
the EventSys.h file I created abstract definitions of the interfaces
needed for their interoperability included their empty classes such as
EventSystem
and EventSubscription
, see the following
snippet code:
#region COM+ EventSystem Interfaces
namespace RKiss.EventSystem
{
[ComImport, Guid("4E14FBA2-2E22-11D1-9964-00C04FBBB345")]
public class EventSystem {}
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch),
Guid("4E14FB9F-2E22-11D1-9964-00C04FBBB345")]
public interface IEventSystem
{
[PreserveSig]
object Query([In, MarshalAs(UnmanagedType.BStr)] string progId,
[In, MarshalAs(UnmanagedType.BStr)] string queryCriteria,
[Out] out Int32 errorIndex);
[PreserveSig]
void Store([In, MarshalAs(UnmanagedType.BStr)] string progId,
[In, MarshalAs(UnmanagedType.Interface)] object pInterface);
[PreserveSig]
void Remove([In, MarshalAs(UnmanagedType.BStr)] string progId,
[In, MarshalAs(UnmanagedType.BStr)] string queryCriteria,
[Out] out Int32 errorIndex);
[PreserveSig]
string get_EventObjectChangeEventClassID();
[PreserveSig]
object QueryS([In, MarshalAs(UnmanagedType.BStr)] string progId,
[Out] out Int32 errorIndex);
[PreserveSig]
void RemoveS([In, MarshalAs(UnmanagedType.BStr)] string progId,
[Out] out Int32 errorIndex);
}
[ComImport, Guid("7542E960-79C7-11D1-88F9-0080C7D771BF")]
public class EventSubcription {}
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch),
Guid("4A6B0E15-2E38-11D1-9965-00C04FBBB345")]
public interface IEventSubcription
{
string SubscriptionID { get; set; }
string SubscriptionName { get; set; }
string PublisherID { get; set; }
string EventClassID { get; set; }
string MethodName { get; set; }
string SubscriberCLSID { get; set; }
object SubscriberInterface { get; set; }
bool PerUser { get; set; }
string OwnerSID { get; set; }
bool Enabled { get; set; }
string Description { get; set; }
string MachineName { get; set; }
[PreserveSig]
object GetPublisherProperty([In, MarshalAs(UnmanagedType.BStr)]
string PropertyName);
[PreserveSig]
void PutPublisherProperty([In, MarshalAs(UnmanagedType.BStr)]
string PropertyName, ref object propertyValue);
[PreserveSig]
void RemovePublisherProperty([In, MarshalAs(UnmanagedType.BStr)]
string PropertyName);
[PreserveSig]
object GetPublisherPropertyCollection();
[PreserveSig]
object GetSubscriberProperty([In, MarshalAs(UnmanagedType.BStr)]
string PropertyName);
[PreserveSig]
void PutSubscriberProperty([In, MarshalAs(UnmanagedType.BStr)]
string PropertyName, ref object propertyValue);
[PreserveSig]
void RemoveSubscriberProperty([In, MarshalAs(UnmanagedType.BStr)]
string PropertyName);
[PreserveSig]
object GetSubscriberPropertyCollection();
string InterfaceID { get; set; }
}
}
#endregion
Now, access to the COM+ Event System is transparently and it�s not different
from the regular .Net class programming, see the following snippet code:
public void Deactivate()
{
int errorIndex = 0;
IEventSystem es = new EventSystem.EventSystem() as IEventSystem;
string strCriteria = "SubscriptionID=" + "{" + guidSub + "}";
es.Remove("EventSystem.EventSubscription", strCriteria, out errorIndex);
}
Event Class
This is a design start point and only this assembly will have
interoperability with a COM+ services. Here is all magic glue how to use this
service in the .Net Framework and it is done by an abstract definition of the
Event
Interface, Event
Class and their attributes.
Interface.
The contract between the publisher and subscriber is represented by
IEventWriteLog
interface. This abstract has only one method
signature with a returned type �void� (nothing to return in the LCE). The
interface is public and it will be use in the subscriber design to receive the
event call.
Class.
As a next step is a definition of our application Event
Class,
which the publisher will use to delegate a job to the event system. This class
represents a layer between a managed (.Net) world and unmanaged COM+ services.
The class has no method implementation and it is derived from the
EnterpriseServices.ServicedComponent
and IEventWriteLog
interface.
Attributes.
There are two kinds of attributes: assembly and class. - The assembly
attributes describes an application properties in the COM+ catalog (name,
activation, access, etc.). - The class attributes describes a component in the
application registered into COM+ catalog (poolable, no transactional, event
class). Note that only the EventClassAttribute
is a mandatory
decoration for the LCE COM+ service, the otherwise the class will not be
registered in the Event System Store.
The EventClass implementation is shown in the following snippet code:
using System;
using System.Diagnostics;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
using System.Reflection;
using RKiss.EventSystem;
[assembly: AssemblyKeyFile(@"..\..\EventClass.snk")]
[assembly: ApplicationName("EventClassLogger")]
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(Value = false,
Authentication = AuthenticationOption.None)]
#region COM+ EventSystem Interfaces
#endregion
namespace RKiss.EventClassLogger
{
[Guid("050355EC-83C9-4723-9E2D-591AD3CA3B45")]
public interface IEventWriteLog
{
void Write(object e);
}
[EventClass(FireInParallel = true)]
[Guid("F6B2E72A-677F-4356-B33D-C1B5CE1688FA")]
[Transaction(TransactionOption.Disabled)]
[ObjectPooling(Enabled=true, MinPoolSize=4, MaxPoolSize=10)]
[EventTrackingEnabled]
public class EventClassLog : ServicedComponent, IEventWriteLog
{
public void Write(object e)
{
Trace.WriteLine(
"EventClassLog: COM+ LCE system doesn't work properly");
}
}
}
After compiling this subproject, the EventClass
assembly is
necessary to install into the Global Assemble Cache (c:\gacutil �i
EventClass.dll) and register in the COM+ catalog using the
regsvcs.exe utility (c:\regsvcs /tlb EventClass.dll).
Publisher.
In my design the LogMessage
Publisher is a light .Net class �
gateway to the COM+ LCE notification. It doesn�t require any registration
process with the COM+ Event System. As you can see in the following snippet, the
application method WriteLog(object msg)
is creating an instance of
the EventClass
and invoking its method Write(msg)
.
Notice that the �using� statement is used to dispose underlying resources in the
ServicedComponent, otherwise the COM+ object will be not return back to the
pool.
using System;
using System.Diagnostics;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
using RKiss.EventClassLogger;
namespace RKiss.EventPublisherLogger
{
[Guid("4766CB18-B680-4430-AD9E-56411B73E802")]
public class PublisherLog
{
public void WriteLog(object msg)
{
using (EventClassLog evt = new EventClassLog())
{
evt.Write(msg);
}
}
}
}
This is a simple scenario. In the real product, the publisher will have more
application methods to hide all wrapping, filtering and formatting arguments
into properly message format, for instance in XML. The snippet code might be
looked like the following example:
public void WriteLog(LOG_SEVERITY sev, LOG_CATEGORY cat,
string key, string[] msg)
{
long starttime = Environment.TickCount;
XmlLogMsg lm = new XmlLogMsg(sev, cat, key, msg);
StringBuilder xmlStringBuilder = new StringBuilder();
XmlTextWriter tw = new XmlTextWriter(
new StringWriter(xmlStringBuilder));
XmlSerializer serializer = new XmlSerializer(typeof(XmlLogMsg));
serializer.Serialize(tw, lm);
string xmlMsg = xmlStringBuilder.ToString();
tw.Close();
WriteLog(xmlMsg);
}
namespace EventXmlLogMessage
{
public enum LOG_SEVERITY : int { Unknown = 0, Info, Warning,
Error, Fatal }
public enum LOG_CATEGORY : int { Unknown = 0, System,
User, Database, Network,
Security, Core, Config, Services, Framework }
[XmlRoot("LogMessage")]
public class XmlLogMsg
{
[XmlElement(ElementName = "Header")]
public XmlLogHeader hdr;
[XmlArray(ElementName = "Body")]
[XmlArrayItem(ElementName = "Message")]
public string[] body;
public XmlLogMsg() {}
public XmlLogMsg(LOG_SEVERITY sev, LOG_CATEGORY cat, string key,
string[] msg)
{
hdr = new XmlLogHeader(sev, cat,key);
body = msg;
}
public class XmlLogHeader
{
[XmlAttribute(AttributeName = "Severity")]
public string sev;
[XmlAttribute(AttributeName = "Catalog")]
public string cat;
[XmlAttribute(AttributeName = "Key")]
public string key;
[XmlAttribute(AttributeName = "TickCount")]
public string tickcount;
[XmlAttribute(AttributeName = "TimeStamp")]
public string timestamp;
public XmlLogHeader() {}
public XmlLogHeader(LOG_SEVERITY sev, LOG_CATEGORY cat, string key)
{
this.sev = sev.ToString(); this.cat = cat.ToString(); this.key = key;
timestamp = DateTime.Now.ToString();
tickcount = Environment.TickCount.ToString();
}
}
}
}
Now, all tightly design pattern is done. The application can run without any
impact in the business process. The publisher will run in the �short circuit�
when the Event System will not find a properly target (subscription). So, the
next step is to design a component on the consumer side.
Subscriber.
The Subscriber is a managed class with a loosely coupled design pattern based
only on the Event interface contract, which is an abstract definition. The
subscriber and publisher are �wired� using the Subscription, which is a set of
the properties (metadata) to perform the LCE task during the run-time. The Event
Subscription is stored in the Event System and based on that, the publisher will
know how to invoke the subscriber�s method under the interface contract in the
late-binding manner. There are three kinds of form subscriptions: moniker,
persistent and transient. In our case, the transient subscription is used, so
the consumer (client) has responsibility to register a valid IUnknown
pointer, where wants to receive incoming event calls. The other important
thing in the subscriber design is how long the incoming event call stayed here
(except option FireInParallel
). To avoid the thread blocking by
some subscribers or slow UI, the event call is queued into the
System.Threading.ThreadPool
in the �fire & forgot� way. This
solution will guarantee a full isolation and minimum overhead between the
business process and its post-processing activities. Of course, in some cases,
the tightly coupled notification is an advantage and it is requested and then
the thread pool is obsolete.
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using RKiss.EventSystem;
using RKiss.EventClassLogger;
namespace RKiss.EventSubscriberLogger
{
public delegate void SubscriberLogNotify(object e);
public class SubscriberLog : IEventWriteLog
{
public event SubscriberLogNotify notify;
private string guidSub = Guid.NewGuid().ToString();
public string SubscriptionID { get { return guidSub; }}
public void Activate()
{
IEventSystem es = new EventSystem.EventSystem() as IEventSystem;
IEventSubcription sub = new EventSubcription() as IEventSubcription;
sub.Description = "Subscription LogMessage";
sub.SubscriptionName = "SubscriberLog";
sub.SubscriptionID = "{" + guidSub + "}";
sub.Enabled = true;
sub.EventClassID = "{" + Type.GetTypeFromProgID(
"RKiss.EventClassLogger.EventClassLog").GUID.ToString() + "}";
sub.MethodName = "Write";
sub.SubscriberInterface = this;
es.Store("EventSystem.EventSubscription", sub);
}
public void Deactivate()
{
int errorIndex = 0;
IEventSystem es = new EventSystem.EventSystem() as IEventSystem;
string strCriteria = "SubscriptionID=" + "{" + guidSub + "}";
es.Remove("EventSystem.EventSubscription",
strCriteria, out errorIndex);
}
public void Write(object e)
{
if(notify != null)
ThreadPool.QueueUserWorkItem(new WaitCallback(notify), e);
}
}
}
The owner of the subscriber is a client, which initiates the subscriber,
activating and deactivating its subscription in the Event System and
post-processing of the publisher notifications. The subscriber is �wired� with
the client using the delegate technique. The publisher notification
post-processing in the client represents its business orientation such as
monitoring tool, logging, etc.
Tester
The tester is a very simple windows form client to test a COM+ LCE
functionality of the about components and showing its usage. There are two
independent parts: publisher and subscriber. The client initiates the subscriber
and publisher in its ctor:
public Form1()
{
InitializeComponent();
subLog = new SubscriberLog();
pubLog = new PublisherLog();
eventhandler = new SubscriberLogNotify(OnSubscriberLogNotify);
}
private void OnSubscriberLogNotify(object e)
{
listBoxSubscriber.Items.Add(e.ToString());
}
The user interface is very simply solution with two buttons:
subscribe/unsubscribe and FireEvent
. The first one is used to
activate or deactivate the subscription in the Event System and registry
delegate in the subscriber event class:
private void buttonSubscriber_Click(object sender, System.EventArgs e)
{
try
{
if(buttonSubscriber.Text == "subscribe")
{
subLog.notify += eventhandler;
subLog.Activate();
buttonSubscriber.Text = "unsubscribe";
listBoxSubscriber.Items.Clear();
}
else
{
subLog.notify -= eventhandler;
subLog.Deactivate();
buttonSubscriber.Text = "subscribe";
}
}
catch(Exception ex)
{
string strText = ex.GetType() + ":" + ex.Message;
Trace.Assert(false, strText,
"Caught exception at buttonSubscriber_Click");
}
}
The other one is to �fire event� � publishing a log message to the Event
System.
private void buttonFireEvent_Click(object sender, System.EventArgs e)
{
pubLog.WriteLog(textBoxMessage.Text);
}
Clicking on the �FireEvent� button you will see this message in the active
subscriber list box. You can open many Tester applications and play with their
subscribers and publishers. Each subscriber has own subscription (random guid),
so closing either application or �unsubscribe� will not have any impact to
others.
Conclusion.
Using the publisher/subscriber system notification supported by the COM+
Event system service in the .Net Application is very useful for any phase of the
product. It will allow to incrementally building a product and its tools in the
pyramid fashion. Attributing the Event class, its assembly and using the COM
interop layer is very straightforward and making your .Net implementation
lightly and understandable.