-1

The following article explains how to build an simple WCF service with Azure Relay Service Bus: https://azure.microsoft.com/en-us/documentation/articles/service-bus-dotnet-how-to-use-relay/ The example shows the use with TCP Binding. I reproduced it and it works flawless. Now I want the same with webHttpRelayBinding but it does not work as expected. I split the code the service in a common dll, a WCFServiceWebRole project, and command line host (as alternative to web.config) and clients :

common dll with interface definition and settings file with bus key, namespace and protocol (tcp or http) namespace WCFRelayCommon { using System.ServiceModel; using System.ServiceModel.Web;

    [ServiceContract(Namespace = "urn:ps")]
    public interface IProblemSolver
    {
        [OperationContract
        WebInvoke(UriTemplate = "/solver", Method = "POST",
            RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Wrapped)]
        int AddNumbers(int a, int b);
    }

    public interface IProblemSolverChannel : IProblemSolver, IClientChannel { }

    public enum BusProtocol { tcp, http };

    public class Utils
    {   
        public static BusProtocol Protocol
        {
            get
            {
                BusProtocol mode;
                if (!Enum.TryParse(AzureSettings.Default.Protocol, out mode))
                {
                    throw new ArgumentException("wrong input, exiting");
                }
                return mode;
            }
        }
    }
}

WCFServiceWebRole project. I first defined all service settings (behavior, biding) in the Web.config and thus should be self sufficient as is.

namespace WCFServiceWebRoleRelay
{
    public class ProblemSolver : WCFRelayCommon.IProblemSolver
    {
        public int AddNumbers(int a, int b)
        {
            return a + b;
        }
    }
}

But I also defined an alternative host project with all settings by code, easier to debug. So just a command line project using the settings file. 2 implementations: either with NetTcpRelayBinding or with WebHttpRelayBinding. with secure transport.

namespace WCFRelayHost
{
    class Program
    {
        static void Main(string[] args)
        {
            var transportClientEndpointBehavior = new TransportClientEndpointBehavior
            {
                TokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider("RootManageSharedAccessKey", AzureSettings.Default.BusKey)
            };

            ServiceHost sh;
            switch (Utils.Protocol)
            {
                case BusProtocol.http:
                    sh = CreateWebHost(AzureSettings.Default.BusNamespace, transportClientEndpointBehavior);
                    break;
                case BusProtocol.tcp:
                    sh = CreateTcpBindingHost(AzureSettings.Default.BusNamespace, transportClientEndpointBehavior);
                    break;
                default:
                    throw new Exception("wrong mode");
            }
            sh.Open();
            Console.WriteLine("Press ENTER to close");
            Console.ReadLine();
            sh.Close();
        }

        static ServiceHost CreateTcpBindingHost(string busNamespace, TransportClientEndpointBehavior transportClientEndpointBehavior)
        {
            Uri tcpAddress = ServiceBusEnvironment.CreateServiceUri("sb", busNamespace, "solver");
            ServiceHost sh = new ServiceHost(typeof(ProblemSolver));            
            var binding = new NetTcpRelayBinding(EndToEndSecurityMode.Transport, new RelayClientAuthenticationType());            
            return AddServiceEndpoint(sh, binding, tcpAddress, transportClientEndpointBehavior);
        }

        static ServiceHost CreateWebHost(string busNamespace, TransportClientEndpointBehavior transportClientEndpointBehavior)
        {
            // https://<namespace>.servicebus.windows.net/solver
            Uri webAddress = ServiceBusEnvironment.CreateServiceUri("https", busNamespace, "solver");
            var binding = new WebHttpRelayBinding(EndToEndWebHttpSecurityMode.Transport, new RelayClientAuthenticationType());
            WebServiceHost wsh = new WebServiceHost(typeof(ProblemSolver), webAddress);
            return AddServiceEndpoint(wsh, binding, webAddress, transportClientEndpointBehavior);
        }

        static ServiceHost AddServiceEndpoint(ServiceHost sh, Binding binding, Uri uri, TransportClientEndpointBehavior transportClientEndpointBehavior)
        {
            sh.AddServiceEndpoint(typeof(IProblemSolver), binding, uri).Behaviors.Add(transportClientEndpointBehavior);            
            return sh;
        }
    }
}

And the client command line app.

namespace WCFRelayClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Enter 2 numbers to add separated by space");
            var line = Console.ReadLine();
            var array = line.Split(' ');
            int first, second;
            if (!int.TryParse(array[0], out first) || !int.TryParse(array[1], out second))
            {
                Console.WriteLine("wrong input, exiting");
            }
            else
            {
                Console.WriteLine("Wait the host to run, press ENTER when ready to send the request");
                Console.ReadLine();

                var transportClientEndpointBehavior = new TransportClientEndpointBehavior
                {
                    TokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider("RootManageSharedAccessKey", AzureSettings.Default.BusKey)
                };

                IProblemSolverChannel ch;
                switch (Utils.Protocol)
                {
                    case BusProtocol.http:
                        ch = WebBinding(AzureSettings.Default.BusNamespace, transportClientEndpointBehavior);
                        break;
                    case BusProtocol.tcp:
                        ch = TcpBinding(AzureSettings.Default.BusNamespace, transportClientEndpointBehavior);
                        break;
                    default:
                        throw new Exception("wrong mode");
                }
                Console.WriteLine(ch.AddNumbers(first, second));
                ch.Dispose();
            }

            Console.ReadLine();
        }

        static IProblemSolverChannel TcpBinding(string busNamespace, TransportClientEndpointBehavior transportClientEndpointBehavior)
        {
            var binding = new NetTcpRelayBinding(EndToEndSecurityMode.Transport, new RelayClientAuthenticationType());            
            var uri = ServiceBusEnvironment.CreateServiceUri("sb", busNamespace, "solver");
            return CreateChannel(binding, uri, transportClientEndpointBehavior);
        }

        static IProblemSolverChannel WebBinding(string busNamespace, TransportClientEndpointBehavior transportClientEndpointBehavior)
        {
            var binding = new WebHttpRelayBinding(EndToEndWebHttpSecurityMode.Transport, new RelayClientAuthenticationType());
            var uri = ServiceBusEnvironment.CreateServiceUri("https", busNamespace, "solver");            
            return CreateChannel(binding, uri, transportClientEndpointBehavior);
        }

        static IProblemSolverChannel CreateChannel(Binding binding, Uri uri, TransportClientEndpointBehavior transportClientEndpointBehavior)
        {
            var cf = new ChannelFactory<IProblemSolverChannel>(binding, new EndpointAddress(uri));
            cf.Endpoint.Behaviors.Add(transportClientEndpointBehavior);
            return cf.CreateChannel();
        }
    }
}

I just have to change the setting parameter to http or tcp to use either webHttpRelayBinding or netTcpRelayBiding

As said, with netTcpRelayBiding, the code run as expected.

With webHttpRelayBinding, I get a InvalidOperationException in mscorlib

System.InvalidOperationException was unhandled
HResult=-2146233079
  Message=Manual addressing is enabled on this factory, so all messages sent must be pre-addressed.
  Source=mscorlib

What did I miss? Maybe some configuration on the Azure Portal? I just followed the instructions from the tutorial...

EricBDev
  • 1,279
  • 13
  • 21
  • In another project, I managed to use webHttpRelayBinding with a GET method, but still not with a POST method. – EricBDev Aug 15 '16 at 09:47

1 Answers1

3

I was able to get your example working in "http" mode by changing WCFRelayClient.Program.CreateChannel method to use WebChannelFactory<T> when dealing with WebHttpBinding/WebHttpRelayBinding:

static IProblemSolverChannel CreateChannel(Binding binding, Uri uri, TransportClientEndpointBehavior transportClientEndpointBehavior)
{
    ChannelFactory<IProblemSolverChannel> cf;
    if (binding is WebHttpBinding || binding is WebHttpRelayBinding)
    {
        cf = new WebChannelFactory<IProblemSolverChannel>(binding, uri);
    }
    else
    {
        cf = new ChannelFactory<IProblemSolverChannel>(binding, new EndpointAddress(uri));
    }

    cf.Endpoint.Behaviors.Add(transportClientEndpointBehavior);
    return cf.CreateChannel();
}

If you are sending with some HTTP Client other than the *HttpRelayBinding and your relay endpoint requires client authentication then you need to build the SAS Token and put the authorization into the HTTP Authorization header.

NodeJs, JAVA, PHP, C# examples and general description of how to build the SAS Token: https://azure.microsoft.com/en-us/documentation/articles/service-bus-sas-overview/

This page appears to have a Javascript example (I haven't verified it works): http://developers.de/blogs/damir_dobric/archive/2013/10/17/how-to-create-shared-access-signature-for-service-bus.aspx

Dave Stucki
  • 121
  • 3
  • Wonderful, the WebChannelFactory was indeed the missing point! – EricBDev Aug 16 '16 at 07:19
  • And obviously, my CreateChannel method does not make so much sense anymore, I put the ChannelFactory call directly in the methods above. But thanks to answer by replacing exactly my wrong method! – EricBDev Aug 16 '16 at 07:29
  • Using WebChannelFactory makes a lot of sense. I was also wondering how the ch.AddNumbers(first, second) method deals with JSON in the http example. Now I understand that it done by the WebChannelFactory! – EricBDev Aug 16 '16 at 07:43
  • I managed to achieve the same with only configuration files: many client endpoint possible but only one service host with full address. – EricBDev Aug 17 '16 at 06:53
  • Next step: if I use only a browser REST client, like ARC/"Advanced Rest Client" in Chrome, how should I "encode" the azure bus key in JSON? Or should I put it in the request uri? – EricBDev Aug 17 '16 at 06:54
  • And yet another challenge: host within the organization network and proxy settings. So far, I've been working on a virtual machine in Azure. Setting the proxy address in the app.config of the host did not help yet to make it run behind our firewalls (in https modus). Any further trick? – EricBDev Aug 17 '16 at 06:57
  • I added instructions above about how to build the SAS token and to put it into the HTTP Authorization head. – Dave Stucki Aug 18 '16 at 23:42
  • To work with an HTTP proxy set `ServiceBusEnvironment.SystemConnectivity.Mode = ConnectivityMode.Http` before opening your relay listener. – Dave Stucki Aug 19 '16 at 18:59