Metadata Exchange

By default, the service will not publish its metadata. However, this does not preclude clients that have obtained the metadata via some other mechanism from invoking operations on the service. A service that does not publish metadata is the WCF analogy to an internal class in .NET.

Publishing your service's metadata involves significant effort, since you have to convert CLR types and binding information into WSDL or some other low-level representation, and all that effort does not add any business value. But fortunately, the host already knows everything there is to know about your service and its endpoints, so it can publish the metadata for you if explicitly instructed to do so.

There are two options for publishing a service's metadata: you can provide the metadata over HTTP-GET, a simple text-based protocol that most platforms support, or you can use a dedicated endpoint (as discussed later).

WCF can provide the metadata for your service over HTTP-GET automatically; all you need to do is enable it by adding an explicit service behavior. Behaviors are described fully in subsequent chapters. For now, all you need to know is that a behavior is a local aspect of the service, such as whether or not it wants to have the host publish its metadata over HTTP-GET. You can add this behavior administratively or programmatically.

Enabling Metadata Exchange Administratively

Example 1-10 shows a host application config file, where both hosted services reference a custom behavior section that enables metadata publishing over HTTP-GET.

Example 1-10. Enabling metadata exchange behavior using a config file

<system.serviceModel>
   <services>
      <service name = "MyService" behaviorConfiguration = "MEXGET">
         <host>
            <baseAddresses>
               <add baseAddress = "http://localhost:8000/"/>
            </baseAddresses>
         </host>
         ...
      </service>
      <service name = "MyOtherService" behaviorConfiguration = "MEXGET">
         <host>
            <baseAddresses>
               <add baseAddress = "http://localhost:8001/"/>
            </baseAddresses>
         </host>
         ...
      </service>
   </services>
   <behaviors>
      <serviceBehaviors>
         <behavior name = "MEXGET">
            <serviceMetadata httpGetEnabled = "true"/>
         </behavior>
      </serviceBehaviors>
   </behaviors>
</system.serviceModel>

By default, the address the clients need to use for HTTP-GET is the registered HTTP base address of the service. If the host is not configured with an HTTP base address, loading the service will throw an exception. You can also specify a different address (or just a URI appended to the HTTP base address) at which to publish the metadata by setting the httpGetUrl property of the serviceMetadata tag:

<behavior name = "MEXGET">
   <serviceMetadata httpGetEnabled = "true" httpGetUrl = "http://localhost:8002"/>
</behavior>

Once you have enabled the metadata exchange over HTTP-GET, you can navigate to the address you configured (the HTTP base address, by default, or an explicit address) using a browser. If all is well, you will get a confirmation page like the one shown in Figure 1-6, letting you know that you have successfully hosted a service. The confirmation page is unrelated to IIS hosting, and you can use a browser to navigate to the service address even when self-hosting.

A service confirmation page

Figure 1-6. A service confirmation page

Enabling Metadata Exchange Programmatically

To programmatically enable metadata exchange over HTTP-GET, you first need to add the behavior to the collection of behaviors the host maintains for the service type. The ServiceHostBase class offers the Description property of the type ServiceDescription:

public abstract class ServiceHostBase : ...
{
   public ServiceDescription Description
   {get;}
   //More members
}

The service description, as its name implies, is the description of the service with all its aspects and behaviors. ServiceDescription contains a property called Behaviors of the type KeyedByTypeCollection<T>, with IServiceBehavior as the generic type parameter:

public class KeyedByTypeCollection<T> : KeyedCollection<Type,T>
{
   public T Find<T>(  );
   public T Remove<T>(  );
   //More members
}
public class ServiceDescription
{
   public KeyedByTypeCollection<IServiceBehavior> Behaviors
   {get;}
}

IServiceBehavior is the interface that all behavior classes and attributes implement. KeyedByTypeCollection<I> offers the generic method Find<T>( ), which returns the requested behavior if it is in the collection, and null otherwise. A given behavior type can be found in the collection at most once.

Example 1-11 shows how to enable the metadata exchange behavior programmatically.

Example 1-11. Enabling the metadata exchange behavior programmatically

ServiceHost host = new ServiceHost(typeof(MyService));

ServiceMetadataBehavior metadataBehavior;
metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>(  );
if(metadataBehavior == null)
{
   Debug.Assert(BaseAddresses.Any(uri=>uri.Scheme == "http"));

   metadataBehavior = new ServiceMetadataBehavior(  );
   metadataBehavior.HttpGetEnabled = true;
   host.Description.Behaviors.Add(metadataBehavior);
}

host.Open(  );

Note the defensive manner in which the hosting code first verifies that no metadata behavior was provided in the config file, by calling the Find<T>( ) method of KeyedByTypeCollection<I> and using ServiceMetadataBehavior as the type parameter. ServiceMetadataBehavior is defined in the System.ServiceModel.Description namespace:

public class ServiceMetadataBehavior : IServiceBehavior
{
   public bool HttpGetEnabled
   {get;set;}

   public Uri HttpGetUrl
   {get;set;}
   //More members
}

If the returned behavior is null, it means the config file contains no metadata behavior. In this case, the hosting code creates a new ServiceMetadataBehavior instance, sets HttpGetEnabled to true, and adds it to the behaviors in the service description. By defensively checking for the presence of the behavior first, the hosting code avoids overriding the config file and always allowing the administrator to tweak the behavior or turn it on or off. Note also that the code asserts the presence of an HTTP base address. The assertion uses the LINQ Any( ) query on an inline lambda predicate that checks whether the base addresses collection contains an HTTP base address.

The Metadata Exchange Endpoint

Publishing metadata over HTTP-GET is merely a WCF feature; there are no guarantees that other platforms you interact with will support it. There is, however, a standard way for publishing metadata over a special endpoint called the metadata exchange endpoint (sometimes referred to as the MEX endpoint). Figure 1-7 shows a service with business endpoints and a metadata exchange endpoint. However, you typically do not show the metadata exchange endpoint in your design diagrams.

The metadata exchange endpoint

Figure 1-7. The metadata exchange endpoint

The MEX endpoint supports an industry standard for exchanging metadata, represented in WCF by the IMetadataExchange interface:

[ServiceContract(...)]
public interface IMetadataExchange
{
   [OperationContract(...)]
   Message Get(Message request);
   //More members
}

The details of this interface are inconsequential. Like most of these industry standards, it is difficult to implement, but fortunately WCF can have the service host automatically provide the implementation of IMetadataExchange and expose the metadata exchange endpoint. All you need to do is designate the address and the binding to use, and add the service metadata behavior. For the bindings, WCF provides dedicated binding transport elements for the HTTP, HTTPS, TCP, and IPC protocols. For the address, you can provide a full address or use any of the registered base addresses. There is no need to enable the HTTP-GET option, but there is no harm in doing so. Example 1-12 shows a service that exposes three MEX endpoints, over HTTP, TCP, and IPC. For demonstration purposes, the TCP and IPC MEX endpoints use relative addresses and the HTTP endpoint uses an absolute address.

Example 1-12. Adding MEX endpoints

<services>
   <service name = "MyService" behaviorConfiguration = "MEX">
      <host>
         <baseAddresses>
            <add baseAddress = "net.tcp://localhost:8001/"/>
            <add baseAddress = "net.pipe://localhost/"/>
         </baseAddresses>
      </host>
      <endpoint
         address  = "MEX"
         binding  = "mexTcpBinding"
         contract = "IMetadataExchange"
      />
      <endpoint
         address  = "MEX"
         binding  = "mexNamedPipeBinding"
         contract = "IMetadataExchange"
      />
      <endpoint
         address  = "http://localhost:8000/MEX"
         binding  = "mexHttpBinding"
         contract = "IMetadataExchange"
      />
   </service>
</services>
<behaviors>
   <serviceBehaviors>
      <behavior name = "MEX">
         <serviceMetadata/>
      </behavior>
   </serviceBehaviors>
</behaviors>

Tip

Note in Example 1-12 that all you have to do to have the host implement the MEX endpoint for your service is include the serviceMetadata tag in the behavior. If you do not reference the behavior, the host will expect your service to implement IMetadataExchange. While this normally adds no value, it is the only way to provide for custom implementation of IMetadataExchange for advanced interoperability needs.

Adding MEX endpoints programmatically

Like any other endpoint, you can only add a metadata exchange endpoint programmatically before opening the host. WCF does not offer a dedicated binding type for the metadata exchange endpoint. Instead, you need to construct a custom binding that uses the matching transport binding element, and then provide that binding element as a construction parameter to an instance of a custom binding. Finally, call the AddServiceEndpoint( ) method of the host, providing it with the address, the custom binding, and the IMetadataExchange contract type. Example 1-13 shows the code required to add a MEX endpoint over TCP. Note that before adding the endpoint you must verify the presence of the metadata behavior.

Example 1-13. Adding a TCP MEX endpoint programmatically

BindingElement bindingElement = new TcpTransportBindingElement(  );
CustomBinding binding = new CustomBinding(bindingElement);

Uri tcpBaseAddress = new Uri("net.tcp://localhost:9000/");
ServiceHost host = new ServiceHost(typeof(MyService),tcpBaseAddress);

ServiceMetadataBehavior metadataBehavior;
metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>(  );
if(metadataBehavior == null)
{
   metadataBehavior = new ServiceMetadataBehavior(  );
   host.Description.Behaviors.Add(metadataBehavior);
}
host.AddServiceEndpoint(typeof(IMetadataExchange),binding,"MEX");
host.Open(  );

Streamlining with ServiceHost<T>

You can extend ServiceHost<T> to automate the code in Example 1-11 and Example 1-13. ServiceHost<T> offers overloaded versions of the EnableMetadataExchange( ) method, which you can call to both publish metadata over HTTP-GET and add the MEX endpoints:

public class ServiceHost<T> : ServiceHost
{
   public bool EnableMetadataExchange(  );
   public bool EnableMetadataExchange(bool enableHttpGet);

   public bool HasMexEndpoint
   {get;}
   public void AddAllMexEndPoints(  );
   //More members
}

The parameter-less EnableMetadataExchange( ) publishes metadata over HTTP-GET, and if no MEX endpoint is available, EnableMetadataExchange( ) adds a MEX endpoint for each registered base address scheme. Using ServiceHost<T>, Example 1-11 and Example 1-13 are reduced to:

ServiceHost<MyService> host = new ServiceHost<MyService>(  );
host.EnableMetadataExchange(  );
host.Open(  );

EnableMetadataExchange( ) will not override the behavior in the config file if one is present.

EnableMetadataExchange( ) can also accept a parameter controlling publishing over HTTP-GET (that is, MEX endpoints only).

ServiceHost<T> offers the HasMexEndpoint Boolean property, which returns true if the service has any MEX endpoint (regardless of transport protocol), and the AddAllMexEndPoints( ) method, which adds a MEX endpoint for each registered base address of the scheme type of HTTP, TCP, or IPC. Example 1-14 shows the implementation of these methods.

Example 1-14. Implementing EnableMetadataExchange and its supporting methods

public class ServiceHost<T> : ServiceHost
{
   public void EnableMetadataExchange(  )
   {
      EnableMetadataExchange(true);
   }
   public void EnableMetadataExchange(bool enableHttpGet)
   {
      if(State == CommunicationState.Opened)
      {
         throw new InvalidOperationException("Host is already opened");
      }
      ServiceMetadataBehavior metadataBehavior
                       = Description.Behaviors.Find<ServiceMetadataBehavior>(  );

      if(metadataBehavior == null)
      {
         metadataBehavior = new ServiceMetadataBehavior(  );
         Description.Behaviors.Add(metadataBehavior);

         if(BaseAddresses.Any((uri)=>uri.Scheme == "http"))
         {
            metadataBehavior.HttpGetEnabled = enableHttpGet;
         }
      }
      AddAllMexEndPoints(  );
   }
   public bool HasMexEndpoint
   {
      get
      {
         return Description.Endpoints.Any(
                                      endpoint=>endpoint.Contract.ContractType ==
                                      typeof(IMetadataExchange));
      }
   }
   public void AddAllMexEndPoints(  )
   {
      Debug.Assert(HasMexEndpoint == false);

      foreach(Uri baseAddress in BaseAddresses)
      {
         BindingElement bindingElement = null;
         switch(baseAddress.Scheme)
         {
            case "net.tcp":
            {
               bindingElement = new TcpTransportBindingElement(  );
               break;
            }
            case "net.pipe":
            {...}
            case "http":
            {...}
            case "https":
            {...}
         }
         if(bindingElement != null)
         {
            Binding binding = new CustomBinding(bindingElement);
            AddServiceEndpoint(typeof(IMetadataExchange),binding,"MEX");
         }
      }
   }
}

EnableMetadataExchange( ) verifies that the host has not been opened yet using the State property of the CommunicationObject base class. The HasMexEndpoint property uses the LINQ Any( ) query on an inline lambda predicate that checks whether a given endpoint's contract is indeed IMetadataExchange. Any( ) invokes the predicate on each endpoint in the collection, returning true if any one of the endpoints in the collection satisfies the predicate (that is, if the invocation of the lambda expression method returned true), and false otherwise. The AddAllMexEndPoints( ) method iterates over the BaseAddresses collection. For each base address found, it creates a matching MEX transport-binding element, creates a custom binding, and uses that (as in Example 1-13) to add the endpoint.

The Metadata Explorer

The metadata exchange endpoint provides metadata that describes not just contracts and operations, but also information about data contracts, security, transactions, reliability, and faults. To visualize the metadata of a running service, I developed the Metadata Explorer tool, which is available along with the rest of the source code of this book. Figure 1-8 shows the Metadata Explorer reflecting the endpoints of Example 1-7. To use the Metadata Explorer, simply provide it with the HTTP-GET address or the metadata exchange endpoint of the running service, and it will reflect the returned metadata.

The Metadata Explorer

Figure 1-8. The Metadata Explorer

Get Programming WCF Services, 2nd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.