Contents
- Introduction
- Usage
Server
config file - Client
config file
- Concept and Design
- Bridge
- Implementation
-
RemoteEventSubscriberLib
-
Virtual Subscriber Proxy
- Test
-
RemoteEventClass
-
RemoteSubscriberObject
- HostServer
- TestClient
- TestPublisher
- Conclusion
Publishing of the application behaviour is a foundational issue of the event
driven distributed architectures. In the COM+ services this feature has been
introduced by the COM+ Event Model. This model represents a loosely coupled
event system based on the Event knowledge stored in the COM+ catalog and using
the Publisher/Subscriber design pattern. The COM+ Services are managed in .Net
Framework using the System.EnterpriseServices.ServicedComponent class,
where the Event Class is created as a managed object located in the COM+ catalog.
The Publisher initiates the Event Class either locally or remotely using the
remoting client proxy mechanism (note that the Event Class is derived from the
MarshalByRefObject class). Firing an event from the managed code is
straightforward and transparently to the unmanaged COM+ Event System. The
situation on the other side, consumer of the events, is slightly different for
distributed Subscribers specially using the Remoting object mechanism. This
article describes how to make a bridge between the COM+ Event Subscription and
remote object using the Custom Remoting Channel mechanism. This Bridge (LCE pseudo
Channel) is created on the fly using the run-time compiling technique. Before
than we will go to its implementation details, let's start it with its usage and
configuration issues. I am assuming that you have a knowledge of the .Net
Remoting and COM+ Event System.
Using the remote object for the distributed COM+ Event Subscriber requires
the following:
- the Remote object has to be derived
from the Event Interface. This interface represents a
contract between the Publisher, Subscriber and Event Class.
- installing the RemoteEventClass assembly into the GAC (abstract
definition of the Event Class and Interface).
- registering the RemoteEventClass into the COM+ catalog using the
utility regsvcs.exe
- installing the
RemoteEventSubscriberLib assembly into the GAC, which it is an implementation
of the LCE pseudo channel.
- configuring the LCE channel as a bridge between the Event Subscription and
remoting object on the client side. Note that this bridge using the specified
channel for remoting object such as http, tcp or custom channel.
Once we configured a bridge (programmatically or administratively), the COM+
Event system will forward the event calls to the remote object via the bridge
using the remoting infrastructure. The loosely coupled event notification is
using the fire&forget design pattern, that's why the remote event method can be
attributed for OneWay. In this case the call is returned immediately back
to the event system without waiting for its completion.
All pieces of the Distributed Event Subscriber are gluing using the
configuration technique on the server and client hosted sides:
Server config file:
The server side requires a standard configuration of the wellknown remoting
object (Singleton or SingleCall) and it might look like the following snippet:
<configuration>
<system.runtime.remoting>
<application name="RemoteEventSubscriber">
<service>
<wellknown mode="Singleton" type="RemoteSubscriberObject.Subscriber, RemoteSubscriberObject"
objectUri="Subscriber" />
</service>
<channels>
<channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" port="12345" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
Client config file:
The client side needs to add additional channel into the
config file using the custom channel properties. The following properties are
available:
Configuring the LCE Channel:
- id specifies an unique name of the channel that can be used
when referring to it (e.g. id="lce")
- type is a full name of the channel that will be loaded (e.g. type="RKiss.RemoteEventSubscriberLib.RemoteEventSubscriber, RemoteEventSubscriberLib"
means a RemoteEventSubscriber class in the assembly RemoteEventSubscriberLib), that's why
assembly has to be installed into the GAC
- ref is the channel template being referenced (e.g. ref="lce")
- name specifies the name of the channel. Each channel has an
unique name which is used for properly message routing (e.g. name="subscription").
Default name is lce.
- priority specifies a preference for the channel. The default
value is 1. Higher numbers mean higher priority. So the messages are queued
based on this number.
- objectType specifies a type of the Event Interface
- objectUrl specifies an endpoint of the remoting object
- eventClass specifies an Event class
- methodName specifies a method name of the Event class, which
is going to be fired
- subscriptionName specifies a name of the subscription
(option)
- subscriptionDesc specifies a description of the subscription
(option)
- subscriptionId specifies a guid of the subscription
(option). The default value is random Guid.
- filterCriteria specifies a subscription filter criteria
(option). The default value is null.
- asyncCall specifies an asynchronously invoking the remote
method call (option). Default value is false.
then the client config file might look like the
following snippet:
<configuration>
<system.runtime.remoting>
<application >
<client>
<wellknown type="RemoteSubscriberObject.Subscriber, RemoteSubscriberObject"
url="tcp://localhost:12345/RemoteEventSubscriber/Subscriber" />
</client>
<channels>
<channel type="RKiss.RemoteEventSubscriberLib.RemoteEventSubscriber, RemoteEventSubscriberLib"
objectType="RKiss.RemoteEventClass.ILogMessage, RemoteEventClass"
objectUrl="tcp://localhost:12345/RemoteEventSubscriber/Subscriber"
subscriptionId="B35204DD-3D49-481d-852D-27D4887672F2"
eventClass="RKiss.RemoteEventClass.RemoteEventClass" methodName="Write2" filterCriteria="e='Hello World'"
asyncCall="true"/>
</channels>
</application>
</system.runtime.remoting>
</configuration>
The above config bridge is creating the transient Event Subscription to fire
a method Write2 when its argument has value Hello World on the
remote object RemoteEventSubscriber using the tcp channel and port
12345.
Another example of the client config file shows how to config to bridges (subscription1
and subscription2):
<configuration>
<system.runtime.remoting>
<application >
<client>
<wellknown type="RemoteSubscriberObject.Subscriber, RemoteSubscriberObject"
url="tcp://localhost:12345/RemoteEventSubscriber/Subscriber" />
</client>
<channels>
<channel ref="lce" name="subscription1"
eventClass="RKiss.RemoteEventClass.RemoteEventClass" methodName="Write" asyncCall="true"/>
<channel ref="lce" name="subscription2"
subscriptionId="B35204DD-3D49-481d-852D-27D4887672F2"
eventClass="RKiss.RemoteEventClass.RemoteEventClass" methodName="Write2" filterCriteria="e='Hello World'" />
</channels>
</application>
<channels>
<channel id="lce" type="RKiss.RemoteEventSubscriberLib.RemoteEventSubscriber, RemoteEventSubscriberLib"
objectType="RKiss.RemoteEventClass.ILogMessage, RemoteEventClass"
objectUrl="tcp://localhost:12345/RemoteEventSubscriber/Subscriber"
subscriptionName="RemoteEventSusbcriber" subscriptionDesc="This is a test" />
</channels>
</system.runtime.remoting>
</configuration>
The concept of using the Remote object for Distributed Event Subscriber is
based on creating a transient subscription for virtual subscriber proxy.
The virtual subscriber proxy represents a remote subscriber and its
responsibility is to delegate events to the actually subscriber in
either synchronously or asynchronously manner. In the following picture this
concept is hidden in the Bridge module. The Bridge
represents a dynamically glue between the Event System and Remoting
infrastructure (transparent proxy of the remote object).
The scenario of the Publisher/Subscriber notification is straightforward and
full transparently. The Publisher fires an event
method published in the COM+ Event System (tightly coupled system). Based on the
registered subscription in the Event Store for this event class, the Event
System invokes a specified event method on the transient subscription using the
late binding design pattern. In this case, the transient subscription is
represented by the Virtual Subscriber Proxy
created and initiated on the fly based on the config custom channel properties.
So, the virtual subscriber proxy is an actually client of the remote subscriber
object using the standard remoting mechanism. Note that this concept solved the
problem of direct using a transparent proxy of the remoting object as a
transient subscriber.
From
the configuration viewpoint, the Bridge is represented by the LCE Channel. This
channel is not a message transport channel, it is only way and mechanism how to
administratively make a plumbing between the Event System and Remoting
infrastructures. Its responsibilities are:
- retrieving the custom channel properties
- creating a proxy of the remoting object
- dynamically creating an assembly of the Virtual Subscriber Proxy (remote
object wrapper)
- creating an instance of the Virtual Subscriber Proxy
- creating LCE transient subscription and its registration into the COM+
Event Store
The key part of the Bridge is a dynamically creating assembly of the Virtual
Subscriber Proxy. The technique (run-time compiling) what I am using in this solution can be used for
another purpose also. It's generic mechanism and I would like to describe more
details about that. The following flowchart shows its internals:
The source code of the Virtual Subscriber Proxy is generated on the fly based
on the config properties such as a name of the Event Method, method arguments,
etc. Then the source code is compiled and its assembly is stored in the memory.
From this assembly we can get a type of required class, which it is necessary
for the Activator.CreateInstance. This process is done only one time
during the Remoting Configuration. Note that assembly could be created using the
Reflextion.Emit namespace classes, but the above technique is more
convenient and readable.
The LCE pseudo channel (Bridge) is implemented in the assembly
RemoteEventSubscriberLib.dll using only references to the .Net namespaces.
Metadata of the Event System unmanaged code such as IEventSystem,
IEventSubscription and IEventSubscription2 have been created manually
and inserted into the assembly. Let's look at in details about this
module:
There is only one class - RemoteEventSubscriber, which is usually called by
the Remoting Configuration with a collection of the custom channel properties,
see the following code snippet:
public RemoteEventSubscriber(){}
public RemoteEventSubscriber(IDictionary properties) : this( properties, null) {}
public RemoteEventSubscriber(IDictionary properties, IServerChannelSinkProvider serverSinkProvider)
{
Connect(properties);
}
The class constructor (except default one) calling the method Connect to
perform all functionality of the Bridge included its registration in the Event
Store:
public string Connect(IDictionary properties)
{
bool asyncCall = false;
string subscriptionName = "RemoteEventSusbcriber";
string subscriptionDesc = "The Bridge between the Remote Object and COM+ Event System";
string filterCriteria = null;
(xxx.exe.config file)
string[] objectType = properties["objectType"].ToString().Split(new char[]{','}, 2);
string objectUrl = properties["objectUrl"].ToString();
string eventClass = properties["eventClass"].ToString();
string methodName = properties["methodName"].ToString();
if(properties.Contains("name"))
m_ChannelName = properties["name"].ToString();
if(properties.Contains("asyncCall"))
asyncCall = Convert.ToBoolean(properties["asyncCall"]);
if(properties.Contains("subscriptionName"))
subscriptionName = properties["subscriptionName"].ToString();
if(properties.Contains("subscriptionDesc"))
subscriptionDesc = properties["subscriptionDesc"].ToString();
if(properties.Contains("subscriptionId"))
m_subscriptionId = properties["subscriptionId"].ToString();
if(properties.Contains("filterCriteria"))
filterCriteria = properties["filterCriteria"].ToString();
Assembly asm = Assembly.Load(objectType[1].TrimStart(new char[]{' '}));
Type interfaceType = asm.GetType(objectType[0]);
object robj = Activator.GetObject(interfaceType, objectUrl);
MethodInfo mi = interfaceType.GetMethod(methodName);
Type trow = typeofROW("RKiss.RemoteObjectWrapper", "Wrapper", mi);
object wpobj = Activator.CreateInstance(trow, new object[]{robj, mi, asyncCall});
IEventSystem es = new EventSystem.EventSystem() as IEventSystem;
IEventSubcription2 sub = new EventSubcription() as IEventSubcription2;
sub.SubscriptionName = subscriptionName;
sub.Description = subscriptionDesc;
sub.SubscriptionID = "{" + m_subscriptionId + "}";
sub.Enabled = true;
sub.EventClassID = "{" + Type.GetTypeFromProgID(eventClass).GUID.ToString() + "}";
sub.MethodName = methodName;
sub.FilterCriteria = filterCriteria;
sub.SubscriberInterface = wpobj;
es.Store("EventSystem.EventSubscription", sub);
return m_subscriptionId;
}
Virtual Subscriber Proxy
This is a key part of the implementation, it allows to dynamically create a
remote event subscriber proxy for the transient subscription. The source code of
the proxy is shown in the following text snippet:
as you can see, the above source code is a wrapper around the remote object
proxy. The method XYZ(...) represents an event method. The name and its
arguments are generated during the run-time using the System.Reflection
namespace classes. When we have an image of the source code done, the rest of
work is performing by CSharpCodeProvider class as it is shown in the
following code snippet:
private Type typeofROW(string ns, string name, MethodInfo mi)
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("namespace {0}", ns);
sb.Append("{using System;using System.Reflection;");
sb.AppendFormat("public class {0}", name);
sb.Append("{object m_obj; MethodInfo m_mi; bool m_async;");
sb.Append("delegate object delegateInvoke(object obj, object[] parameters);");
sb.AppendFormat("public {0} (object obj, MethodInfo mi, bool async)", name);
sb.Append("{m_obj=obj; m_mi=mi; m_async=async;}");
sb.AppendFormat("public void {0}(", mi.Name);
foreach(ParameterInfo pi in mi.GetParameters())
{
sb.AppendFormat("{0} {1} ,", pi.ParameterType.Name, pi.Name);
}
sb.Remove(sb.Length - 1, 1);
sb.Append("){object[] args=new object[]{");
foreach(ParameterInfo pi in mi.GetParameters())
{
sb.AppendFormat("{0},", pi.Name);
}
sb.Remove(sb.Length - 1, 1);
sb.Append("}; if(m_async==true){");
sb.Append("delegateInvoke di=new delegateInvoke(m_mi.Invoke);");
sb.Append("di.BeginInvoke(m_obj, args, null, null);}");
sb.Append("else m_mi.Invoke(m_obj, args);");
sb.Append("}}}");
string srcWrapper = sb.ToString();
CompilerParameters cp = new CompilerParameters();
cp.ReferencedAssemblies.Add("System.dll");
cp.GenerateExecutable = false;
cp.GenerateInMemory = true;
cp.IncludeDebugInformation = false;
ICodeCompiler icc = new CSharpCodeProvider().CreateCompiler();
CompilerResults cr = icc.CompileAssemblyFromSource(cp, srcWrapper);
if(cr.Errors.Count > 0)
{
throw new Exception(string.Format("Build failed: {0} errors", cr.Errors.Count));
}
return cr.CompiledAssembly.GetType(ns + "." + name);
}
The Bridge can be disconnected from the COM+ Event System calling the
following method:
public void Disconnect()
{
int errorIndex = 0;
IEventSystem es = new EventSystem.EventSystem() as IEventSystem;
string strCriteria = "SubscriptionID=" + "{" + m_subscriptionId + "}";
es.Remove("EventSystem.EventSubscription", strCriteria, out errorIndex);
}
I created the following package of the assemblies to test the
RemoteEventSubscriberLib
solution:
- RemoteEventClass.dll
- RemoteSubscriberObject.dll
- HostServer.exe
- TestCient.exe
- TestPublisher.exe
Note the above projects has only a test purpose, to show a usage of the LCE
Channel and its evaluation. Their design and implementation has been simplified:
This is an abstract definitions of the Event Class and Interface.
[assembly: ApplicationID("02385BFA-7A55-4704-9614-9BD15C7154CA")]
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(Value = false, Authentication = AuthenticationOption.None)]
namespace RKiss.RemoteEventClass
{
[Guid("21EF4F1D-85E0-45f2-BFDC-858075327A2B")]
public interface ILogMessage
{
[OneWay]
void Write(object e);
[OneWay]
void Write2(DateTime dt, string e);
}
[Guid("E5A05525-3E0D-408d-9EEA-04827DEFA150")]
[EventClass]
[Transaction(TransactionOption.Disabled)]
[ObjectPooling(Enabled=true, MinPoolSize=4, MaxPoolSize=60)]
[EventTrackingEnabled]
public class RemoteEventClass : ServicedComponent, ILogMessage
{
public void Write(object e)
{
throw new Exception("This is a meta class");
}
public void Write2(DateTime dt, string e)
{
throw new Exception("This is a meta class");
}
}
}
This is a Remote Subscriber object to receive events from the COM+ Event
Subscription.
using RKiss.RemoteEventClass;
namespace RemoteSubscriberObject
{
public class Subscriber : MarshalByRefObject, ILogMessage
{
public Subscriber()
{
Console.WriteLine("Subscriber is ready.");
}
[OneWay]
public void Write(object e)
{
Console.WriteLine(e);
}
[OneWay]
public void Write2(DateTime dt, string e)
{
Console.WriteLine("{0}, {1}", dt, e);
}
}
}
This is a server console program to host the remote subscriber object.
namespace HostServer
{
public class Server
{
public static void Main(string[] args)
{
try
{
RemotingConfiguration.Configure(@"..\..\HostServer.exe.config");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
System.Console.Write("Hit <enter> to exit server...\n");
System.Console.ReadLine();
}
}
}
This is a client console program.
namespace TestClient
{
class Client
{
static void Main(string[] args)
{
try
{
RemotingConfiguration.Configure(@"..\..\TestClient.exe.config");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
System.Console.Write("Hit <enter> to exit client...\n");
System.Console.ReadLine();
}
}
}
This is a console program to fire an event.
using RKiss.RemoteEventClass;
namespace TestPublisher
{
class Publisher
{
static void Main(string[] args)
{
int counter = 0;
while(true)
{
System.Console.Write("Hit <enter> to fire event\n");
System.Console.ReadLine();
try
{
using(RemoteEventClass rec = new RemoteEventClass())
{
rec.Write(string.Format("This is a test #{0}", counter++));
rec.Write2(DateTime.Now, "Hello World");
}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
}
<p2>
Before starting the test the following assemblies have to be installed in the
GAC:
- RemoteEventClass
on the both machine
- RemoteEventSubscriberLib on the client machine
- RemoteSubscriberObject on the server machine
and registering RemoteEventClass
into the COM+ catalog on the client machine, see the following picture:
After all the above checks, start the hostserver console program and then
testclient program. To check the transient subscription in the COM+ Event
Store use the Event Subscription Viewer from my article [1]. It's a very useful
tool for that purpose (viewing transient subscriptions), because there is no
other way to see it.
In this moment we have our test environment ready. Launch
the TestPublisher console program and press Enter key. Each time
you pressed Enter, the Publisher will fire an event and the Subscriber
will notify that
message, see the following picture:
Note that the remote object is hosted on the server as a singleton
wellknown object, that's why we can see a prompt text Subscriber is ready
from its ctor only one time at the beginning. (try to change mode to
singlecall to see differences)
In this article has been described how the Remote object can be plumbed with
the COM+ Event System as a transient subscription. Using the custom remoting
channel feature this can be accomplished administratively, which it makes an
easy deployment in the production environment.
[1] http://www.codeproject.com/useritems/subscriptionviewer.asp
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.