10

I am trying to set up a Publish/Subscribe system using WCF and where the WCF server is in a Windows service. The binding is net.TCP. The service is providing a "Subscribe" method to the client so the client can register a callback handler to an event that will be raised from a DLL linked to the server. In the Subscribe method I attempt to get the callback channel using the OperationContext.Current.GetCallbackChannel method. When I attempt this the OperationContext.Current property returns NULL.

Can anyone tell me under what circumstances this property would return null?? Have I missed setting something up? I will include the service code and the interface code below. I am using c# in Visual Studio 2012 and targeting framework 4.5.

Service:

namespace WService
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class WcfPublisherService : IWcfPublisherContract
{
    IOALogic logic = new OAControlExample();
    IWcfSubscriberContract _callback = null;

    public void Subscribe()
    {
        _callback = OperationContext.Current.GetCallbackChannel<IWcfSubscriberContract>();
        logic.BarriersChanged += logic_BarriersChanged;
    }

    public void UnSubscribe()
    {
        logic.BarriersChanged -= logic_BarriersChanged;
    }

    void logic_BarriersChanged(object sender, BarriersChangedEventArgs e)
    {
        _callback.BarriersChanged(e.BarrierLines);
    }
}
}

Interface:

namespace WService
{
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IWcfSubscriberContract))]
public interface IWcfPublisherContract
{
    [OperationContract(IsOneWay=false, IsInitiating=true)]
    void Subscribe();
    [OperationContract(IsOneWay = false, IsTerminating=true)]
    void UnSubscribe();
}

public interface IWcfSubscriberContract
{
    [OperationContract(IsOneWay = true)]
    void BarriersChanged(BarrierLines barrierLines);
}
}

Client:

namespace TestClient
{
public partial class Form1 : Form
{
    WcfPublisherService myService
        = new WcfPublisherService();

    ServiceCallback serviceCallback = new ServiceCallback();

    public Form1()
    {
        InitializeComponent();
        serviceCallback.NewMessage += serviceCallback_NewMessage;
    }

    private delegate void serviceCallback_NewMessageDelegate(object sender, NewMessageEventArgs e);
    void serviceCallback_NewMessage(object sender, NewMessageEventArgs e)
    {
        if (textBox1.InvokeRequired)
        {
            textBox1.Invoke(new serviceCallback_NewMessageDelegate(serviceCallback_NewMessage), new object[] {sender, e});
        }
        else
        {
            if (textBox1.Text.Trim().Length > 1)
            {
                textBox1.Text += Environment.NewLine;
            }
            textBox1.Text += e.Msg;
        }
    }

    private void button1_Click(object sender, EventArgs e)
    {
        myService.Subscribe();
    }

    private void button2_Click(object sender, EventArgs e)
    {
        myService.UnSubscribe();
    }
}

[CallbackBehaviorAttribute(UseSynchronizationContext = false)]
class ServiceCallback : IWcfSubscriberContract
{
    public delegate void NewMessageEventHandler(object sender, NewMessageEventArgs e);
    public event NewMessageEventHandler NewMessage;
    protected virtual void OnNewMessage(string msg)
    {
        if (NewMessage != null)
        {
            NewMessage(this, new NewMessageEventArgs(msg));
        }
    }

    public void BarriersChanged(OA.BarrierLines barrierLines)
    {
        OnNewMessage("new barrier lines");
    }
}

public class NewMessageEventArgs : EventArgs
{
    public NewMessageEventArgs(string msg)
    {
        this.Msg = msg;
    }
    public string Msg { get; set; }
}
}

********* New Edit *************** Thanks to SalientBrain's suggestions, I have made considerable changes to my project because I realized the service had to be long running and continually running even if no clients are connected so I changed it to a singleton. Even so, my original problem still persists. SalientBrain has asked to see my config file so I will include it below along with all the other pertinent files. I've stripped it out to conserve space, but I don't think I removed anything important. The error occurs in Subscribe method of the PulisherService class. I hope it is something stupid I did in the config file. Well, here it is:

Config:

    <configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
    <system.serviceModel>
        <behaviors>
            <serviceBehaviors>
                <behavior name="WService.WCFPublisherServiceBehavior">
                    <serviceMetadata httpGetEnabled="false" httpsGetEnabled="false" />
                    <serviceDebug includeExceptionDetailInFaults="false" />
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <services>
            <service behaviorConfiguration="WService.WCFPublisherServiceBehavior"
                name="WService.WcfPublisherService">
                <endpoint address="" binding="netTcpBinding" bindingConfiguration=""
                    name="NetTcpBindingEndpoint" contract="WService.IWcfPublisherContract">
                    <identity>
                        <dns value="localhost" />
                    </identity>
                </endpoint>
                <endpoint address="mex" binding="mexTcpBinding" bindingConfiguration=""
                    name="MexTcpBindingEndpoint" contract="IMetadataExchange" />
                <host>
                    <baseAddresses>
                        <add baseAddress="net.tcp://localhost:8523/Publisher" />
                    </baseAddresses>
                </host>
            </service>
        </services>
    </system.serviceModel>
</configuration>

WcfContracts:

    namespace WService
{
    [ServiceContract(SessionMode = SessionMode.Allowed, CallbackContract = typeof(IWcfSubscriberContract))]
    public interface IWcfPublisherContract
    {
        [OperationContract(IsOneWay=false)]
        void Subscribe(string key);
        [OperationContract(IsOneWay = false)]
        void UnSubscribe(string key);
    }

    public interface IWcfSubscriberContract
    {
        [OperationContract(IsOneWay = true)]
        void BarriersChanged(BarrierLines barrierLines);
    }
}

WcfService:

    namespace WService
{
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
    public class WcfPublisherService : IWcfPublisherContract
    {
        private static WcfPublisherService _instance = null;
        private IOALogic _logic = null;
        private Dictionary<string, IWcfSubscriberContract> _callbacks
            = new Dictionary<string, IWcfSubscriberContract>();
        private ReaderWriterLock _callbacksLock = new ReaderWriterLock();

        private WcfPublisherService() { }

        public static WcfPublisherService TheInstance()
        {
            if (_instance == null)
            {
                _instance = new WcfPublisherService();
            }
            return _instance;
        }

        public void StopWcf()
        {
            _logic.StopRequest();
        }

        public void StartWcf(IOALogic logic)
        {
            _logic = logic;
            _logic.BarriersChanged += logic_BarriersChanged;
            ThreadPool.QueueUserWorkItem(new WaitCallback(StartWork), null);
        }

        public void StartWork(object state)
        {
            _logic.Run();
        }

        public void Subscribe(string key)
        {
            OperationContext context = OperationContext.Current;
            // The above line returns null ***********************************************
            _callbacksLock.AcquireWriterLock(2000);
            if (_callbacksLock.IsWriterLockHeld)
            {
                _callbacks.Add(key, context.GetCallbackChannel<IWcfSubscriberContract>());
                // The above line throws a null execption because context is null  ********
                _callbacksLock.ReleaseWriterLock();
            }
        }

        public void UnSubscribe(string key)
        {
            _callbacksLock.AcquireWriterLock(2000);
            if (_callbacksLock.IsWriterLockHeld)
            {
                _callbacks.Remove(key);
                _callbacksLock.ReleaseWriterLock();
            }
        }

        void logic_BarriersChanged(object sender, BarriersChangedEventArgs e)
        {
            _callbacksLock.AcquireReaderLock(1000);
            if (_callbacksLock.IsReaderLockHeld)
            {
                try
                {
                    foreach (IWcfSubscriberContract callback in _callbacks.Values)
                    {
                        callback.BarriersChanged(e.BarrierLines);
                    }
                }
                finally
                {
                    _callbacksLock.ReleaseReaderLock();
                }
            }
        }
    }
}

WindowsService:

    namespace WService
{
    public partial class WService : ServiceBase
    {
        internal static ServiceHost _serviceHost = null;
        internal static IOALogic _logic = new OAControlExample();

        public WService()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
            if (_serviceHost != null)
            {
                _serviceHost.Close();
            }

            _serviceHost = new ServiceHost(WcfPublisherService.TheInstance());
            WcfPublisherService.TheInstance().StartWcf(_logic);
            _serviceHost.Open();
        }

        protected override void OnStop()
        {
            if (WcfPublisherService.TheInstance() != null)
            {
                WcfPublisherService.TheInstance().StopWcf();
            }
            if (_serviceHost != null)
            {
                _serviceHost.Close();
                _serviceHost = null;
            }
        }
    }
}

TestForm:

    namespace TestClient
{
    public partial class Form1 : Form
    {
        ServiceCallback serviceCallback = new ServiceCallback();

        public Form1()
        {
            InitializeComponent();

            serviceCallback.NewMessage += serviceCallback_NewMessage;
        }

        private delegate void serviceCallback_NewMessageDelegate(object sender, NewMessageEventArgs e);
        void serviceCallback_NewMessage(object sender, NewMessageEventArgs e)
        {
            if (textBox1.InvokeRequired)
            {
                textBox1.Invoke(new serviceCallback_NewMessageDelegate(serviceCallback_NewMessage), new object[] {sender, e});
            }
            else
            {
                if (textBox1.Text.Trim().Length > 1)
                {
                    textBox1.Text += Environment.NewLine;
                }
                textBox1.Text += e.Msg;
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            serviceCallback.Subscribe();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            serviceCallback.Unsubscribe();
        }
    }
}

TestCallbackClass:

    namespace TestClient
{
    [CallbackBehaviorAttribute(UseSynchronizationContext = true)]
    class ServiceCallback : IWcfSubscriberContract
    {
        WcfPublisherService myService
            = WcfPublisherService.TheInstance();
        string callbackKey = Guid.NewGuid().ToString();

        public delegate void NewMessageEventHandler(object sender, NewMessageEventArgs e);
        public event NewMessageEventHandler NewMessage;
        protected virtual void OnNewMessage(string msg)
        {
            if (NewMessage != null)
            {
                NewMessage(this, new NewMessageEventArgs(msg));
            }
        }

        public void Subscribe()
        {
            try
            {
                myService.Subscribe(callbackKey);
            }
            catch (Exception ex)
            {
                OnNewMessage("exception: " + ex.Message);
            }
        }

        public void Unsubscribe()
        {
            try
            {
                myService.UnSubscribe(callbackKey);
            }
            catch (Exception ex)
            {
                OnNewMessage("exception: " + ex.Message);
            }
        }

        public void BarriersChanged(OAInterface.BarrierLines barrierLines)
        {
            OnNewMessage("new barrier lines");
        }
    }

    public class NewMessageEventArgs : EventArgs
    {
        public NewMessageEventArgs(string msg)
        {
            this.Msg = msg;
        }
        public string Msg { get; set; }
    }
}
dtaylor
  • 997
  • 3
  • 10
  • 26
  • you are using nettcpbinding? – SalientBrain Mar 04 '13 at 18:48
  • yes I am using nettcpbinding – dtaylor Mar 04 '13 at 19:07
  • 2
    did you try to play with other InstanceContextMode? – SalientBrain Mar 04 '13 at 19:32
  • Actually I am implementing the "List-Based Publish-Subscribe Pattern" at http://msdn.microsoft.com/en-us/library/ms752254.aspx... In this case the InstanceContextMode must be PerSession because the variable "IWcfSubscriberContract _callback" is defined at the class scope and needs to be a different value for each client. This is kept separate by sessions. Also, if I had the InstanceContextMode any finer than Session, the value would not be remembered between calls. Therefore it must be PerSession. – dtaylor Mar 04 '13 at 19:41
  • Since this is taken directly from the documented pattern, I don't understand why it isn't working. I must have overlooked something. Any other ideas? – dtaylor Mar 04 '13 at 19:42
  • 2
    I am always using InstanceContextMode.Single and ConcurrencyMode = ConcurrencyMode.Multiple without a problem. I am storing callbacks in concurrent dictionary. It allow me to notify clients about events raised by other clients. – SalientBrain Mar 04 '13 at 20:55
  • SalientBrain - thanks for you help. I went back a rewrote my service with your suggestions. Unfortunately, I still have the same error. The InstanceContextMode is Single and the ConcurrencyMode is Multiple. I had to get rid of the _callback class scopped variable and replace it with a Dictionary variable called _callbacks. I also had to add only one handler for my logic.BarriersChanged event and within that handler I now loop through all callbacks in the dictionary and make the callback calls. – dtaylor Mar 05 '13 at 20:45
  • To keep the callbacks straight, my Subscribe method takes a string variable named key which I use as a key in the dictionary. I can then call the Unsubscribe with the same key and remove the correct dictionary entry. I will have to go back and wrap the dictionary access with thread safe stuff, but it should all work now. The error still exists. The first line in the Subscribe(string key) method is: "OperationContext context = OperationContext.current;" After executing this line "context" is null. – dtaylor Mar 05 '13 at 20:46
  • We are back to my original question which was "what are the reasons that this method would return null?". Any other ideas? Anyone else have any ideas? – dtaylor Mar 05 '13 at 20:46
  • SalientBrain, I'll have to enter it in the form of an answer since I've changed the code a lot and it won't fit here. I'm in the middle of cleaning it up so I will enter it shortly. Thanks. – dtaylor Mar 06 '13 at 15:34
  • SalentBrain, please see my edits in the original question after the **** – dtaylor Mar 06 '13 at 17:28
  • 5
    I wasn't totally clear from the code above - are you creating an instance of the service type in the client? Or a WCF proxy/clientchannel? If you're directly creating an instance of the service type and then calling a method on it, there would be no OperationContext. – Ian Gilroy Mar 06 '13 at 17:53
  • 1
    Ian you nailed it!!! I remember when writing it that it felt wrong. As soon as I looked back at it, it was obvious. Now I'm off fighting new dragons. If you will add this as an answer, I will mark it "answered". – dtaylor Mar 06 '13 at 21:01

5 Answers5

23

As discussed in the comments, if you directly create an instance of the service type - as opposed to a WCF proxy/clientchannel - and then you call a method on it, there is no OperationContext. WCF provides an OperationContext instance when your operation is running within a service.

Ian Gilroy
  • 2,031
  • 16
  • 14
  • This was the correct answer. When I created an instance of the service in the client instead of an instance of the proxy, I went merrily on my way calling methods on my service not realizing it was the wrong instance of the service. The only thing that stopped me was the absence of the OperationContext. Thanks again. – dtaylor Mar 07 '13 at 13:33
4

In the client code neither proxy created not channel factory. Service class instance is created as a class library.

You should consume service as below code

 ServiceCallback serviceCallback = new ServiceCallback();
 InstanceContext instanceContext = new InstanceContext(serviceCallback);

 var pubsubProxy = new PubSubProxy.WcfPublisherContractClient(instanceContext);
 pubsubProxy.Subscribe();

And when the service is running, OperationContext is created and you can access OperationContext.Current

Itai Bar-Haim
  • 1,686
  • 16
  • 40
Milan Raval
  • 1,880
  • 1
  • 16
  • 33
  • 2
    Milan - you are correct. Unfortunately, Ian had figured this out in the comments of the question yesterday and I can only mark one answer as "answered". – dtaylor Mar 07 '13 at 13:45
4

I've faced this issue and non of the solutions worked and Most Important thing is if you're using

async await 
OperationContext.Current; will be null

My usage is to get Ip so used it like this before any awaitable call

var clientIpAddress = System.Web.HttpContext.Current?.Request?.UserHostAddress;

After the first await statement in your async service operation, OperationContext.Current could be null because the rest of the method body may be running on a different thread (and OperationContext does not flow between threads

So to get it you can write your code before any awaitable action

May be it'll help someone :)

dnxit
  • 7,118
  • 2
  • 30
  • 34
0

I had a similar issue: in my case when the InstanceContextMode was set to Single the WebOperationContext.Current was null in the constructor. However it was available within the services/classes methods.

d219
  • 2,707
  • 5
  • 31
  • 36
John Meyer
  • 2,296
  • 1
  • 31
  • 39
0

In my case it was me being stupid...

I tried to set

 callback = OperationContext.Current.GetCallbackChannel<IWcfSubscriberContract>();

In the CALLBACK function, instead of the server side funciton... When in callback function - obviously there's no current context.

ephraim
  • 379
  • 1
  • 3
  • 15