4

I wanted a discoverable service that would listen on all interfaces and publish discovery announcements for each interface. I was hoping to be able to eventually just configure this in the config file using tcp://0.0.0.0:0/blah as the service endpoint. But when I run the code below, the announcements that it sends out use tcp://0.0.0.0:0/blah as the EndpointAddress which is useless to clients.

I want to receive announcements for every endpoint it derived from tcp://0.0.0.0:0/blah and I would prefer to use a config file and not a programmatic service host setup like below. Any ideas for a workaround?

    [TestFixtureSetUp]
    public void SetUp()
    {
        service1 = new MyContract();
        EndpointDiscoveryBehavior discoveryBehavior = new EndpointDiscoveryBehavior();
        ServiceDiscoveryBehavior serviceDiscoveryBehavior = new ServiceDiscoveryBehavior(discoveryUri);
        serviceDiscoveryBehavior.AnnouncementEndpoints.Add(new UdpAnnouncementEndpoint(announcementUri));

        serviceHost1 = new ServiceHost(service1,
            new Uri[] {new Uri("net.pipe://localhost"), new Uri("net.tcp://0.0.0.0:0")});
        ServiceEndpoint localEndpoint1 = serviceHost1.AddServiceEndpoint(typeof (IContract),
            new NetNamedPipeBinding(),
            "/Pipe");
        ServiceEndpoint localEndpoint2 = serviceHost1.AddServiceEndpoint(typeof (IContract),
            new NetTcpBinding(),
            "/Tcp");
        localEndpoint2.Behaviors.Add(discoveryBehavior);
        serviceHost1.Description.Behaviors.Add(serviceDiscoveryBehavior);
        serviceHost1.AddServiceEndpoint(new UdpDiscoveryEndpoint(discoveryUri));

        serviceHost1.Open();
    }
insipid
  • 3,238
  • 3
  • 26
  • 38
  • Can you make your example more clear? What's the point of serviceHost2 registering same IContract on the same port as 1st service? What's 'discoveryBehaviour', 'serviceDiscoveryBehaviour', etc... – Nenad Jun 05 '13 at 20:34
  • @Nenad Edited for clarity. The extra service host was for testing something not relevant to the problem so I removed it. discoveryUri and announcementUri are whatever you want to use. You just can't make them the same because of a WCF network storm bug http://support.microsoft.com/kb/2777305. :) – insipid Jun 05 '13 at 21:20
  • Hey, @insipid, considered looking at my answer? I'm using it to great success in my own program. – Alex May 05 '15 at 08:28

2 Answers2

2

While my solution may not be "correct", strictly speaking (this should really be fixed in WCF itself, if you ask me), it works, and is sufficient for my purposes.

First, declare a new endpoint behavior, like so:

public class WcfDiscoveryAddressFixEndpointBehavior : IEndpointBehavior, IDispatchMessageInspector
{
    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        // Attach ourselves to the MessageInspectors of reply messages
        clientRuntime.CallbackDispatchRuntime.MessageInspectors.Add(this);
    }

    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        object messageProperty;
        if (!OperationContext.Current.IncomingMessageProperties.TryGetValue(RemoteEndpointMessageProperty.Name, out messageProperty)) return null;
        var remoteEndpointProperty = messageProperty as RemoteEndpointMessageProperty;
        if (remoteEndpointProperty == null) return null;

        // Extract message body
        string messageBody;
        using (var oldMessageStream = new MemoryStream())
        {
            using (var xw = XmlWriter.Create(oldMessageStream))
            {
                request.WriteMessage(xw);
                xw.Flush();
                messageBody = Encoding.UTF8.GetString(oldMessageStream.ToArray());
            }
        }

        // Replace instances of 0.0.0.0 with actual remote endpoint address
        messageBody = messageBody.Replace("0.0.0.0", remoteEndpointProperty.Address);

        // NOTE: Do not close or dispose of this MemoryStream. It will be used by WCF down the line.
        var newMessageStream = new MemoryStream(Encoding.UTF8.GetBytes(messageBody));
        XmlDictionaryReader xdr = XmlDictionaryReader.CreateTextReader(newMessageStream, new XmlDictionaryReaderQuotas());

        // Create a new message with our modified endpoint address and
        // copy over existing properties and headers
        Message newMessage = Message.CreateMessage(xdr, int.MaxValue, request.Version);
        newMessage.Properties.CopyProperties(request.Properties);
        newMessage.Headers.CopyHeadersFrom(request.Headers);
        request = newMessage;
        return null;
    }

    public void BeforeSendReply(ref Message reply, object correlationState)
    {
    }

    public void Validate(ServiceEndpoint endpoint)
    {
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
    }
}

This endpoint behavior replaces the original WCF Discovery reply message with a copy in which instances of 0.0.0.0 have been replaced with the address from which the message was received, available in RemoteEndpointMessageProperty's Address property.

To use it, just add the new endpoint behavior to the UdpDiscoveryEndpoint when you're creating the DiscoveryClient:

var udpDiscoveryEndpoint = new UdpDiscoveryEndpoint();
udpDiscoveryEndpoint.EndpointBehaviors.Add(new WcfDiscoveryAddressFixEndpointBehavior());
_discoveryClient = new DiscoveryClient(udpDiscoveryEndpoint);

// Proceed as usual.
Alex
  • 3,429
  • 4
  • 36
  • 66
  • Too bad I wasnt able to implement this because I'm using ServiceMetadataBehavior. But I like the way you're thinking, but I find it too intrusive to inspect and manipulate all responses. Can you turn this into a solution where on each request you check if the endpoint hostname is equal to the hostname used by the client, and if not just change the endpoint hostname? – nl-x Aug 24 '16 at 21:23
  • What about 'Announcement'? How can we add similar fixed-behavior to an announcement service? – Chandler Jun 30 '17 at 07:53
  • 1
    I found the solution. Add a new endpoint behavior to AnnouncementService. In the new endpoint behavior, implement ApplyDispatchBehavior instead of ApplyClientBehavior in Alex's WcfDiscoveryAddressFixEndpointBehavior. – Chandler Jul 06 '17 at 01:38
1

I found another solution that works without altering messages.

I noticed that an endpoint has an addressand a listening address. The idea is to create an endpoint for each ip address and share only one listening address (0.0.0.0 or use the machine name to get also ipv6).

On the discovery side, all the address will be received, someone could try to connect to find which one of them is reachable.

Ther server part looks like this:

var listenUri = new Uri("net.tcp://<Environment.MachineName>:<port>/IServer";
var binding = new NetTcpBinding(SecurityMode.None);
foreach (ip address)
{ 
   var addr = new Uri("net.tcp://<ip>:<port>/IServer");
   Host.AddServiceEndpoint(typeof(IService), binding, addr, listenUri)
}
Hichem
  • 11
  • 1