Click here to Skip to main content
15,867,986 members
Articles / Programming Languages / C#
Article

A generic and typed way to transfer .NET objects to COM+ queued components

Rate me:
Please Sign up or sign in to vote.
3.63/5 (7 votes)
21 Apr 20045 min read 50.1K   519   19   2
Article describes a way to pass any .NET managed object as a parameter to a COM+ queued component in an easy way

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:

  1. How to Pass Managed Objects As a Parameter of a Queued Component
  2. 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:

  1. The import of the COM IPersist and IPersistStream interfaces.
  2. The class QCTransferObject.
  3. 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.

C#
using System;
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Serialization.Formatters.Binary;
namespace QCUtil
{
  // Import definitions IPersist and IPersistStream
  #region Interfaces of the IPersistStream
  [InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
    Guid("0000010c-0000-0000-C000-000000000046")]
  public interface IPersist {
    void GetClassID( /* [out] */ 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

  // QCTransferObject can transfer itself as an 
  // object to a serviced COM+ queued component,
  // including its payload which must be a serializable object. 
  // If the payload is a (serializable)
  // object graph the complete graph 
  // is transferred (.NET serialization semantics).
  //[Serializable]
  [Guid("c547e5f2-aa59-4902-a3b3-015ffffcc4bb")]
  public class QCTransferObject : IPersistStream {
    // Payload must be a serializable object or object graph.
    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());
    }
    
    // The guid property is virtual in case you 
    // want to use typed subclasses.
    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; 
      }
    }

    // IPersistStream implementation. 
    // This makes this object go over the wire.
    #region IPersistStream Members
    public bool m_bRequiresSave;
  
    // Class ID will be of this class, 
    // or any subtype if typed transfer is used.
    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;
    }

    // Called on player side (COM+ queued component server),
    // when this object (or subclass object)
    // is instantiated from byte array present in stream.
    public unsafe void Load(UCOMIStream pStm) {
      Debug.WriteLine(@"QCTransferObject::Load\n");
      Int32 cb;
      byte [] arrLen = new Byte[2];
      if (null==pStm)
        return ;
      //read the length of the string;
      Int32* pcb = &cb;
      pStm.Read(arrLen, 2, new IntPtr(pcb));
      //calculate the length.
      cb = 256 * arrLen[1] + arrLen[0];
      //read the stream to get the string.
      byte [] arr = new byte[cb];  // BUG BUG BUG
      pStm.Read(arr, cb, new IntPtr(pcb));

      payload = Codec.ByteArrayToObject(arr);
    }

    // Called on recorder side (client sidd) when this
    // object (or subclass object) is to
    // be sent over the network as a byte array.
    public unsafe void Save(UCOMIStream pStm, bool fClearDirty) {
      Debug.WriteLine(@"QCTransferObject::Save\n");
      Int32 cb;
      Int32* pcb = &cb;
      byte[] arrLen = new byte[2];

      //convert the string into a byte array.
      byte [] arr = Codec.ObjectToByteArray(payload);
      arrLen[0] = (byte)(arr.Length % 256);
      arrLen[1] = (byte)(arr.Length / 256);

      if (null==pStm)
        return ;

      //save the array in the stream
      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);
      //the total size is equal to the length of the string plus 2.
      pcbSize = arr.Length +2;
    }
    #endregion
  }

  // Utility class that serializes any Serializable 
  // object into a byte array and vice versa.
  public class Codec {
    // Convert an object to a byte array
    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();
    }
    // Convert a byte array to an Object
    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.

Image 1

The OrderDTO and OrderLineDTO objects are simple, [Serializable] objects as shown below.

C#
// Order Domain Transfer Object. Serializable. Represents an Order, typically
// used between clients and (ordinary, synchronous) COM+ components.
[Serializable]
public class OrderDTO {
  private int m_Id;
  private ArrayList m_OrderLines;
  ...
  public override String ToString() {...}
}

// OrderLine Domain Transfer Object. Serializable.
// Represents an OrderLine, contained in OrderDTO, 
// as such indirectly
// used between clients and (ordinary, synchronous) 
// COM+ components.
[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).

C#
using System;
using System.EnterpriseServices;
using System.Diagnostics;
using QCUtil;
using Orders;

// Make this a server COM+ application.
[assembly: ApplicationActivation(ActivationOption.Server)]
// Enable queueing and listener, only one 
// thread will be processing object messages.
[assembly: ApplicationQueuing(Enabled=true, 
    QueueListenerEnabled=true, MaxListenerThreads=1)]

namespace QCSolution
{
  // COM+ Queued Component Interface
  [InterfaceQueuing(Enabled=true)]
  public interface IShipOrderRequestHandler
  {
    // Non typed object transfer
    void shipOrderRequestGeneric(QCTransferObject to);
    // Typed object transfer, specific to Orders
    void shipOrderRequest(OrderTransfer ot);
  }

  // COM+ Queued Component Implementation
  [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.

C#
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:

  1. Load the solution into Visual Studio .NET 2003.
  2. Make sure that unsafe code is allowed.
    Image 2
  3. Rebuild the project.

Then, for running:

  1. Using "Component Services" define a new COM+ server application named QCSolution.
  2. Configure it for queuing as shown below.
    Image 3
  3. Check in "Computer Management" that the queues are created.
    Image 4
  4. Using "Component Services" add a new component to QCSolution by pointing to the QCSolution.dll file.
    Image 5
  5. 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.
  6. 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:
Image 6

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.

C#
// Transfer object, containing one OrderDTO 
// with aggregated OrderLineDTO objects,
// so it can be transferred to a COM+ queued component.
[Guid("12e0bcf4-21a5-4a47-bf02-c010cc2a30ba")]
public class OrderTransfer : QCTransferObject {
  public OrderTransfer() {}
    
  // This property makes it typed.
  public OrderDTO orderDto {
    get { return (OrderDTO)payload; }
    set { payload = value; }
  }

  // Every QC Transfer Object must have its own GUID. See also
  // [Guid] attribute higher up.
  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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Belgium Belgium
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionUnable to find Interface Pin
DynamicPages14-Oct-10 2:26
DynamicPages14-Oct-10 2:26 
GeneralGreat minds think alike... Pin
Michael Bouck27-Apr-04 13:31
Michael Bouck27-Apr-04 13:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.