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 (such as a project reference to a class library containing the contracts) from invoking operations on the service.

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. 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.

Metadata over HTTP-GET

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 = "MyMEXAddress"/>
</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-10, 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-10. A service confirmation page

Enabling metadata exchange programmatically

To enable metadata exchange over HTTP-GET programmatically, 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 U Find<U>();
   public U Remove<U>();
   //More members
}
public class ServiceDescription
{
   public KeyedByTypeCollection<IServiceBehavior> Behaviors
   {get;}

   //More members
}

IServiceBehavior is the interface that all behavior classes and attributes implement. KeyedByTypeCollection<T> offers the generic method Find<U>(), 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(baseAddress=>baseAddress.Uri.Scheme == "http"));

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

host.Open();

Notice 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 checking defensively 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 expression 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 of publishing metadata over a special endpoint, called the metadata exchange endpoint (sometimes referred to as the MEX endpoint). Figure 1-11 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-11. 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>

Note

In Example 1-12, 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.

Standard endpoints

In the vast majority of cases, a MEX endpoint always has the same three elements: the contract is always IMetadataExchange, the binding is always the reserved binding element, and the only variable is the address (and even that is typically just the base address). Having developers stipulate these endpoint elements time and time again is excessive. To streamline this and similar infrastructure endpoints, WCF provides pre-canned definitions of several endpoint types, called standard endpoints. WCF offers standard endpoints for metadata exchange, discovery, announcements, workflow, and web. You can use the standard endpoints both in config and programmatically.

You can reference the desired standard endpoint with the kind tag:

<endpoint
   kind = "..."
/>

Whatever is not specified (usually the address or the binding) always defaults to some predefined value, depending on the other fields of the endpoint. Appendix C will take advantage of the standard discovery and announcements endpoints. In the context of this section, you can use the kind value of mexEndpoint to define the MEX endpoint.

For example, suppose you do not specify an address and binding, like so:

<service ...
   <host>
      <baseAddresses>
         <add baseAddress = "http://..."/>
         <add baseAddress = "net.tcp://..."/>
      </baseAddresses>
   </host>

   <endpoint
      kind = "mexEndpoint"
   />
   ...
</service>

WCF will add a MEX endpoint whose address is the HTTP base address. This mandates the presence of an HTTP base address and that no other endpoint is using the base address for its address.

You can also append a URI to the base address:

<endpoint
   kind = "mexEndpoint"
   address = "MEX"
/>

If you specify the binding, WCF will infer the correct base address to use from the binding type, for example:

<service ...
   <host>
      <baseAddresses>
         <add baseAddress = "http://..."/>
         <add baseAddress = "net.tcp://..."/>
      </baseAddresses>
   </host>

   <endpoint
      kind = "mexEndpoint"
      binding = "mexTcpBinding"
   />
   <endpoint
      kind = "mexEndpoint"
      address = "MEX"
      binding = "mexTcpBinding"
   />
   ...
</service>

You can also specify a fully qualified address irrespective of the base address.

Note that WCF is not smart enough to infer the binding to use from the address scheme, meaning the following configuration is invalid:

<!-- Invalid configuration -->
<endpoint
   kind = "mexEndpoint"
   address = "net.tcp://..."
/>

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 provide that binding element as a construction parameter to an instance of a custom binding. To streamline this process, use the MetadataExchangeBindings static helper class defined as:

public static class MetadataExchangeBindings
{
   public static Binding CreateMexHttpBinding();
   public static Binding CreateMexNamedPipeBinding();
   public static Binding CreateMexTcpBinding();

   //More members
}

Finally, call the AddServiceEndpoint() method of the host, providing it with the address, the MEX 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

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);
}
Binding binding = MetadataExchangeBindings.CreateMexTcpBinding();
host.AddServiceEndpoint(typeof(IMetadataExchange),binding,"MEX");
host.Open();

You can also add a MEX endpoint using the standard MEX endpoint. To do so, use the type ServiceMetadataEndpoint, defined as:

public class ServiceMetadataEndpoint : ServiceEndpoint
{
   public ServiceMetadataEndpoint();
   public ServiceMetadataEndpoint(EndpointAddress address);
   public ServiceMetadataEndpoint(Binding binding,EndpointAddress address);
}

The default constructor of ServiceMetadataEndpoint defaults to using the HTTP base address and binding. The constructor that takes an endpoint address must receive a fully qualified HTTP or HTTPS address:

ServiceHost host = new ServiceHost(typeof(MyService));
host.Description.Behaviors.Add(new ServiceMetadataBehavior());

EndpointAddress address = new EndpointAddress("http://localhost:8000/MEX");

ServiceEndpoint endpoint = new ServiceMetadataEndpoint(address);
host.AddServiceEndpoint(endpoint);
...
host.Open();

In addition, ServiceMetadataEndpoint will never use the host base addresses.

Streamlining with ServiceHost<T>

You can extend ServiceHost<T> to automate the code in Example 1-11 and Example 1-13. ServiceHost<T> offers 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 void EnableMetadataExchange(bool enableHttpGet = true);

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

The default 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.

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(bool enableHttpGet = true)
   {
      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)
      {
         Binding binding = null;

         switch(baseAddress.Scheme)
         {
            case "net.tcp":
            {
               binding = MetadataExchangeBindings.CreateMexTcpBinding();
               break;
            }
            case "net.pipe":
            {...}
            case "http":
            {...}
            case "https":
            {...}
         }
         if(binding != null)
         {
            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 expression that checks whether a given endpoint’s contract is indeed IMetadataExchange. Any() invokes the expression on the endpoints in the collection, returning true when 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 binding and adds the MEX endpoint with a MEX URI under the base address.

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 for this book. Figure 1-12 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-12. The Metadata Explorer

Get Programming WCF Services, 3rd 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.