Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

WS-Discovery for WCF

4.95/5 (31 votes)
27 Nov 2008CPOL12 min read 141.6K   3.3K  
This article describes the design, implementation, and usage of WS-Discovery for Windows Communication Foundation (WCF).

Introduction

Windows Communication Foundation represents the state of the art communication library in the .NET environment. The flexibility of this device is amazing, and it allows to easily remotize functions on specific services, automatically serializing all the parameters in the signature through the simple decoration of properties and objects.

WCF allows to interconnect with services through web services standard communications or custom communications by simply modifying the configuration file, supporting a huge number of transfer protocols (HTTP, net TCP, MSMQ, ...) and communication protocols (security, reliable message...).

Another major quality of the library is its extensibility: it is possible to create new protocols, behaviors that can be defined at a service level or at an endpoint level.

The implementation of a service or of a service consumer through WCF is based on three concepts that are resumed by ABC: the Address where the service answers, the Binding which is how the service answers, and the Contract which are the methods exposed by the service. This article aims to describe a component which extends WCF in terms of configuration to make a service to be discovered at runtime by a service consumer through the WS-Discovery specification.

Background

WS-Discovery is a specification in the stack of WS-* protocols within the metadata.

StackProtocolli.gif

The WS-Discovery specification foresees that a service is characterized not by its ABC parameters, but only by the Contract and the 'scopes' that are useful information to distinguish two services having the same interface. Each service implements a scheme of scopes called ScopeMatchBy, and eventually, the scopes in URI format (Uniform Resource Identifier). A service can change its metadata, or the scopes themselves, during its life, but it has to keep track of the version through an information called MetadataVersion.

At the opening, the service introduces itself to the network through a multicast Hello message containing the above information. Similarly, at closure, it sends a multicast Bye message to the network. If the metadata or the scopes change, the service must send just a Hello message with the new MetadataVersion number.

The client looks for a service through the type (that is the contract), eventually attaching the information about scope and ScopeMatchBy. The search can be carried out in two ways: without discovery proxy, and with discovery proxy.

Without discovery proxy, the client sends a multicast message called Probe containing the search parameters (contract type, ScopeMatchBy, and scopes).

Probe-ProbeMatch.Png

The figure above shows the multicast communication in red and the unicast communication in blue.

Every service thinking of satisfying the request answers through a unicast ProbeMatch message. If ProbeMatch information is sufficient, the client can connect to the service, asking the service metadata to collect information about the binding first. Otherwise, the client must send a new multicast message called 'Resolve', and the service must answer with a unicast ResolveMatch message, giving necessarily the missing information.

If the discovery proxy is present in the network, the communication takes place with the same messages (Probe, ProbeMatch...), but modes change from multicast to unicast towards the discovery proxy.

To remind myself how communications take place, I introduce an analogy belonging to an environment other than the IT one. I think about several students in their classroom, every time a new student arrives, he enters the room greeting aloud and saying his name (Hello multicast). Suddenly, a student needs Robert, but he doesn’t’ know him. So, he calls Robert aloud in the silent room (Probe multicast). As a consequence, all the people called Robert stand up and go towards the person who called them (ProbeMatch unicast). Well, the discovery proxy represents the teacher in the room: when someone calls Robert aloud, besides all the people called Robert, the teacher stands up, too. He goes to the student and he introduces himself, 'I’m the teacher, and the next time you need someone, come and ask me for (Hello unicast). After that, the student stands up and asks the teacher when he needs someone (Probe unicast).

In this implementation, I won’t consider the version with the discovery proxy, but as soon as I have time, I will extend my code to include the discovery proxy as well, maybe integrating the IStorageAdapter of Roman Kiss’ WS-Transfer.

Using the code

The WS-Discovery implementation for WCF can be divided into two parts (into three parts if the discovery proxy implementation is included): a part concerning the service and the other concerning the client.

The integration of the discovery approach from the client side point of view extending the WCF configuration is problematic because WCF standard proxy works by specifying the Address Binding and Contract, while the discovery proxy needs only the Contract; other information are just discovered at runtime.

Moreover, I didn’t consider the client-side configuration aspect so important. The scopes are optional: if they aren’t specified, all services implementing the contract will satisfy the scopes request, unless particular service configurations are present.

The implementation of a discovery proxy doesn’t need to inherit from a class as per a WCF proxy, but it is enough to declare a DiscoveryClient<TChannel> class. In fact, the Channel property allows to interact with the methods of the remote service.

C#
DiscoveryClient<IServiceSample> proxy = new DiscoveryClient<IServiceSample>();

The constructor foresees to be able to eventually receive the scopes required for the service, and so it begins the search for the required service by sending a Probe message to the network. It is important to have an instance of the proxy as soon as possible so that at the first call of a proxy method, the service to be connected to has already been found avoiding useless waits.

ProbeDiscoveryClient.GIF

When the ProbeMatch message arrives, the client can release the WaitHandle, invoke the remote call, and return the result. If the client receives more than one ProbeMatch, the first one unlocks the semaphore, and at the moment of the call invocation, the best service is found through the virtual method GetBestMemento(). It is probable that at the first call, the first service who sent the ProbeMatch is used, unless a sufficient time went by between instantiation and method invocation to receive all ProbeMatch before the method invocation. Once the client uses a channel, it continues to use it all its life long. By re-creating the client, all the early stored ClientMemento remain, and so GetBestMemento will be able to find the best candidate in a more careful way.

As already said, GetBestMemento is a virtual function, that means that it is possible to inherit the DiscoveryClient<IServiceSample> class and to execute the overridden method to run the custom selection logic.

ClientMemento is the class that represents the basic information to interconnect to the service. Through the data received by the probe, it is possible to instantiate the memento that takes care of getting other possible information, as for instance the binding, through the metadata exposed by the service. If the client and the server use this discovery implementation, a shortcut is present to avoid the metadata roundtrip. The ProbeMatch message foresees the sending of the service address endpoint through the EndpointReference XML structure (see WS-Addressing), which includes not only an address field, but also an extensibility paradigm within the ReferenceParameters field. Within this field, the service enters all information concerning the communication binding. If there is this information, ClientMemento doesn’t need the metadata roundtrip, and it is ready for communication.

A major quality of the client implementation is the AutomaticChangeChannelWhenFaulted property, which allows the fault tolerance property on the services. In fact, when there are several candidate services, GetBestMemento returns a channel towards the chosen service. If the proxy operation fails, the client automatically repeats the same operation towards another candidate at disposal, and it throws the exception only when there is no other candidate on which the operation can be executed. This client functionality is disabled by setting the property to false; as a consequence, the exception would be thrown when other candidates are present as well.

One of the main worries dealing with the client concerned with the management of personalized headers in a message sent using WS-Discovery. WCF allows the addition of personalized headers by the use of the OperationContextScope class, which unfortunately was declared sealed and is therefore inextensible to discovery functionalities.

C#
IMyContract proxy = cf.CreateChannel();
using (new OperationContextScope((IClientChannel)proxy))
{
    OperationContext.Current.OutgoingMessageHeaders.Add(
       MessageHeader.CreateHeader("otherHeaderName",
       "http://otherHeaderNs", "otherValue"));
    Console.WriteLine(proxy.echo("Hello World"));
}

The problem is due to the fact that the DiscoveryClient channel doesn’t implement the IClientChannel interface, at least until when the real channel is created at the end of the discovery process. That is why I had to create a less elegant solution that let nevertheless bypass the problem. I’m talking about the DiscoveryOperationContextScope<TChannel> class that allows, in a similar way, to execute the required operation.

C#
DiscoveryClient<IMyContract> proxy = new DiscoveryClient<IMyContract>();
using (DiscoveryOperationContextScope<IMyContract> os 
        = new DiscoveryOperationContextScope<IMyContract>(proxy))
{
    os.OutgoingHeaders.Add(MessageHeader.CreateHeader("MyHeaderName",
                           "" ,"MyheaderValue"));
    Console.WriteLine("Output string: " + proxy.Channel.GetString("qqq"));
}

The implementation of the service part seems easier, the behavior adds to the static ServiceContext class the information concerning the service. ServiceContext hooks the Opened and Closing events of ServiceHost to send Hello and Bye messages and it continues to listen to at the multicast port to receive the Probe/Resolve messages.

A function linked to the extensibility concerns the dynamic scopes. While implementing the service, I wanted to make the scopes to be dynamically manageable to let them be changed during the service life, reminding always to myself that every metadata change has be notified to the client through a new Hello message.

Configuration of the discoverable service

The part concerning the service foresees that WS-Discovery can be set during the configuration phase through a specific Behavior and an associated extension:

XML
<extensions>
  <behaviorExtensions>
    <add name="serviceDiscoverableBehavior"
      type="Masieri.ServiceModel.WSDiscovery.Behaviors.DiscoveryBehaviorSection,
           Masieri.ServiceModel.WSDiscovery, Version=1.0.0.0, 
           Culture=neutral, PublicKeyToken=18ad931e67d285bd"
    />
  </behaviorExtensions>
</extensions>
<services>
  <service behaviorConfiguration="serviceDiscoverable"
    name="ServiceTest.IServiceSample">
    ...

   </service>
</services>

The discovery functionality setting can therefore be introduced without recompiling the code again but by simply introducing the settings in the configuration file. The associated behavior has to be configured with specific sections:

XML
<behavior name="serviceDiscoverable">
      <serviceMetadata
    httpGetEnabled="true"
    httpGetUrl="http://localhost:8080/Mex" />
  <serviceDiscoverableBehavior
       scopesMatchBy="http://schemas.xmlsoap.org/ws/2005/04/discovery/rfc2396">
     <scopes>
    <add url="http://myscope.tempuri.org/"/>
          </scopes>
  </serviceDiscoverableBehavior>
    </behavior>
</serviceBehaviors>

The ServiceMetadata behavior has to be specified in the ServiceBehavior setting to allow to recover the binding settings through WSDL. Actually, this setting would be unnecessary, if this discovery library were used both for the client and for the server, as a mechanism was developed to avoid the metadata roundtrip and to allow a faster communication. The automatic metadata creation by WCF is very comfortable, but not so fast: it is true that it is created the first time and then kept in memory for the following requests; that is normal in 90% of the cases, but in some architectural cases, I found the services were often instantiated and destroyed with a lot of trouble for me as a consequence. If the WSDL creator had problems, for instance, due to unknown custom protocols (I noticed it in the implementation of the SDK soap.udp), the creation could be slowed down for some seconds.

The service setting through the configuration is done, but it is possible to configure the service in a programmatic way.

Log

The log management was realized in a particularly attentive way to allow a complete integration with the WCF logs to have an exhaustive vision of the communication scenario. By the use of the configuration file, it is possible to add the System.ServiceModel.WSDiscovery source to the System.ServiceModel one and to make the log messages to flow together to a specific listener. The WS-Discovery messages are instead added to the System.ServiceModel.MessageLogging source as all the WCF messages.

XML
<system.diagnostics>
    <sources>
      <source name="System.ServiceModel.WSDiscovery" 
                switchValue="Warning, Error">
        <listeners>
          <add initializeData="InfoServiceDebug.e2e"
          type="System.Diagnostics.XmlWriterTraceListener, 
        System, Version=2.0.0.0, Culture=neutral, 
        PublicKeyToken=b77a5c561934e089"
             name="ServiceModel Listener"
             traceOutputOptions="LogicalOperationStack,  DateTime, 
                                 Timestamp, ProcessId, ThreadId, Callstack" />
        </listeners>
      </source>
      <source name="System.ServiceModel.MessageLogging" 
              switchValue="Warning, Error" >
        <listeners>
          <clear />
          <add type="System.Diagnostics.DefaultTraceListener" name="Default"
                        traceOutputOptions="None" />
          <add initializeData="MessageLog.e2e"
            type="System.Diagnostics.XmlWriterTraceListener, 
                  System, Version=2.0.0.0, Culture=neutral, 
                  PublicKeyToken=b77a5c561934e089"
            name="MessageLogging Listener"
            traceOutputOptions="LogicalOperationStack, DateTime, Timestamp, 
                                ProcessId, ThreadId, Callstack" />
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add type="System.Diagnostics.DefaultTraceListener"
            name="Default" />
    </sharedListeners>
</system.diagnostics>

Log.Png

Points of interest

In this paragraph, I would like to share my architectural experiences regarding the use of WS-Discovery.

Service redundancy

The key-aspect of an architecture based on WS-Discovery is the realisation of service redundancy when possible. Some time ago, I worked on the system of the Italian railway network (RFI). Talking about used solutions, it is as various and heterogeneous as every other system of such a great dimension, but the whole system is controlled through web services. The geographical aspect of the systems, which are located in stations, compartments, and in the RFI CED (data centre) was a peculiarity to be taken in an adequate consideration, because it was unconceivable that a web service could face all the calls coming from the whole country (actually, it is possible, but really expensive!).

In such a scenario, I developed the first version of WS-Discovery to try to completely change the existing model (of course, the sources were completely rewritten and improved in a lot of aspects). Working in a team of people who work well together, we developed a solution that made several clients from the whole territory, not to interface directly with the central web service, but to find run time local providers that gave them the same information. These local providers were actually cache managers pretending to be the required service. If the client needs information coming directly from the source, it can always get it by the use of the correct scope.

Scalar.Png

Fault tolerance

Fault tolerance represents another magic aspect of WS-Discovery. When a client cannot make a call to a service, it can try with another service satisfying the same scopes criteria. This behaviour is obviously settable through the client AutomaticChangeChannelWhenFaulted property.

C#
do
{
    try
    {
      OnInvoking();
      object ret = method.Invoke(_lastUsedChannel, parameters);
      OnInvoked();
      DiscoveryLogger.Info("Method invoked successfully");
      return ret;
    }
    catch (Exception ex)
    {
      DiscoveryLogger.Error("Errore nell'invocazione del servizio", ex);
      lock (_lock)
      {
        //endpoint error
        ClientContext.Current.RemoveDiscoveredEndpoint(
    Helpers.ContractDescriptionsHelper.GetContractFullName<TChannel>(), mem);
        //Look for another one
        mem = GetBestMemento();
        if (mem == null)
        {
          //Start a new probe process for the future
          StartProbeProcess();
          DiscoveryLogger.Warn(
            @"The DiscoveryClient can't scale on another service");

          //Now I can throw the exception
          if (originalException == null)
            throw ex;
          else
            throw originalException;
        }
        //I try again but I store the original exception before
        if (originalException == null)
          originalException = ex;
      }
    }
} while (AutomaticChangeChannelWhenFaulted);

Scopes with quality of service and dynamic change of the system

In another project developed together with the railway society, we studied a solution with dynamic scopes which represented the service QOS (Quality of service) to allow the client to hook the service whose QOS is compatible with its needs, without being too demanding, to avoid blocking all the services with better QOS. By executing the GetBestMemento overload, the client can select the service with the most suitable QOS, while the services update the QOS according to the number of simultaneous users through the MetadataVersion mechanism.

Extension of Roman Kiss’ ESB

WS-Discovery is completely compatible with the implementation of WS-Eventing and WS-Transfer dealt with in Roman Kiss’ article: server-side, by including the discovery Behavior into the service; client-side, by banally using the DiscoveryClient<RKiss.WSEventing.IWSEventing> class, for instance.

Dynamic load balancing

In a recent architecture for video surveillance and scene analyses systems, I implemented a service that can dynamically load some components of scene analyses which allowed, through a SubscriptionManager, to subscribe to the analyzed events. The computational work carried out by these components was high. I thought of integrating them in a server that was sufficiently scalable in several PCs, giving the identifier of the analyzed scene in the scopes. In such a way, the system can optimize the load on several machines without clustering configuration. It can dynamically distribute the load without taking care of the interconnected clients. In the case of dynamic reallocation of a component from a machine to another one, server 1 starts the host preparation process on a new machine (server 2), which sends a hello with the list of scopes of the served component when it is ready. The client, which receives the hello message, files the Memento among those at disposal. Server 1 sends a Hello with a new MetadataVersion version, where the scope of the removed component is no more present, and sends an EndSubscription message to the connected clients automatically, therefore causing the transfer to the new Memento and the new Subscribe on server 2.

EventListener.Png

History

  • October 2008 - First release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)