Search code examples
c#wcfazureazure-service-fabric

Exposing WCF/TCP Endpoints from Service Fabric with WS-MetadataExchange for external clients and WcfTestClient


I have a Stateless Service that I am migrating some CloudService WCF Endpoints to. These enpoints are available publicly and require that I am able to discover their properties using WS-MetadataExchange via both References > Add Service Reference from a Visual Studio project as well as from WCFTestClient.

I've followed a few tutorials and have a test endpoint set up:

 <Endpoint Protocol="tcp" Name="WcfServiceEndpoint" Type="Input" Port="8081" />

As well as a service contract

[ServiceContract]
public interface ITestContract
{
    [OperationContract]
    int TestMethod(int value1, int value2);
}

The method:

public class TestService : ITestContract
{
    public int TestMethod(int value1, int value2)
    {
        var result = value1 + value2;
        return result;
    }
}

And a service listener override:

protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
    {

return new[] { new ServiceInstanceListener((context) =>
    new WcfCommunicationListener<ITestContract>(
        this.Context,
        new TestService(),
        WcfUtility.CreateTcpClientBinding(),
        "WcfServiceEndpoint"
    )
)};

In my previous project (that I am migrating from) I set up custom ServiceHost objects and was able to customize their bindings/Urls. I was able to have clients discover the services just fine. I need to be able to expose this service in the same manner so that WcfTestClient as well as References > Add Service Reference can discover their properties using WS-MetadataExchange. As it stands now I don't even have any control over what the service paths are!

Ideally I would like to still use ServiceHost:

var host = new ServiceHost(ITestContract);
host.AddServiceEndpoint(TestService, new NetTcpBinding(SecurityMode.None), "net.tcp://...", new Uri("net.tcp://..."));
host.Description.Behaviors.Remove(typeof(ServiceDebugBehavior));
host.Description.Behaviors.Add(new ServiceDebugBehavior { IncludeExceptionDetailInFaults = true });
host.Open();

UPDATE

Based on the suggestion from VipulM-MSFT (below) I am now creating the communication listener first:

var testCommunicationListener = new WcfCommunicationListener<ITestContract>(
    this.Context,
    new TestService(),
    WcfUtility.CreateTcpClientBinding(),
    "WcfServiceEndpoint"
);

Then modifying the ServiceHost object to allow for MetadataExchange Behavior:

ServiceMetadataBehavior metaDataBehavior = new ServiceMetadataBehavior();
testCommunicationListener.ServiceHost.Description.Behaviors.Add(metaDataBehavior);

As well as allowing for exception details in faults:

testCommunicationListener.ServiceHost.Description.Behaviors.Remove(typeof(ServiceDebugBehavior));
testCommunicationListener.ServiceHost.Description.Behaviors.Add(new ServiceDebugBehavior { IncludeExceptionDetailInFaults = true });

I then create a service endpoint (with my desired path to the service):

testCommunicationListener.ServiceHost.AddServiceEndpoint(typeof(ITestContract), new NetTcpBinding(SecurityMode.None), "net.tcp://localhost:8081/Services/Tests", new Uri("net.tcp://localhost:8081/Services/Tests"));

As well as the MexEndpoint (to allow for MetaExchange binding over TCP):

Binding mexBinding = MetadataExchangeBindings.CreateMexTcpBinding();
testCommunicationListener.ServiceHost.AddServiceEndpoint(typeof(IMetadataExchange), mexBinding, "net.tcp://localhost:8081/Services/Tests/mex", new Uri("net.tcp://localhost:8081/Services/Tests/mex"));

And finally I assign the listener to the Stateless Service:

return new[] { new ServiceInstanceListener((context) => testCommunicationListener)};

When I push this to my local cluster I get the following error:

The Service contains multiple ServiceEndpoints with different ContractDescriptions which each have Name='ITestContract' and Namespace='http://tempuri.org/'. Either provide ContractDescriptions with unique Name and Namespaces, or ensure the ServiceEndpoints have the same ContractDescription instance.

I thought maybe I needed to remove the default endpoint to avoid this collision so I tried:

 testCommunicationListener.ServiceHost.Description.Endpoints.RemoveAt(0);

prior to calling:

testCommunicationListener.ServiceHost.AddServiceEndpoint(typeof(ITestContract), new NetTcpBinding(SecurityMode.None), "net.tcp://localhost:8081/Services/Tests", new Uri("net.tcp://localhost:8081/Services/Tests"));
Binding mexBinding = MetadataExchangeBindings.CreateMexTcpBinding();
testCommunicationListener.ServiceHost.AddServiceEndpoint(typeof(IMetadataExchange), mexBinding, "net.tcp://localhost:8081/Services/Tests/mex", new Uri("net.tcp://localhost:8081/Services/Tests/mex"));

Which gives me the following error:

Replica had multiple failures in API call: IStatelessServiceInstance.Open(); Error = System.NullReferenceException (-2147467261) Object reference not set to an instance of an object. at Microsoft.ServiceFabric.Services.Communication.Wcf.Runtime.WcfCommunicationListener1.b__0(IAsyncResult ar) at System.Threading.Tasks.TaskFactory1.FromAsyncCoreLogic(IAsyncResult iar, Func2 endFunction, Action1 endAction, Task`1 promise, Boolean requiresSynchronization)

I've tried a few other variations with similar results...

I also tried allowing the MetadataExchange behavior on the default bindings (without creating any additional endpoints). But in this scenario how do I know what my endpoints url is? I tried using net.tcp://localhost:8081/TestService (which should be the default) but I cannot connect to this from a console app or WcfTestClient.


UPDATE 2

I was able to host my WCF endpoint as desired by only adding the MetadataExchangeBinding as an additional endpoint and assigning my desired service path within:

Binding mexBinding = MetadataExchangeBindings.CreateMexTcpBinding();
testCommunicationListener.ServiceHost.AddServiceEndpoint(typeof(IMetadataExchange), mexBinding, "net.tcp://localhost:8081/Services/Tests/mex", new Uri("net.tcp://localhost:8081/Services/Tests/mex"));

Solution

  • You can customize the ServiceHost before the communication listener is opened by accessing the host property.

    https://learn.microsoft.com/en-us/dotnet/api/microsoft.servicefabric.services.communication.wcf.runtime.wcfcommunicationlistener-1#Microsoft_ServiceFabric_Services_Communication_Wcf_Runtime_WcfCommunicationListener_1_ServiceHost

    By default the code adds few behaviors including service debug and throttling behavior, so if you remove that the client side stack for WCF provided in the SDK may not function correctly.