Introduction
COM+ queued components are a great solution when you need a reliable and transactional mechanism to transfer data in an asynchronous way. However, passing objects in COM+ is not so easy; your objects have to implement the IPersistStream
interface which is cumbersome.
.NET gives object serialization and reliable queuing which is extremely easy to use - but no transactions or other COM+ features.
What I wanted was a combination of both, ease of programming and COM+ features. This already exists in the J2EE world - so I guess it will pop up in the next release of .NET ;-)
In this article I describe a class QCTransferObject
that you can use to transfer any object or object graph as a parameter to a queued component. By subclassing QCTransferObject
you can make it typed.
Background
I based my solution on a combination of two earlier articles in CodeProject:
- How to Pass Managed Objects As a Parameter of a Queued Component
- Remote Logging using .NET Queued Components
The first article describes in detail how to program a class that implements IPersistStream
so that it can be transferred over the wire and used as a parameter of a COM+ queued component. But it requires a lot of work for each different class you need to transfer: for each member object you must write code to convert to and from a byte array. This becomes even more difficult if these member objects contain other objects that also need to be transferred, and so on.
The second article describes a way to serialize any .NET managed object from and to a byte array. This is used to send logging information to a queued component. However, the interface void LogExceptionMessage(string logString, byte[] arrBytes);
still uses the primitive type byte[]
, where I would like objects.
Clearly, combining both of the above techniques provides us with a IPersistStream
capable class that has as a sole member a serializable object. Since the type of this member is Object
it can be anything, even an object graph. Using the byte array serialization and deserialization of the 2nd article this IPersistStream
capable class can (de)serialize this member object in a generic way.
Using the code
The central class, QCTransferObject
.
The class QCTransferObject
is a IPersistStream
capable class with which you can transfer any Serializable object graph to a COM+ queued component written on top of .NET. It is contained in the QCUtil
package that consists of 3 parts:
- The import of the COM
IPersist
and IPersistStream
interfaces.
- The class
QCTransferObject
.
- A
Codec
utility class to convert any serializable object from and to a byte array.
Here is the full code. Your code would only use QCTransferObject
.
using System;
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Serialization.Formatters.Binary;
namespace QCUtil
{
#region Interfaces of the IPersistStream
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("0000010c-0000-0000-C000-000000000046")]
public interface IPersist {
void GetClassID( out Guid pClassID);
};
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("00000109-0000-0000-C000-000000000046")]
public interface IPersistStream : IPersist {
new void GetClassID(out Guid pClassID);
[PreserveSig]
int IsDirty( );
void Load([In] UCOMIStream pStm);
void Save([In] UCOMIStream pStm, [In,
MarshalAs(UnmanagedType.Bool)] bool fClearDirty);
void GetSizeMax(out long pcbSize);
};
#endregion
[Guid("c547e5f2-aa59-4902-a3b3-015ffffcc4bb")]
public class QCTransferObject : IPersistStream {
private Object m_Payload;
public Object payload {
get { return m_Payload; }
set { m_Payload = value; }
}
public QCTransferObject() {}
public override String ToString() {
String s = "QCTransferObject: payload=";
return ((payload == null)? s + "null" : s + payload.ToString());
}
private Guid m_Guid = new Guid(
"c547e5f2-aa59-4902-a3b3-015ffffcc4bb");
protected virtual Guid guid {
get {
Debug.WriteLine(@"QCTransferObject::guid="+m_Guid+"\n");
return m_Guid;
}
}
#region IPersistStream Members
public bool m_bRequiresSave;
public void GetClassID(out Guid pClassID) {
Debug.WriteLine(@"QCTransferObject::GetClassID\n");
pClassID = guid;
}
public int IsDirty() {
Debug.WriteLine(@"QCTransferObject::IsDirty\n");
return m_bRequiresSave ? 0 : -1;
}
public unsafe void Load(UCOMIStream pStm) {
Debug.WriteLine(@"QCTransferObject::Load\n");
Int32 cb;
byte [] arrLen = new Byte[2];
if (null==pStm)
return ;
Int32* pcb = &cb;
pStm.Read(arrLen, 2, new IntPtr(pcb));
cb = 256 * arrLen[1] + arrLen[0];
byte [] arr = new byte[cb];
pStm.Read(arr, cb, new IntPtr(pcb));
payload = Codec.ByteArrayToObject(arr);
}
public unsafe void Save(UCOMIStream pStm, bool fClearDirty) {
Debug.WriteLine(@"QCTransferObject::Save\n");
Int32 cb;
Int32* pcb = &cb;
byte[] arrLen = new byte[2];
byte [] arr = Codec.ObjectToByteArray(payload);
arrLen[0] = (byte)(arr.Length % 256);
arrLen[1] = (byte)(arr.Length / 256);
if (null==pStm)
return ;
pStm.Write(arrLen, 2, new IntPtr(pcb));
pStm.Write(arr, arr.Length, new IntPtr(pcb));
}
public void GetSizeMax(out long pcbSize) {
Debug.WriteLine(@"QCTransferObject::GetSizeMax\n");
byte [] arr = Codec.ObjectToByteArray(payload);
pcbSize = arr.Length +2;
}
#endregion
}
public class Codec {
public static byte[] ObjectToByteArray(Object obj) {
if(obj == null)
return null;
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, obj);
return ms.ToArray();
}
public static Object ByteArrayToObject(byte[] arrBytes) {
MemoryStream memStream = new MemoryStream();
BinaryFormatter binForm = new BinaryFormatter();
memStream.Write(arrBytes, 0, arrBytes.Length);
memStream.Seek(0, SeekOrigin.Begin);
Object obj = (Object) binForm.Deserialize(memStream);
return obj;
}
}
}
Example COM+ Queued Component.
The example simulates a Shipping department, accepting a QCTransferObject
that contains a single OrderDTO
objects with aggregated OrderLineDTO
objects.
The OrderDTO
and OrderLineDTO
objects are simple, [Serializable]
objects as shown below.
[Serializable]
public class OrderDTO {
private int m_Id;
private ArrayList m_OrderLines;
...
public override String ToString() {...}
}
[Serializable]
public class OrderLineDTO {
private int m_ProdRef;
private String m_ProdDesc;
...
public override String ToString() {...}
}
The queued component IShipOrderRequestHandler
justs calls ToString()
on the received object and outputs that to the application log. (I'm not yet familiar with .NET tracing facilities - sorry for that). Note that it is a transactional component, with commit after each invocation (the [AutoComplete]
attribute).
using System;
using System.EnterpriseServices;
using System.Diagnostics;
using QCUtil;
using Orders;
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationQueuing(Enabled=true,
QueueListenerEnabled=true, MaxListenerThreads=1)]
namespace QCSolution
{
[InterfaceQueuing(Enabled=true)]
public interface IShipOrderRequestHandler
{
void shipOrderRequestGeneric(QCTransferObject to);
void shipOrderRequest(OrderTransfer ot);
}
[InterfaceQueuing(Interface = "IShipOrderRequestHandler")]
[Transaction]
public class ShipOrderRequestHandler :
ServicedComponent, IShipOrderRequestHandler
{
public ShipOrderRequestHandler() {}
[AutoComplete]
public void shipOrderRequestGeneric(QCTransferObject to) {
EventLog.WriteEntry("ShippingApp",
"QCTranserferObject received, ToString="+
to.ToString(),EventLogEntryType.Information);
}
[AutoComplete]
public void shipOrderRequest(OrderTransfer ot) {
EventLog.WriteEntry("ShippingApp", "OrderTransfer received, order="
+ot.orderDto.ToString(),EventLogEntryType.Information);
}
}
}
There are two interface methods, the second one being a typed version and explained later.
Finally we have a client application that binds to the queue and sends an OrderDTO
with some aggregated OrderLineDTO
objects to the queued component. A console test application just does that.
class Tester
{
public static OrderDTO helperCreateDto(int id) {
OrderDTO order = new OrderDTO(id);
order.ShipTo = "test to";
order.AddOrderLine(new OrderLineDTO(3,"Tandpasta",10,5));
order.AddOrderLine(new OrderLineDTO(1,"Floss draad",7,2));
return order;
}
[STAThread]
static void Main(string[] args)
{
IShipOrderRequestHandler iHandler
= (IShipOrderRequestHandler)Marshal.BindToMoniker(
"queue:/new:QCSolution.ShipOrderRequestHandler");
QCTransferObject to = new QCTransferObject();
OrderDTO orderDto = helperCreateDto(1);
to.payload = orderDto;
iHandler.shipOrderRequestGeneric(to);
Marshal.ReleaseComObject(iHandler);
}
}
Building and Running the application.
There are quite some steps involved. Perhaps there are faster ways, but at least this one works.
For building:
- Load the solution into Visual Studio .NET 2003.
- Make sure that unsafe code is allowed.
- Rebuild the project.
Then, for running:
- Using "Component Services" define a new COM+ server application named
QCSolution
.
- Configure it for queuing as shown below.
- Check in "Computer Management" that the queues are created.
- Using "Component Services" add a new component to QCSolution by pointing to the
QCSolution.dll
file.
- In a command window, change directory to where the dll is located and execute
gacutil -i QCSolution.dll
to register the component so that other components (like a separate OrderManagement component) can use the Shipping service and the QCTransferObject
class.
- Test the application by executing the
Tester
class.
After successful execution of the client check the Application log in "Computer Management". One of the events should say this:
If you do not see this message check the queues. One of the retry queues will contain your message, that will eventually end up in the dead letter queue.
When rebuilding and retrying I noticed some problems. Make sure the QCSolution COM+ application stopped running. Use gacutil -u QCSolution
to deregister the faulty implementation before registering it again. Sometimes you even have to delete the component QCSolution.ShipOrderRequestHandler
and re-register it.
A Typed transfer object.
The void shipOrderRequestGeneric(QCTransferObject to)
operation is not self-documenting because you need to check the documentation (most often the code though ;-) to know that this operation only functions with OrderDTO
objects. In fact things are worse since you can send any object type to the queued component wrapped inside a QCTransferObject
, which can lead to unexpected or erroneous results at runtime.
One solution is to change the interface as follows:
void shipOrderRequest(OrderTransfer to)
and provide an implementation of OrderTransfer
that only wraps OrderDTO
objects and that behaves the same as QCTransferObject
otherwise.
As the code below shows this can be done very easy through subclassing.
[Guid("12e0bcf4-21a5-4a47-bf02-c010cc2a30ba")]
public class OrderTransfer : QCTransferObject {
public OrderTransfer() {}
public OrderDTO orderDto {
get { return (OrderDTO)payload; }
set { payload = value; }
}
private Guid m_Guid = new Guid(
"12e0bcf4-21a5-4a47-bf02-c010cc2a30ba");
protected override Guid guid {
get {
Debug.WriteLine(@"OrderTransfer::guid="+m_Guid+"\n");
return m_Guid;
}
}
}
Just make sure you generate a unique guid for your own classes, using uuidgen
.