How to Use a Singleton WCF Proxy to Call Different Workflow Service Instances in .NET 3.5?

In .NET 3.5, a new type WorkflowServiceHost is introduced to provide the integration of WF and WCF. On the server-side, ReceiveActivty plays the main role to implement WCF service operations. With this, you can have WCF clients to talk to WF services. Also ContextChannel is introduced to flow the context between the client and the service so that a call to the service from the client can be routed correctly to the right WF instance.

One question that people asked is:

How can we use a single WCF proxy to call different Workflow service instances and flow the context information to the right instances? Is it doable?

Yes, it is doable. By default, the proxy (the context channel) caches the context internally and flows the same context to the service. In order to use the same proxy to flow different contexts, the first step is to disable the context on the IContextManager property of the proxy before it’s opened:

IContextManager contextManager = ((IClientChannel)helloProxy).GetProperty<IContextManager>();

contextManager.Enabled = false;

Now, you can simply attach the context to the ContextMessageProperty property of the outgoing message:

using (OperationContextScope scope = new OperationContextScope( (IContextChannel)helloProxy))

{

    ContextMessageProperty property = new ContextMessageProperty(context);

  OperationContext.Current.OutgoingMessageProperties.Add( "ContextMessageProperty", property);

    helloProxy.Hello("Hello");

}

Below is the sample code that shows how to achieve this. You can see how the client initiates two calls to get two different contexts and then calls the WF service with the contexts interleaved. The sample output is as following:

You said: Hello

You said: Hello 43f5aa31-9b71-4978-9a84-ad19ea727026

You said: Hello

You said: Hello f4962a99-3ec5-4467-8330-a96f0d7aaa21

You said: Hello 43f5aa31-9b71-4978-9a84-ad19ea727026

You said: Hello f4962a99-3ec5-4467-8330-a96f0d7aaa21

You can also store the context data in a file or a database and reconstruct it once you need to connect to the existing WF instance with that context. Here is the code snippet that shows how to reconstruct the context given only the InstanceId (a GUID) data:

// Argument 'contextData' is the InstanceId (a GUID, for example,

// "ad79216c-768f-4f1a-a9b8-3bb6cc512bb4") that you stored earlier

// for the WF instance that you want to reconnect to.

IDictionary<XmlQualifiedName, string> ReconstructContext(string contextData)

{

string contextNamespace = "https://schemas.microsoft.com/ws/2006/05/context";

string contextName = "InstanceId";

XmlQualifiedName xqn = new XmlQualifiedName(contextName, contextNamespace);

Dictionary<XmlQualifiedName, string> context = new Dictionary<XmlQualifiedName, string>();

context.Add(xqn, contextData);

return context;

}

 

Sample code:

namespace UseContextPerCall

{

    using System;

    using System.Collections.Generic;

    using System.Text;

    using System.ServiceModel;

    using System.Workflow.Activities;

    using System.Workflow.Activities.Rules;

    using System.ServiceModel.Channels;

    using System.Collections;

    using System.Xml;

    using System.Workflow.ComponentModel.Compiler;

    using System.Workflow.ComponentModel;

    using System.ServiceModel.Description;

    class Program

    {

        const string url = "https://localhost/foo/bar.svc";

        WorkflowServiceHost serviceHost;

        IHelloService helloProxy;

        ChannelFactory<IHelloService> factory;

        static void Main(string[] args)

        {

            try

            {

                Program p = new Program();

                p.Start();

            }

            catch (Exception ex)

            {

                Exception inner = ex;

                while (inner.InnerException != null)

                    inner = inner.InnerException;

                if (inner is WorkflowValidationFailedException)

                {

                    WorkflowValidationFailedException valE = inner as WorkflowValidationFailedException;

                    for (int i = 0; i < valE.Errors.Count; i++)

                    {

                        Console.WriteLine(valE.Errors[i].ErrorText);

                }

                }

                else

                {

                    Console.WriteLine(inner.ToString());

                }

            }

        }

        void StartService()

        {

            serviceHost = new WorkflowServiceHost(typeof(MyWorkflow), new Uri(url));

            serviceHost.AddServiceEndpoint(typeof(IHelloService), new WSHttpContextBinding(), "");

            ServiceDebugBehavior sdb = serviceHost.Description.Behaviors.Find<ServiceDebugBehavior>();

            sdb.IncludeExceptionDetailInFaults = true;

            serviceHost.Open();

        }

        IDictionary<XmlQualifiedName, string> FirstCall()

        {

            IHelloService proxy = factory.CreateChannel();

            Console.WriteLine(proxy.Hello("Hello"));

            IContextManager contextManager = ((IClientChannel)proxy).GetProperty<IContextManager>();

            IDictionary<XmlQualifiedName, string> context = contextManager.GetContext();

            ((IClientChannel)proxy).Close();

            return context;

        }

        void Call(IDictionary<XmlQualifiedName, string> context)

        {

            using (OperationContextScope scope = new OperationContextScope( (IContextChannel)helloProxy))

            {

                ContextMessageProperty property = new ContextMessageProperty(context);

  OperationContext.Current.OutgoingMessageProperties.Add( "ContextMessageProperty", property);

                foreach (string val in context.Values)

                {

                    Console.WriteLine(helloProxy.Hello("Hello " + val));

                    break;

                }

            }

        }

        void CreateProxy()

        {

            factory = new ChannelFactory<IHelloService>(new WSHttpContextBinding(), new EndpointAddress(url));

            helloProxy = factory.CreateChannel();

            // Disabling default context switching

            IContextManager contextManager = ((IClientChannel)helloProxy).GetProperty<IContextManager>();

            contextManager.Enabled = false;

            ((IClientChannel)helloProxy).Open();

        }

        void Clenaup()

        {

            if (helloProxy != null)

            {

                ((IClientChannel)helloProxy).Abort();

            }

            if (factory != null)

            {

                factory.Abort();

            }

            serviceHost.Abort();

        }

        void Start()

        {

            StartService();

            CreateProxy();

            IDictionary<XmlQualifiedName, string> context = FirstCall();

            Call(context);

            IDictionary<XmlQualifiedName, string> context1 = FirstCall();

            Call(context1);

            // Intermixed

            Call(context);

            Call(context1);

            Clenaup();

        }

    }

    [ServiceContract]

    interface IHelloService

    {

        [OperationContract]

        string Hello(string message);

    }

    public class MyWorkflow : SequentialWorkflowActivity

    {

        public MyWorkflow()

        {

            ReceiveActivity receiveActivity = new ReceiveActivity("HelloReceive");

            receiveActivity.CanCreateInstance = true;

            receiveActivity.ServiceOperationInfo = new TypedOperationInfo(typeof(IHelloService), "Hello");

            CodeActivity codeActivity = new CodeActivity("HelloCode");

            codeActivity.ExecuteCode += new EventHandler(codeActivity_ExecuteCode);

            receiveActivity.Activities.Add(codeActivity);

            WhileActivity whileActivity = new WhileActivity("HelloWhile");

            CodeCondition condition = new CodeCondition();

            condition.Condition += new EventHandler<ConditionalEventArgs>(condition_Condition);

            whileActivity.Condition = condition;

            whileActivity.Activities.Add(receiveActivity);

            // adding binding

            ActivityBind activityBindArg = new ActivityBind("MyWorkflow");

            activityBindArg.Path = "HelloArg1";

            WorkflowParameterBinding bindingArg1 = new WorkflowParameterBinding("message");

            bindingArg1.SetBinding(WorkflowParameterBinding.ValueProperty, activityBindArg);

            ActivityBind activityBindReturn = new ActivityBind("MyWorkflow");

            activityBindReturn.Path = "HelloResult";

            WorkflowParameterBinding bindingResult = new WorkflowParameterBinding("(ReturnValue)");

            bindingResult.SetBinding(WorkflowParameterBinding.ValueProperty, activityBindReturn);

            receiveActivity.ParameterBindings.Add(bindingArg1);

            receiveActivity.ParameterBindings.Add(bindingResult);

            this.Activities.Add(whileActivity);

        }

        public static readonly DependencyProperty HelloArg1Property = DependencyProperty.Register(

         "HelloArg1",

            typeof(string),

            typeof(MyWorkflow),

            new PropertyMetadata(DependencyPropertyOptions.Default));

        public string HelloArg1

        {

            get

            {

                return (string)base.GetValue(MyWorkflow.HelloArg1Property);

            }

            set

            {

                base.SetValue(MyWorkflow.HelloArg1Property, value);

            }

        }

        public static readonly DependencyProperty HelloResultProperty = DependencyProperty.Register(

            "HelloResult",

            typeof(string),

            typeof(MyWorkflow),

            new PropertyMetadata(DependencyPropertyOptions.Default));

        public string HelloResult

        {

            get

            {

                return (string)base.GetValue(MyWorkflow.HelloResultProperty);

            }

            set

            {

                base.SetValue(MyWorkflow.HelloResultProperty, value);

            }

        }

        void condition_Condition(object sender, ConditionalEventArgs e)

        {

            e.Result = true;

        }

        void codeActivity_ExecuteCode(object sender, EventArgs e)

        {

            this.HelloResult = "You said: " + this.HelloArg1;

        }

    }

}

Comments

  • Anonymous
    November 07, 2007
    Yesterday, following my &quot;What's the context for this conversation&quot; presentation, I was approached

  • Anonymous
    November 07, 2007
    Yesterday, following my &quot;What&#39;s the context for this conversation&quot; presentation, I was

  • Anonymous
    November 07, 2007
    Hi Wenlong Wow ... Thanks for the super quick reply! I still have a question though ... In the .NET 3.0 world the reason the asp pages (or any middle tier client) are sharing a singleton is due to the expense of creating a new proxy every time a request comes in (The expense is in creating, securing & setting up a new Channel each time). Using the singleton pattern each 'middle tier client' can invoke the exact same method at the exact same time & still be thread safe because we have a PerCall instancing on the WCF service. If I read your code correctly you are using the same proxy but creating a new Channel (with the relevant Context settings ... a WF Guid) for each 'client' or call in this case. Aren't you still going to hit the same 'expense' in creating the channels ? ...in essence a new 'proxy (aka Channel) is being created for each request. Rob.

  • Anonymous
    November 08, 2007
    I cannot find Enabled property in IContextManager interface.

  • Anonymous
    November 08, 2007
    The IContextManager.Enabled property was added late in the product. You will get it once it's released (soon). Rob, actually the new proxies (also called channel) that I used in the sample code was to cause the WF service to create two new instances with new contexts (GUID). In that way, the client can get the context from new instances. After that, I cached the contexts. Then I use the same cached proxy to communicate with the two different WF instances (with different InstanceIds or GUIDs) on the server-side. You can see the GUIDs printed out. If you already know your WF instance contexts, you don't need to create new proxies (with ContextManager enabled). For example, you can use Persistence Service to save the WF instances into databases. Suppose in your ASP.NET (or any other) applications, you can create a single proxy to talk to those WF instances as long as they exist either in memory or in the database. In my example, for simplicity, I did not use WF Persistence Service and thus I need to create the two WF instances in the memory first so that I could demonstrate how to reuse the same proxy. Hope that this helps!

  • Anonymous
    November 08, 2007
    Hi Wenlong Thanks for that. I'm still a little unsure about what you're saying... Will this work if say 2 separate threads sharing the singleton call the same proxy method at the same time ? (& obviously set the Context details just prior to the proxy method call) Will there not have to be some sort of lock applied so they don't overwrite each others context ? In which case the calls effectivly get queued & we have sequential calling into the WorkflowHostService ? Thanks Rob

  • Anonymous
    November 13, 2007
    Hi Rob, that's a good question! Yes, it should work if two separate threads sharing the singleton proxy call the same method at the same time. This is because the context that is associated with OperationContext.Current is per-thread data. To provide sequencing, you would have to implement the logic in your code on the client-side, no matter whether you use singleton or not. At the same time, to be safer, I also provided a pooling sample that is posted below: http://blogs.msdn.com/wenlong/archive/2007/11/14/a-sample-for-wcf-client-proxy-pooling.aspx. If there is any concern of re-using a proxy concurrently, this would be a better choice. Thanks!

  • Anonymous
    November 16, 2007
    Hi Wenlong I'll try this out. One thing does concern me though is that creating & setting the context data yourself does leave you open to possible 'future issues' in that if MS need to stuff more data in this dictionary object in future for other reasons or extra functionality then It'd be missing Aside from the InstanceId & Guid what else is stored in the IDictionary<XmlQualifiedName, string> collection as part of a call ? Rob.