Udostępnij za pośrednictwem


WCF Extensibility – WSDL Export Extension

This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .

Now that we’re done with serialization, let’s get back to the service portion of the series. This post and the next ones will deal with metadata export (and import), which is the process by which a service describes itself so that other tools can consume that description and create a proxy class which can talk to that service. WCF has out-of-the-box metadata capabilities – in order to expose metadata from a service we simply add the ServiceMetadataBehavior to the service and set one property or add a specific metadata endpoint , and to consume metadata from other services and create a proxy we have two tools for that: svcutil.exe and Add Service Reference (and some APIs to let you write your own tool, mostly centered around the MetadataExchangeClient and ServiceContractGenerator classes.

But, with almost everything in WCF, there are many ways to tinker with that process. During export, we can add an export extension which is called and lets us change what metadata the service will expose to the world. And during import, we can hook into the metadata import process and change how the metadata will be converted into the code DOM which will then be used to create the proxy class. This post will cover the first part (export extension) and I’ll leave the import extension for the next week.

Hooking up to the WSDL Export pipeline in WCF means getting to work with many of the classes on the “old” System.Web.Services.Description namespace (legacy from the ASMX days). A little different than the “description” classes in WCF, but nothing extraordinary. The main interface for this scenario is the IWsdlExportExtension, and unlike most of the other extensibility points, there’s no place to “set” the WSDL export extension – as long as a class which implements one of the contract, endpoint or operation behavior interfaces (IContractBehavior, IEndpointBehavior, or IOperationBehavior – if used with an IServiceBehavior it’s ignored) also implements IWsdlExportExtension (and the behavior is added to the description), the export extension will be used. A binding element class can also implement IWsdlExportExtension, and it will be used as well.

Interface definition

  1. public interface IWsdlExportExtension
  2. {
  3.     void ExportContract(WsdlExporter exporter, WsdlContractConversionContext context);
  4.     void ExportEndpoint(WsdlExporter exporter, WsdlEndpointConversionContext context);
  5. }

The method ExportContract is the first to be called. It’s called first on contract behaviors, then on operation behaviors. When the method is called for the contract, the exporter still doesn’t have all the detailed information about the operations; when it’s called for the individual operations, information such as the operation messages, message parts and the expected schema for the parts. Then ExportEndpoint is called, first on endpoint behaviors, then on binding elements, then on contract behaviors and finally on operation behaviors. When it’s called for the endpoint, besides all the contract information from ExportContract, this time there is already some information about the endpoint binding and policy assertions coming from the specific binding elements. The call to the binding element have any additional policies exported by IPolicyExportExtension implementations. At the call in the contract behavior, the information is augmented with the detailed address information of the endpoint, including any identity information. Finally, at each operation behavior, ExportEndpoint is called with additional details about the operation itself, including the SOAP use and the SOAP action.

This isn’t very easy to explain, so I added in the code in this post (in the gallery) linked below a small program which prints out the metadata passed to the exporter at each individual step; it will dump the metadata as XML files, and you can use a tool such as windiff to compare those files.

Public implementations in WCF

There are quite a lot of classes in WCF which implement the IWsdlExportExtension interface, including all transport and message encoding binding elements, and the serializer operation behaviors (DataContractSerializerOperationBehavior and XmlSerializerOperationBehavior, since they determine the schema of the input / output of the operations). I won’t list them all here since the behaviors have been talked about in the post about operation behaviors, and the encoding / transport binding elements will be covered on the posts about transports and message encodings.

How to add a WSDL export extension

As I mentioned before, as long as an endpoint, contract, operation behavior or binding element class also implements IWsdlExportExtension, the metadata system will pick up that extension. The code below shows an example of a contract inspector which, if added to an endpoint contract, will cause the export extension methods to be called when the service metadata is being generated.

  1. public class ContractBehaviorWithWsdlExport
  2.     : IContractBehavior, IWsdlExportExtension
  3. {
  4.     // IContractBehavior methods ommitted
  5.  
  6.     public void ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
  7.     {
  8.         Console.WriteLine("In ExportContract");
  9.     }
  10.  
  11.     public void ExportEndpoint(WsdlExporter exporter, WsdlEndpointConversionContext context)
  12.     {
  13.         Console.WriteLine("In ExportEndpoint");
  14.     }
  15. }

Real world scenario: deprecating / hiding service operation on WSDL

This came from stack overflow: the user has a service which is used by many clients out of their control. Some operations, however, didn’t make sense for them, so they wanted to deprecate those operations, preventing new clients from using them. But they also didn’t want to break existing clients, so they couldn’t simply remove the operations. The request on the post was to have some sort of attribute which would remove the operation from the metadata, but not from the runtime. This already exists for whole contracts (using the ServiceMetadataContractBehavior), but not for individual operations, so I decided to write a sample which does exactly that.

First some operations to start. I’m not really creative today, so I’ll just use the calculator contract with the normal arithmetic operations (add, subtract, multiply, divide). But after a while, we decide that the Multiply and Divide operations were not as profitable for our business, so we want to gradually discontinue it. In order to do that, I’ll use some attributes marking the operations as deprecated. I’ll also add an attribute to the contract itself, which is the one which will act as a WSDL export extension and remove the deprecated operations from the service metadata.

  1. [ServiceContract]
  2. [DeprecatedOperationsWsdlExportExtension]
  3. public interface ICalculator
  4. {
  5.     [OperationContract]
  6.     int Add(int x, int y);
  7.     [OperationContract]
  8.     int Subtract(int x, int y);
  9.     [OperationContract]
  10.     [DeprecatedOperation]
  11.     int Multiply(int x, int y);
  12.     [OperationContract]
  13.     [DeprecatedOperation]
  14.     int Divide(int x, int y);
  15. }

And before moving on, the usual disclaimer: this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios (please let me know if you find a bug or something missing). One issue that I know is that this sample removes the operations from the WSDL, but if the operations used some custom (composite) type, the schema for those types is still listed in the metadata (an improvement for the sample if anyone wants to try to do it).

Now for the attributes mentioned above. The DeprecatedOperationAttribute doesn’t have anything, it’s only a marker.

  1. public class DeprecatedOperationAttribute : Attribute
  2. {
  3. }

The WSDL export extension is more interesting. As I mentioned before, it needs to be either an operation/contract/endpoint behavior or a binding element, in this case I’m using a contract behavior. In many cases the methods for the IContractBehavior interface are left unimplemented, but in this case we’ll use the ApplyDispatchBehavior to collect the list of all deprecated operations, which will be later removed from the metadata when it’s requested.

  1. class DeprecatedOperationsWsdlExportExtensionAttribute : Attribute, IContractBehavior, IWsdlExportExtension
  2. {
  3.     List<string> operationsToRemove = new List<string>();
  4.  
  5.     public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
  6.     {
  7.     }
  8.  
  9.     public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  10.     {
  11.     }
  12.  
  13.     public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
  14.     {
  15.         foreach (OperationDescription operation in contractDescription.Operations)
  16.         {
  17.             MethodInfo method = operation.SyncMethod ?? operation.BeginMethod;
  18.             if (method != null)
  19.             {
  20.                 bool isDeprecated = Attribute.IsDefined(method, typeof(DeprecatedOperationAttribute));
  21.                 if (isDeprecated)
  22.                 {
  23.                     this.operationsToRemove.Add(operation.Name);
  24.                 }
  25.             }
  26.         }
  27.     }
  28.  
  29.     public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
  30.     {
  31.     }
  32. }

Now for the implementation of the methods in IWsdlExportExtension. Since the code will remove all of the operations at once, I decided to implement it on ExportEndpoint. The outline of the implementation shouldn’t be hard to follow.

  1. public void ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
  2. {
  3. }
  4.  
  5. public void ExportEndpoint(WsdlExporter exporter, WsdlEndpointConversionContext context)
  6. {
  7.     System.Web.Services.Description.ServiceDescription wsdl = exporter.GeneratedWsdlDocuments[0];
  8.     List<string> messageNamesToRemove;
  9.     RemoveOperationsFromPortTypes(wsdl, out messageNamesToRemove);
  10.     List<string> policiesToRemove;
  11.     RemoveOperationsFromBindings(wsdl, out policiesToRemove);
  12.     RemoveWsdlMessages(wsdl, messageNamesToRemove);
  13.     RemoveOperationRelatedPolicies(wsdl, policiesToRemove);
  14. }

The service description object contains many internal links, and as we start removing the operations from all places where they appear, we store their references in other locations to remove later. For example, the port types in WSDL list the possible messages which can be sent / received for each operation, and those messages are further detailed in the messages section. This is shown in the excerpt below. It loops through all the operations in the services port types, removing those which are marked as obsolete. And for those operations, we store the messages which are supposed to be removed in the other part of the WSDL.

  1. private void RemoveOperationsFromPortTypes(System.Web.Services.Description.ServiceDescription wsdl, out List<string> messageNamesToRemove)
  2. {
  3.     messageNamesToRemove = new List<string>();
  4.     foreach (System.Web.Services.Description.PortType portType in wsdl.PortTypes)
  5.     {
  6.         for (int i = portType.Operations.Count - 1; i >= 0; i--)
  7.         {
  8.             if (this.operationsToRemove.Contains(portType.Operations[i].Name))
  9.             {
  10.                 foreach (System.Web.Services.Description.OperationMessage operationMessage in portType.Operations[i].Messages)
  11.                 {
  12.                     messageNamesToRemove.Add(operationMessage.Message.Name);
  13.                 }
  14.  
  15.                 portType.Operations.RemoveAt(i);
  16.             }
  17.         }
  18.     }
  19. }

We do something similar for the bindings – iterate through the WSDL bindings, and remove each deprecated operation inside the binding, and store the policy references to be removed later.

  1. private void RemoveOperationsFromBindings(System.Web.Services.Description.ServiceDescription wsdl, out List<string> policiesToRemove)
  2. {
  3.     policiesToRemove = new List<string>();
  4.     foreach (System.Web.Services.Description.Binding binding in wsdl.Bindings)
  5.     {
  6.         for (int i = binding.Operations.Count - 1; i >= 0; i--)
  7.         {
  8.             if (this.operationsToRemove.Contains(binding.Operations[i].Name))
  9.             {
  10.                 string inputPolicy = GetPolicyReferences(binding.Operations[i].Input);
  11.                 if (inputPolicy != null)
  12.                 {
  13.                     policiesToRemove.Add(inputPolicy);
  14.                 }
  15.  
  16.                 string outputPolicy = GetPolicyReferences(binding.Operations[i].Output);
  17.                 if (outputPolicy != null)
  18.                 {
  19.                     policiesToRemove.Add(outputPolicy);
  20.                 }
  21.  
  22.                 binding.Operations.RemoveAt(i);
  23.             }
  24.         }
  25.     }
  26. }

Removing the messages is trivial, just iterate through the WSDL messages and remove those marked for deletion (I’ll skip the code here, it’s on the code link at the end of this post). For removing operation policies, since they’re stored as extensions in the WSDL (instead of first-class objects), we need to look at their XML properties to find them out. The code below shows that method.

  1. private static void RemoveOperationRelatedPolicies(System.Web.Services.Description.ServiceDescription wsdl, List<string> policiesToRemove)
  2. {
  3.     const string WsdlPolicyNamespace = "https://schemas.xmlsoap.org/ws/2004/09/policy";
  4.     const string WsSecurityUtilityNamespace = "https://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
  5.  
  6.     for (int i = wsdl.Extensions.Count - 1; i >= 0; i--)
  7.     {
  8.         XmlElement extension = wsdl.Extensions[i] as XmlElement;
  9.         if (extension != null && extension.LocalName == "Policy"
  10.             && extension.NamespaceURI == WsdlPolicyNamespace)
  11.         {
  12.             XmlAttribute id = extension.Attributes["Id", WsSecurityUtilityNamespace];
  13.             if (id != null && policiesToRemove.Contains(id.Value))
  14.             {
  15.                 wsdl.Extensions.RemoveAt(i);
  16.             }
  17.         }
  18.     }
  19. }

And that’s it. Now it’s time to test the implementation. The code below starts up the service, and calls one of the deprecated operations – it still works fine.

  1. static void Main(string[] args)
  2. {
  3.     string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  4.     ServiceHost host = new ServiceHost(typeof(CalculatorService), new Uri(baseAddress));
  5.     ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(ICalculator), new WSHttpBinding(), "");
  6.     host.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });
  7.     host.Open();
  8.     Console.WriteLine("Host opened");
  9.  
  10.     ChannelFactory<ICalculator> factory = new ChannelFactory<ICalculator>(new WSHttpBinding(), new EndpointAddress(baseAddress));
  11.     ICalculator proxy = factory.CreateChannel();
  12.  
  13.     Console.WriteLine(proxy.Multiply(3, 5));
  14.  
  15.     ((IClientChannel)proxy).Close();
  16.     factory.Close();
  17.  
  18.     Console.Write("Press ENTER to close the host");
  19.     Console.ReadLine();
  20.     host.Close();
  21. }

But when we use svcutil.exe to generate the client for it, the client doesn’t have that operation:

  1. [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
  2. [System.ServiceModel.ServiceContractAttribute(ConfigurationName = "ICalculator")]
  3. public interface ICalculator
  4. {
  5.  
  6.     [System.ServiceModel.OperationContractAttribute(Action = "https://tempuri.org/ICalculator/Add", ReplyAction = "https://tempuri.org/ICalculator/AddResponse")]
  7.     int Add(int x, int y);
  8.  
  9.     [System.ServiceModel.OperationContractAttribute(Action = "https://tempuri.org/ICalculator/Subtract", ReplyAction = "https://tempuri.org/ICalculator/SubtractResponse")]
  10.     int Subtract(int x, int y);
  11. }

And that’s it for WSDL export extensions.

Coming up

More metadata extensibility, with the WSDL import extension.

[Code in this post]

[Back to the index]

Comments

  • Anonymous
    October 05, 2011
    Hi!Long time ago, I was looking for a way to use XML comments or some comments from additional custom attributes to fulfil the generated WSDL with XML comments, that again will be used to generate XML comments for the generated client proxy. Is the way described by you in this article a good starting point to do that? Do you know any way to do so?Regards, Maciej

  • Anonymous
    October 06, 2011
    Hi Maciej, there's an official WCF sample that does exactly that: msdn.microsoft.com/.../aa717040.aspx.

  • Anonymous
    December 05, 2011
    Hi,how to I use the ServiceMetadataContractBehavior?Regards, Dennis

  • Anonymous
    December 05, 2011
    Hi Dennis, you have to add it programmatically (see an example in the post about contract behaviors at blogs.msdn.com/.../wcf-extensibility-icontractbehavior.aspx). Contract behaviors can only be added programmatically, or, if they're an attribute class, they can be applied to the contract interface. Since ServiceMetadataContractBehavior is not an attribute, your only option is the first one.

  • Anonymous
    December 12, 2014
    Hi, Carlos!Thank you for excellent blog series, it's a holy bible for everyone, who wants to extend WCF and figure out, how it works under the hood!Thnks to you publications I managed to implement JSON RPC for WCF.Now, what I'm trying to do, is to implement JSDL metadata output, not WSDL.Is there WCF way to do that?

  • Anonymous
    June 20, 2015
    Hi Carlos is there any way to do same thing for DataMember of DataContract? i want to Change DataMembers which different client can see

  • Anonymous
    June 22, 2015
    Hi Soheil, That should be possible. With the service description you have access to the whole, well, service description, including the data contracts. It's been a few years since I worked with that, but if you keep looking at the object model of the System.Web.Services.Description.ServiceDescription class (and the classes it references), I guess you should be able to find something that does what you need. Hope this helps, Carlos.