How to publish the WSDL at the public domain name in a WCF service hosted in an Azure worker role?

731 Views Asked by At

I host a WCF service in an Azure worker role.
The service is accessible at the public domain name of the cloud services instance (myservice.cloudapp.net), however, the links to the WSDL, and the URLs inside the WSDL use the internal IP address instead, that cannot be accessed from outside. Thus tools like Add service reference and WCFTestClient.exe does not work as smoothly as they should, because they try to access the internal IP address.

I am creating my service with the following code:

RoleInstanceEndpoint endpoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["MyServiceEndpoint"];
string endpointAddr = String.Format( "http://{0}/MyInteropService", endpoint.IPEndpoint );

this.serviceHost = new ServiceHost( typeof( MyInteropService ), new Uri( endpointAddr ) );

BasicHttpBinding binding = new BasicHttpBinding();

serviceHost.AddServiceEndpoint( typeof( IMyInteropService ), binding, "" );

ServiceMetadataBehavior smb = new ServiceMetadataBehavior
{
    HttpGetEnabled = true,
    HttpsGetEnabled = true,
};

serviceHost.Description.Behaviors.Add( smb );
//serviceHost.AddServiceEndpoint( ServiceMetadataBehavior.MexContractName, MetadataExchangeBindings.CreateMexHttpBinding(), "mex"); It does not matter whether I include this line or not.

serviceHost.Open();

What am I doing wrong? How should I configure the service to use the public domain name in the WSDL as well?

UPDATE: Peter's answer helped me solve the problem, in my case I only had to add a UseRequestHeadersForMetadataAddressBehavior behavior to the service, after that the WSDL used the public domain name (I guess now it uses the domain the client is sending the request to).

So the complete working code is the following:

RoleInstanceEndpoint endpoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["MyServiceEndpoint"];
string endpointAddr = String.Format( "http://{0}/MyInteropService", endpoint.IPEndpoint );

this.serviceHost = new ServiceHost( typeof( MyInteropService ), new Uri( endpointAddr ) );

BasicHttpBinding binding = new BasicHttpBinding();

serviceHost.AddServiceEndpoint( typeof( IMyInteropService ), binding, "" );

ServiceMetadataBehavior smb = new ServiceMetadataBehavior
{
    HttpGetEnabled = true,
    HttpsGetEnabled = true,
};

serviceHost.Description.Behaviors.Add( smb );

// This part solved the problem.
var requestHeaderBehavior = new UseRequestHeadersForMetadataAddressBehavior();
this.serviceHost.Description.Behaviors.Add(requestHeaderBehavior);

serviceHost.Open();
1

There are 1 best solutions below

2
On BEST ANSWER

Edit: my situation was complicated a bit by the security binding I believe, it may be a bit easier for you

Okay, so there's a few issues here. I'll see if I can remember them all correctly.

Firstly, to expose the endpoints in the manner that you wish you don't actually have the correct permissions to do so (even if you run in an elevated context - which you will still need to do). It'll throw an internal error when it tries to register the endpoint.

I had to change the application pool user that did have the rights to do it (it'll be the same credentials as those you use to rdp to the worker roles.

var roleEndpoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["SslEndpoint"];
this.roleProtocol = roleEndpoint.Protocol;
this.rolePort = roleEndpoint.IPEndpoint.Port.ToString();
if (!RoleEnvironment.IsEmulated)
{
      this.roleHostname = "yourexternalhostname.com";
      var appPoolUser = "user";
      var appPoolPass = "password";
      using (var serverManager = new ServerManager())
      {
           string appPoolName = serverManager.Sites[0].Applications.First().ApplicationPoolName;
           var appPool = serverManager.ApplicationPools[appPoolName];
           appPool.ProcessModel.UserName = appPoolUser;
           appPool.ProcessModel.IdentityType = ProcessModelIdentityType.SpecificUser;
           appPool.ProcessModel.Password = appPoolPass;
           appPool.AutoStart = true;
           appPool["startMode"] = "AlwaysRunning";
           appPool.ProcessModel.IdleTimeout = TimeSpan.Zero;
           appPool.Recycling.PeriodicRestart.Time = TimeSpan.Zero;
           serverManager.CommitChanges();
       } 
}
else
{
     this.roleHostname = roleEndpoint.IPEndpoint.Address.ToString();
}

This gave me the ability to configure the service like below, you may need to modify it to your needs. Pay close attention to settings marked important as I believe these are vital in exposing your service.

var clientUrl = new Uri(string.Format("{0}://{1}:{2}/{3}", protocol, ip, port, serviceAddress + clientId));
var contractDescription = ContractDescription.GetContract(typeof(TServiceInterface));
var host = new ServiceHost(typeof(TServiceImplementation), clientUrl);

 var serviceBehaviorAttribute = new ServiceBehaviorAttribute();
 serviceBehaviorAttribute.AddressFilterMode = AddressFilterMode.Any; // Important
 serviceBehaviorAttribute.ConcurrencyMode = ConcurrencyMode.Multiple; 
 serviceBehaviorAttribute.InstanceContextMode = InstanceContextMode.PerCall;
 host.Description.Behaviors.Remove<ServiceBehaviorAttribute>();
 host.Description.Behaviors.Add(serviceBehaviorAttribute);

 var serviceMetadataBehavior = new ServiceMetadataBehavior();
 serviceMetadataBehavior.HttpGetEnabled = false;
 serviceMetadataBehavior.HttpsGetEnabled = true;
 host.Description.Behaviors.Remove<ServiceMetadataBehavior>();
 host.Description.Behaviors.Add(serviceMetadataBehavior);

 var serviceDebugBehavior = new ServiceDebugBehavior();
 serviceDebugBehavior.IncludeExceptionDetailInFaults = true;
 host.Description.Behaviors.Remove<ServiceDebugBehavior>();
 host.Description.Behaviors.Add(serviceDebugBehavior);

 var requestHeaderBehavior = new UseRequestHeadersForMetadataAddressBehavior(); // Important
 host.Description.Behaviors.Remove<UseRequestHeadersForMetadataAddressBehavior>();
 host.Description.Behaviors.Add(requestHeaderBehavior);

host.AddServiceEndpoint(new ServiceEndpoint(
            contractDescription,
            new InternalBinding(),
            new EndpointAddress(clientUrl, EndpointIdentity.CreateX509CertificateIdentity(serviceCertificate))));
host.Open();

Some bits omitted etc. This took a seriously long time to figure out and a lot of grey hairs.