4

We are currently in the process of moving a system to use WCF and ran into an issue that we can't figure out. The setup is there is a C# DLL file that wraps a C++ and a Visual Basic 6.0 DLL file. The C# DLL file has wrappers for both of these, and instantiates both objects. The C++ object is initialized (grabs data from files) and is then passed to a Visual Basic 6.0 object, that runs a report using the data in the C++ object. This is all happening as a WCF Service Application, and for the most part it works great, but when the Visual Basic 6.0 code calls a method in the C++ object, the whole thing hangs.

I tested out using just a simple application that calls the same C# DLL file (outside of WCF), and it works flawlessly. So, there is something going on with WCF and that C++ DLL file, but we can't figure out what. I've changed the Visual Basic 6.0 DLL file to use Run Unattended and Store in Memory (to be able to use it threaded), but that doesn't seem to matter.

Has anyone had any experience with this, or have any thoughts on why it would be hanging? My thought is that the WCF service is somehow locking the DLL file, and that's why when the Visual Basic 6.0 DLL file uses it, it can't access it, which causes it to deadlock.

C++ Wrapper

    public interface ISummaryWrapper
    {
        void LoadInfo(Application info);
        SummaryApp GetSummary();
    }

    public class SummaryWrapper : ISummaryWrapper
    {
        private SummaryApp _summary;

        public SummaryWrapper()
        {
            _summary = new SummaryApp();
        }

        public SummaryWrapper(Application info)
        {
            _summary = new SummaryApp();
            LoadInfo(info);
        }

        public void LoadInfo(Application info)
        {
            _summary.Initialize(info);
        }

        public SummaryApp GetSummary()
        {
            return _summary;
        }
    }

The info object contains information on what the Summary object needs to generate. It's only used in the Initialize method.

The Visual Basic 6.0 object is loaded through an interface:

public void LoadPageObject(Application info)
{
    _pageInfo = new PageInformation();
    _pageInfo.oInfo = info;
    _pageInfo.oSummary = _summary;
}

So now the Visual Basic 6.0 object PageInformation has the summary object.

Next, we call the method to generate the report:

_pageInfo.BuildReport();

This goes inside the Visual Basic 6.0 DLL file, and at the point where the code tries to use the summary object, it hangs

// Omitted actual params for brevity, though all the params exist
double value = oSummary.GetData(string parm1, string parm2)

If I use this same call in C#, it pulls back the data just fine.

double value = _summary.GetData(string parm1, string parm2);

Again, when I use this wrapper outside of WCF, it goes through the code fine. It's only when it's running in WCF that it hangs.

It seems to be an issue running in MTA, and I'm not sure if a WCF Service Application running on IIS can be set to run in STA. Is this possible?


SOLVED: I found my answer in this Stack Overflow question:

How to make a WCF service STA (single-threaded)

Which lead me to the article XXX.

Basically, I had to create a thread that is set to STA, and run the API (my C# DLL file) in it. Since I am running all of this with TaskFactory (so I can cancel calls, and run multiple requests), it was a little tricky. Now, I still have the ability to run multiple reports at the same time in MTA, but each report is run in STA. Also, I don't lose my cancellation functionality from WCF either.

Here is the code (I have some cleanup to do still):

public class Builder
{
    public string OperationId { get; set; }
    public IServiceCallback CallBack { get; set; }
    public Dictionary<string, CancellationTokenSource> Queue { get; set; }

    public void BuildReport()
    {
        OperationContext context = OperationContext.Current;
        Thread thread = new Thread(
            new ThreadStart(
                delegate
                    {
                        using (OperationContextScope scope = new OperationContextScope(context))
                        {
                            try
                            {
                                CancellationToken token = Queue[OperationId].Token;

                                CallBack.SendStatus(OperationId, Status.Processing);

                                IAPI api = new API(token);

                                api.MessagingEvents += MessageEvent;

                                // Build Report
                                CallBack.SendStatus(OperationId, Status.BuildingReport);
                                if (!api.BuildReport())
                                    return;

                                CallBack.SendStatus(OperationId, Status.Completed);
                            }
                            catch (OperationCanceledException oc)
                            {
                                // Sending this on the method that receives the cancel request, no need to send again
                            }
                            catch (Exception ex)
                            {
                                // May not be able to use callback if it's a Timeout Exception, log error first
                                // TODO: Log Error
                                CallBack.SendMessage(OperationId, MessageType.Error, ex.Message);
                                CallBack.SendStatus(OperationId, Status.Error);
                            }
                            finally
                            {
                                Queue.Remove(OperationId);
                            }
                        }
                    }));
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();
    }
}

And my service calls this via:

// I initialize taskfactory when the service is created, omitting other code for brevity

public void BuildReport(ReportRequest request)
{
    CallBack.SendReportStatus(request.OperationId, Status.Received);
    CancellationTokenSource cancelSource = new CancellationTokenSource();
    Queue.Add(request.OperationId, cancelSource);

    Builder builder = new Builder
    {
        OperationId = request.OperationId,
        CallBack = CallBack,
        Queue = _queue
    };

    _taskFactory.StartNew(builder.BuildReport, cancelSource.Token);
}

I hope this helps anyone else who comes across this problem!

Community
  • 1
  • 1
Ryan Abbott
  • 5,317
  • 7
  • 31
  • 34

1 Answers1

1

VB6 (COM) needs to be run from an STA thread. Your WCF code is probably calling into the VB6 component on one or more MTA threads. I bet your your test (non WCF) app, the one that worked, was a desktop app. You will need to ensure that the VB6 component is not called from arbitrary .NET threads.

tcarvin
  • 10,715
  • 3
  • 31
  • 52
  • Yes, my test app was just a simple winform app. How do I check if it's calling into the VB6 component on MTA threads? – Ryan Abbott Nov 14 '11 at 20:56
  • 1
    System.Threading.Thread.CurrentThread.Apartment (something roughly like that) – tcarvin Nov 14 '11 at 21:14
  • 1
    System.Threading.Thread.CurrentThread.GetApartmentState(), definitely showing MTA. I'm using a TaskFactory in order to allow cancellation on the service, but not sure how I can get the thread to switch to STA? – Ryan Abbott Nov 14 '11 at 21:27
  • 1
    It looks like it is possible in general (http://msdn.microsoft.com/en-us/library/dd997394.aspx) , but in a WCF service it might be tricky to get the plumbing right for this. – tcarvin Nov 14 '11 at 21:52
  • Great article, thank you. At least now I know what the issue is (STA vs MTA). I wonder if since it's running in IIS, is it even possible to have it run in STA? – Ryan Abbott Nov 14 '11 at 22:41
  • Can you spin up a dedicated thread to host your COM component? Does it need to hang around or can you spin it up to service your request? You might want to take a look at COM+ to host you component and call it from your WCF app. – tcarvin Nov 15 '11 at 12:55