Calling an STA COM Object from a WCF Operation

Note-- This code in this post has been updated on 4-3-2010 to include updates to handle exceptions.

One of the things that many people are still doing is making use of old COM objects that run in STA (single threaded apartment) threads. Back in October, 2006, Jeff Prosise wrote how to do this from ASMX. Not too long after that, I had a chance to teach for Wintellect and Jeff asked me to show him how to do the same in WCF. That information went up on a post for a consulting company that didn’t make it through the latest recession. For better or worse, that post was referenced a fair number of times and now, folks are writing to me, asking for the code again.

In general, any time you receive a message via WCF, the message itself will be processed on a MTA (multi-threaded apartment) thread. Normally, this is just fine. Sometimes, you might be calling out to a COM object. COM objects will not run if the threading model the COM object needs differs from the threading model of the calling thread. MTA COM objects work just fine. But, if you have a bunch of STA COM objects (typically produced by Visual Basic but sometimes coming from C++ applications or other utilities) that you use in your WCF service, you have a problem. To allow things to work, the method needs to be invoked on an STA thread.

In .NET, one creates an STA thread by setting the apartment state of the thread prior to Start()-ing the thread.

thread.SetApartmentState(ApartmentState.STA);

thread.Start();

Now, how do we make this happen in WCF? We need to wrap the invocation of the actual method! For this, we resort to an IOperationBehavior. One typically applies a behavior (IServiceBehavior, IContractBehavior, IOperationBehavior) via an attribute. The only exception to this is an IEndpointBehavior, which is applied via manipulation of the EndpointDescription or in the app|web.config file. The IOperationBehavior exposes four methods:

  1. AddBindingParameters(OperationDescription, BindingParameterCollection): In our case, we will just do nothing with this info.
  2. ApplyClientBehavior(OperationDescription, ClientOperation): Only called when the contract is used on a client. We will  do nothing here since we only care about the service implementation.
  3. ApplyDispatchBehavior(OperationDescription, DispatchOperation): Only called when the contract is used on the server. We will do our work here and override the IOperationInvoker on the DispatchOperation so that our operation is invoked on an STA thread.
  4. Validate(OperationDescription): Used to make sure that everything is OK before applying the behavior. Normally, one throws an exception from this method if the environment isn’t right in some way. Our implementation will throw if the method we are calling is being executed asynchronously. If you need an async version of the attribute, that work is left as an exercise for you, dear reader.

Our IOperationBehavior then looks like this:

public class STAOperationBehaviorAttribute: Attribute, IOperationBehavior

{

    public void AddBindingParameters(OperationDescription operationDescription,

      System.ServiceModel.Channels.BindingParameterCollection bindingParameters)

    {

    }

 

    public void ApplyClientBehavior(OperationDescription operationDescription,

      System.ServiceModel.Dispatcher.ClientOperation clientOperation)

    {

        // If this is applied on the client, well, it just doesn't make sense.

        // Don't throw in case this attribute was applied on the contract

        // instead of the implementation.

    }

 

    public void ApplyDispatchBehavior(OperationDescription operationDescription,

      System.ServiceModel.Dispatcher.DispatchOperation dispatchOperation)

    {

        // Change the IOperationInvoker for this operation.

        dispatchOperation.Invoker = new STAOperationInvoker(dispatchOperation.Invoker);

    }

 

    public void Validate(OperationDescription operationDescription)

    {

        if (operationDescription.SyncMethod == null)

        {

            throw new InvalidOperationException("The STAOperationBehaviorAttribute " +

                "only works for synchronous method invocations.");

        }

    }

}

So far, so good. Now, we need to write that IOperationInvoker. You’ll note from the ApplyDispatchBehavior above that our STAOperationInvoker takes an IOperationInvoker in the constructor. This is done because, in general, the IOperationInvoker WCF gives us does everything right. It just needs to do its thing on an STA thread. Our implementation will delegate as much work as possible to the provided IOperationInvoker. This is a pattern you will follow in most WCF extensions as most extensions require a slight tweak to existing behavior. We do just that, except for our implementation of Invoke. In that, we will setup an STA thread and then call the contained IOperationInvoker’s Invoke method from the STA thread.

public class STAOperationInvoker : IOperationInvoker

{

    IOperationInvoker _innerInvoker;

    public STAOperationInvoker(IOperationInvoker invoker){

        _innerInvoker = invoker;

    }

 

    public object[] AllocateInputs()

    {

        return _innerInvoker.AllocateInputs();

    }

 

 

public object Invoke(object instance, object[] inputs, out object[] outputs)
{
    // Create a new, STA thread
    object[] staOutputs = null;
    object retval = null;
    STAException lastException = null;
    Thread thread = new Thread(
        delegate(){
            try
            {
                retval = _innerInvoker.Invoke(instance, inputs, out staOutputs);
            }
            catch (Exception ex)
            {
                lastException = new STAException(ex);
            }
        });
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    try
    {
        thread.Join();
        if (lastException != null)
        {
            throw lastException;
        }
    }
    catch(Exception e)
    {
        Debug.WriteLine(e.ToString());
        throw;
    }
    outputs = staOutputs;
    return retval;
}

    public IAsyncResult InvokeBegin(object instance, object[] inputs,

      AsyncCallback callback, object state)

    {

        // We don't handle async...

        throw new NotImplementedException();

    }

 

    public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result)

    {

        // We don't handle async...

        throw new NotImplementedException();

    }

 

    public bool IsSynchronous

    {

        get { return true; }

    }

}

To test, I wrote the following Service Contract:

[ServiceContract]

public interface ITestService

{

    [OperationContract]

    string GetApartmentTypeMTA();

 

    [OperationContract]

    string GetApartmentTypeSTA();

}

and applied the attribute to this implementation:

class TestService : ITestService

{

    public string GetApartmentTypeMTA()

    {

        return Thread.CurrentThread.GetApartmentState().ToString();

    }

 

    [STAOperationBehavior]

    public string GetApartmentTypeSTA()

    {

        return Thread.CurrentThread.GetApartmentState().ToString();

    }

}

When calling the service, GetApartmentTypeMTA always returns MTA and GetApartmentTypeSTA always returns STA.

If you want to save some typing time or see the sample application, go here.


Posted 07-17-2009 9:00 AM by Scott Seely
Filed under:

[Advertisement]

About The CodeBetter.Com Blog Network
CodeBetter.Com FAQ

Our Mission

Advertisers should contact Brendan

Subscribe
Google Reader or Homepage

del.icio.us CodeBetter.com Latest Items
Add to My Yahoo!
Subscribe with Bloglines
Subscribe in NewsGator Online
Subscribe with myFeedster
Add to My AOL
Furl CodeBetter.com Latest Items
Subscribe in Rojo

Member Projects
DimeCasts.Net - Derik Whittaker

Friends of Devlicio.us
Red-Gate Tools For SQL and .NET

NDepend

SlickEdit
 
SmartInspect .NET Logging
NGEDIT: ViEmu and Codekana
LiteAccounting.Com
DevExpress
Fixx
NHibernate Profiler
Unfuddle
Balsamiq Mockups
Scrumy
JetBrains - ReSharper
Umbraco
NServiceBus
RavenDb
Web Sequence Diagrams
Ducksboard<-- NEW Friend!

 



Site Copyright © 2007 CodeBetter.Com
Content Copyright Individual Bloggers

 

Community Server (Commercial Edition)