| " Right
now, we are just at the very beginning of a change in great magnitude
in the way people
get, access and exchange information." -- Tom
Brokaw, 2004
I'm a big fan of "building blocks". The whole
idea of quality OOP - oriented programming, to me, is to build reusable
infrastructure pieces that can be assembled in various ways and will
inter operate to produce new business logic that is easy to create, extensible,
and fast. That's why I'm pleased to highlight two of what I consider
to be very high-quality "pieces"
that I've made work together in this manner as a sort of proof of concept
you can use in your own development scenarios. These are the "Perfect
Service" infrastructure produced by J. Ambrose Little, and the
Command
Pattern Invoker, by Brad King.
First, a little history on how and why I've selected these
two pieces for customization, improvement, and utilization. I am working
right now in a heavy-duty telephony environment. Software that runs $30,000
telephony board hardware with commercial customers that need to be up
24/7/365 doesn't have the luxury of being "turned off" while some developer
deploys a revised Windows Service or DLL into production. So obviously,
I did plenty of research on not having to "reinvent the wheel", and found
MVP Ambrose's "Perfect Service" idea was the only implementation out
there that really fit this scenario. So, I took it and added some new
features of my own, and also fixed a bug in the process.
Second, I'm also a fan of interface - based
programming and dynamic invocation of interface-based method calls as
an extensible way to run a "pluggable" business deployment
architecture that is easy to change or add to as business scenarios dictate.
Finally,
I've found that I am using MSMQ
more and more in loosely-coupled, sometimes connected business development.
Hence, the use of Brad King's excellent Command Pattern idea with MSMQ.
MSMQ has matured remarkably in version 3.0, kudos to the people at
Redmond, into a full-scale reliable messaging infrastructure. Until SQL
Server 2005 and Service Broker come out, Message Queue is often the best
solution for this type of need.
Many people don't fully understand the
power of MSMQ until they come to realize that a message can contain
an entire class with properties that have values, with
routing information, and methods
that can be invoked by a calling infrastructure when the message
is received, opened, and deserialized. If the class contained in this
message conforms to a particular GOF Pattern interface, its interface
allows the assembly required to be dynamically loaded and invoked to
perform the unit of business work, with no more knowledge necessary than
the assembly name, method name, and any parameters required. So as a
result, the same dynamic "Invoker" service can handle twenty
different types of operations with 20 different custom assemblies, and
the above three items are all it needs to receive. That's a pretty good
example of extensibility.
Now that we've laid the groundwork, let's take a look
at how the pieces of the infrastructure interrelate:
As can be seen above, Client Applications (lower right)
send instances of the ICommand - derived classes to the Receiver Queue
as MSMQ Binary Formatted messages. If desired, acknowledgement messages
can be deposited by the Receiver Queue into an Acknowledgement Queue
to be picked up and processed by the Client.
The Dynamic Service Manager (upper right), which is the
only actual "real" Windows Service, and can run on any machine in the
network, handles the management of one or more Pluggable Service assemblies
that listen for incoming messages on the Receiver Queue. Whenever a message
arrives, it is deserialized by the Pluggable Service ("Invoker")
class, any routing or other metadata is read, and its ICommand
Execute method is called. In this example case I've added a
simple SQLHelper - assisted method that just inserts notification records
into a SQL Server Notifications table, but your particular implementation(s)
of the ICommand interface can do any type of business logic you want
-- it could send out notification emails, print reports, update web pages,
you name it.
The Dynamic Service Manager Infrastructure (a la "Perfect
Service")
While I am not going to review the Service Manager in great
detail since J. Ambrose Little has two complete articles and downloadable
sample code for his creation at the link above, I will try to summarize
what it does, and the changes I've made to enhance it. The ServiceManager
is a "regular" Windows Service that does basically two things:
1) It creates an instance of the ServiceBroker class.
2) It creates a FileSystemWatcher to monitor File Changed events on the
current directory, for dlls and config files.
The ServiceBroker class maintains HybridDictionaries of
various Services assemblies, their last modified dates, and so on, in
order to dynamically load or unload your custom Pluggable Service assemblies
that derive from the IService interface. It sports a RemoteServiceHandler
that spins these up each into its own AppDomain, loads their "PluggableServiceXXX.dll.config"
configuration files, and starts or stops each service. You never have
to stop the Service Manager service. To start a new pluggable service
(or a revised one or a changed configuration file) you only need to drop
a new copy into the ServiceManager's execution path folder. The infrastructure
takes care of all the rest. To stop a service, just delete the dll. Everything
is shadow-copied, nothing is locked. The ServiceBroker is in charge
of starting your implementation and from there, your "Pluggable
Service" can
be written to do virtually anything you want it to, provided it implements
the simple IService interface, which consists of only two methods, StartService and StopService.
What I have done is add a few enhancements that I felt
were either desirable or necessary:
1) I made the ServiceManager a "self-installing" service
a- la Craig Andera's excellent example code. You only need to run "ServiceManager.exe
/install" or "ServiceManager.exe /uninstall" and you are
good to go. This is relatively easy to do, and it eliminates the need
to rely on that nasty InstallUtil.exe thing.
2) I added an eventing mechanism that enables your service
to be notified if its configuration file has been reloaded; it's not
sufficient just to be able to reload the configuration based on FileSystemWatcher
events; your service may need to do things like change property or other
values and so it must have a way of knowing that it should do this. A
sample implementation of this is shown below:
public event EventHandler NotifyService;
private void ConfigChanged(object source, FileSystemEventArgs e)
{
// wait for any locks to be released
System.Threading.Thread.Sleep(1500);
// reload settings
this.LoadSettings();
this.OnNotifyService();
}
protected void OnNotifyService()
{
if(NotifyService !=null)NotifyService(true,EventArgs.Empty);
}
-- and your service would subscribe to this event like
so:
ServiceBroker.Config cfg = new ServiceBroker.Config();
cfg.NotifyService+=new EventHandler(cfg_NotifyService);
private void cfg_NotifyService(object source, EventArgs
e)
{
// do work here
ServiceBroker.Logger.WriteToLog("Config for Invoker Service was reloaded
at "+DateTime.Now.ToLongTimeString(),
System.Diagnostics.EventLogEntryType.Information);
// reload properties, etc. here if necessary because of config file changing
}
3) There is a fairly well-known bug in the FileSystemWatcher
that makes it fire events more than once based on a single file change event,
which can not only be annoying, but could possibly even mess up your business
logic in some cases, so I added a workaround for this like so:
private string FullPathString = String.Empty;
private DateTime TimeFired;
private void ConfigChanged(object source, FileSystemEventArgs e)
{
// nasty bug in FileSystemWatcher fires twice (in about
4 ms) on changed file. This is a workaround...
if(e.FullPath.ToUpper()==FullPathString && TimeFired.Subtract(DateTime.Now).TotalMilliseconds < 50)
return;
// set the values of the fullpath and time of the event fired to check / prevent
dupe firings
FullPathString =e.FullPath.ToUpper();
TimeFired=DateTime.Now;
// wait for any locks to be released
System.Threading.Thread.Sleep(1500);
// reload settings
this.LoadSettings();
// Trigger our notification event
this.OnNotifyService();
}
MSMQ Command Pattern Invoker Service
The rest of my implementation centers squarely on a best-practices
implementation of the ICommand Command Pattern interface for my MSMQ Invoker
Service, which is what we will cover next.
The ICommand Interface
The key to the Command Pattern is the ICommand
Interface, which
is extremely simple:
using System;
namespace MSMQInvokerService
{
public interface ICommand
{
void Execute();
}
}
Provided that your ICommand - derived Invoker
class implements the above Execute method, it is free to do "whatever
it wants" at that point, and as long as the required assembly is available
to by dynamically invoked, that's really about all you need to know!
My reference implementation has a lot of logging
built in, using the MS Exception Management Application Block (included) so
that if you want to use this as a starting point, you will be seeing plenty
of "stuff" in your Event log:
using System;
using System.Messaging;
using System.Net;
namespace MSMQInvokerService
{
/// <summary>
/// MSMQ Invoker Service using Command Pattern and IService Interface
/// NOTE: AssemblyInfo file MUST have ServiceBroker.ServiceEntryPoint attribute
/// </summary>
public class Invoker : ServiceBroker.IService
{
#region Class-level fields
MessageQueue mqReceive=null;
internal static ServiceBroker.Config AppSettings =new ServiceBroker.Config();
string QueuePath = AppSettings["QueuePath"].ToString();
#endregion
#region IService Members
public void StartService()
{
ServiceBroker.Logger.WriteToLog("HOLA! "+AppSettings["StartText"],
System.Diagnostics.EventLogEntryType.Information);
ServiceBroker.Config cfg = new ServiceBroker.Config();
cfg.NotifyService+=new EventHandler(cfg_NotifyService);
if (!MessageQueue.Exists(QueuePath))MessageQueue.Create(QueuePath);
mqReceive = new MessageQueue(QueuePath);
MessageQueue.EnableConnectionCache = false;
// Control Queue permissions here:
mqReceive.SetPermissions("Everyone",
MessageQueueAccessRights.FullControl, AccessControlEntryType.Allow);
mqReceive.Formatter = new BinaryMessageFormatter();
try
{
IAsyncResult ar = mqReceive.BeginReceive(TimeSpan.FromSeconds(5),
mqReceive,new AsyncCallback(OnMessageArrival));
ServiceBroker.Logger.WriteToLog("BeginReceive Called",
System.Diagnostics.EventLogEntryType.Information);
}
catch (MessageQueueException e)
{
if (e.MessageQueueErrorCode != MessageQueueErrorCode.IOTimeout)
{
ServiceBroker.Logger.WriteToLog(e.Message,System.Diagnostics.EventLogEntryType.Information);
}
}
catch (Exception e)
{
ServiceBroker.Logger.WriteToLog(e.Message,System.Diagnostics.EventLogEntryType.Information);
}
}
private void OnMessageArrival(IAsyncResult ar)
{
MessageQueue mq = (MessageQueue)ar.AsyncState;
ServiceBroker.Logger.WriteToLog("OnMessageArrival Called",
System.Diagnostics.EventLogEntryType.Information);
try
{
Message msg=mq.EndReceive(ar);
ICommand cReceived = (ICommand)msg.Body;
ServiceBroker.Logger.WriteToLog("ICommand Object Casted.",
System.Diagnostics.EventLogEntryType.Information);
cReceived.Execute();
ServiceBroker.Logger.WriteToLog("Executed Command at "+DateTime.Now.ToLongTimeString(),
System.Diagnostics.EventLogEntryType.Information);
}
catch
{
ServiceBroker.Logger.WriteToLog("Timeout!",
System.Diagnostics.EventLogEntryType.Information);
}
finally
{
mq.BeginReceive(TimeSpan.FromSeconds(5),mq,new AsyncCallback(OnMessageArrival));
}
}
private void cfg_NotifyService(object source, EventArgs e)
{
ServiceBroker.Logger.WriteToLog("Config file for Invoker Service was reloaded at " +DateTime.Now.ToLongTimeString(),
System.Diagnostics.EventLogEntryType.Information);
// reload properties, etc. here if necessary because of config file changing
}
public void StopService()
{
ServiceBroker.Logger.WriteToLog(AppSettings["StopText"],
System.Diagnostics.EventLogEntryType.Information);
// Closa dee Q!
mqReceive.Close();
}
#endregion
}
} |
Since the implementation above is more or less self-explanatory, I leave the
reader to walk through the code. As can be seen, I am using a familiar asynchronous
Receive method on my Queue, which calls back to the OnMessageArrival method
to cast my ICommand out of the IASyncResult.AsyncState object:
Message msg=mq.EndReceive(ar);
ICommand cReceived = (ICommand)msg.Body;
This automatically uses a ThreadPool under the hood, and is
generally an efficient way to process large numbers of incoming messages
quickly on reusable background threads, provided that they don't tie up their
threads for too long. Of course, there are many other ways to "skin a
queue".
At this point you've got an ICommand object on which you can call the expected
Execute Method.
Calling the Invoker
Ok, what does the "Invoker" invoke? Well, it invokes an instance
of the LateBoundCommand class:
using System;
using System.Reflection;
namespace MSMQInvokerService
{
/// <summary>
/// Late-Bound Command Pattern Class
/// </summary>
[Serializable()]
public class LateBoundCommand : ICommand
{
protected string AssemblyName;
protected string ClassName;
protected string MethodName;
protected object[] Parameters;
/// <summary>
/// Required no-parameter ctor for serializer
/// </summary>
protected LateBoundCommand()
{
}
// constructor with assembly, class, and method arguments
public LateBoundCommand(string assemblyName, string className, string methodName)
{
AssemblyName = assemblyName;
ClassName = className;
MethodName = methodName;
}
// sets all parameter values
public void SetParameters(params object[] myParameters)
{
Parameters = myParameters;
}
private DateTime executionTime;
public DateTime ExecutionTime
{
get
{
return DateTime.Now;
}
set
{
executionTime=value;
}
}
/// <summary>
/// Implement the ICommand Execute method
/// </summary>
public virtual void Execute()
{
ServiceBroker.Logger.WriteToLog("Attempting Execute" + ExecutionTime.ToLongTimeString(), System.Diagnostics.EventLogEntryType.Information);
try
{
// Load the assembly
Assembly a = Assembly.LoadWithPartialName(AssemblyName);
// Get the class we are interested in from the assembly
Type TargetType = a.GetType(ClassName);
// Get the method we are interested in
MethodInfo TargetMethod = TargetType.GetMethod(MethodName);
// Call the method
TargetMethod.Invoke(null, Parameters);
ServiceBroker.Logger.WriteToLog("LateBoundCommand.Execute complete" + Parameters[0].ToString() + " " +Parameters[1].ToString()+": " +ExecutionTime.ToLongTimeString(),System.Diagnostics.EventLogEntryType.Information);
}
catch(Exception ex)
{
ServiceBroker.Logger.WriteToLog("LateBoundCommand.Execute Exception: " +ex.Message+": " +ExecutionTime.ToLongTimeString(),
System.Diagnostics.EventLogEntryType.Information);
}
}
}
} | The Receiver Class
Finally, once the correct ICommand Object has been dynamically loaded and
its TargetMethod has been invoked, it has to have a CommandReceiver to
do the actual work:
using System;
using System.Data;
using System.Data.SqlClient;
using Microsoft.ApplicationBlocks.Data ;
namespace MSMQInvokerService
{
/// <summary>
/// Sample target class / method to be invoked from ICommand Execute interface
method
/// </summary>
public class CommandReceiver
{
public static void LogMessage(string connectionString, string spName, object[]
parms)
{
SqlHelper.ExecuteNonQuery(connectionString,spName,parms);
ServiceBroker.Logger.WriteToLog("executed: " + spName, System.Diagnostics.EventLogEntryType.Information);
}
}
}
In this case, my CommandReceiver simply calls
the named stored procedure given to it, using the connection string and object
array of procedure parameters passed to it. You can use this type of infrastructure
to pass a Message body that calls any stored procedure on any database, with
any parameters, all with the same infrastructure. In this proof - of - concept
demo app, it just writes the entry into the Notifications table, with an ID,
Name, Email and EntryDateTime, the idea being that this table will be polled,
perhaps, by another service that sends out notifications or some similar work.
Note also that my Receiver class uses the MS Data Application
Blocks "SqlHelper" class
to make the actual process of taking a connection string, a proc name, and
an object array of parameters complete childsplay. The overloads that accept
the object array of parameter values also automatically
cache the SqlParameters to be re-used, based on the passed-in connection string and stored proc name.
That covers the basics of my Self-Updating
Windows Service Infrastructure with Command Pattern Message Queue Invoker
Service, and how to use it. In the downloadable VS.NET 2003 solution
zip file below, you'll find a Readme.txt file that covers all the steps
to build, install, and test this infrastructure all on a single machine.
Once you've gotten that done and seen that it works properly, I encourage
you to "use your noodle" to find new and innovative ways to adapt
and extend the concept for your own business needs.
Download the Visual Studio.NET Solution that accompanies this
article
|