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.
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.
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"> <serviceMetadatahttpGetEnabled = "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.
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 KeyedBy
Type
Collection<I>
and using
ServiceMetadataBehavior
as the type
parameter. Service
Metadata
Behavior
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.
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 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.
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://..." />
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.
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 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.
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.