Custom WSDL Publication

The WsdlDocumentation sample demonstrates how to:

Note

The setup procedure and build instructions for this sample are located at the end of this topic.

Service

The service in this sample is marked with two custom attributes. The first, the WsdlDocumentationAttribute, accepts a string in the constructor and can be applied to provide a contract interface or operation with a string that describes its usage. The second, WsdlParamOrReturnDocumentationAttribute, can be applied to return values or parameters to describe those values in the operation. The following example shows a service contract, ICalculator, described using these attributes.

// Define a service contract.
[ServiceContract(Namespace="http://Microsoft.ServiceModel.Samples")]
// Document it.
[WsdlDocumentation("The ICalculator contract performs basic calculation services.")]
public interface ICalculator
{
    [OperationContract]
    [WsdlDocumentation("The Add operation adds two numbers and returns the result.")]
    [return:WsdlParamOrReturnDocumentation("The result of adding the two arguments together.")]
    double Add(
      [WsdlParamOrReturnDocumentation("The first value to add.")]double n1,
      [WsdlParamOrReturnDocumentation("The second value to add.")]double n2
    );

    [OperationContract]
    [WsdlDocumentation("The Subtract operation subtracts the second argument from the first.")]
    [return:WsdlParamOrReturnDocumentation("The result of the second argument subtracted from the first.")]
    double Subtract(
      [WsdlParamOrReturnDocumentation("The value from which the second is subtracted.")]double n1,
      [WsdlParamOrReturnDocumentation("The value that is subtracted from the first.")]double n2
    );

    [OperationContract]
    [WsdlDocumentation("The Multiply operation multiplies two values.")]
    [return:WsdlParamOrReturnDocumentation("The result of multiplying the first and second arguments.")]
    double Multiply(
      [WsdlParamOrReturnDocumentation("The first value to multiply.")]double n1,
      [WsdlParamOrReturnDocumentation("The second value to multiply.")]double n2
    );

    [OperationContract]
    [WsdlDocumentation("The Divide operation returns the value of the first argument divided by the second argument.")]
    [return:WsdlParamOrReturnDocumentation("The result of dividing the first argument by the second.")]
    double Divide(
      [WsdlParamOrReturnDocumentation("The numerator.")]double n1,
      [WsdlParamOrReturnDocumentation("The denominator.")]double n2
    );
}

The WsdlDocumentationAttribute implements IContractBehavior and IOperationBehavior, so the attribute instances are added to the corresponding ContractDescription or OperationDescription when the service is opened. The attribute also implements IWsdlExportExtension. When ExportContract(WsdlExporter, WsdlContractConversionContext) is called, the WsdlExporter that is used to export the metadata and the WsdlContractConversionContext that contains the service description objects are passed in as parameters enabling the modification of the exported metadata.

In this sample, depending upon whether the export context object has a ContractDescription or an OperationDescription, a comment is extracted from the attribute using the text property and is added to the WSDL annotation element as shown in the following code.

public void ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
{
    if (contractDescription != null)
    {
        // Inside this block it is the contract-level comment attribute.
        // This.Text returns the string for the contract attribute.
        // Set the doc element; if this isn't done first, there is no XmlElement in the
        // DocumentElement property.
        context.WsdlPortType.Documentation = string.Empty;
        // Contract comments.
        XmlDocument owner = context.WsdlPortType.DocumentationElement.OwnerDocument;
        XmlElement summaryElement = owner.CreateElement("summary");
        summaryElement.InnerText = this.Text;
        context.WsdlPortType.DocumentationElement.AppendChild(summaryElement);
    }
    else
    {
        Operation operation = context.GetOperation(operationDescription);
        if (operation != null)
        {
            // We are dealing strictly with the operation here.
            // This.Text returns the string for the operation-level attributes.
            // Set the doc element; if this isn't done first, there is no XmlElement in the
            // DocumentElement property.
            operation.Documentation = String.Empty;

            // Operation C# triple comments.
            XmlDocument owner = operation.DocumentationElement.OwnerDocument;
            XmlElement newSummaryElement = owner.CreateElement("summary");
            newSummaryElement.InnerText = this.Text;
            operation.DocumentationElement.AppendChild(newSummaryElement);
        }
    }
}

If an operation is being exported, the sample uses reflection to obtain any WsdlParamOrReturnDocumentationAttribute values for parameters and return values and adds them to the WSDL annotation elements for that operation as follows.

// Get returns information
ParameterInfo returnValue = operationDescription.SyncMethod.ReturnParameter;
object[] returnAttrs = returnValue.GetCustomAttributes(typeof(WsdlParamOrReturnDocumentationAttribute), false);
if (returnAttrs.Length != 0)
{
    // <returns>text.</returns>
    XmlElement returnsElement = owner.CreateElement("returns");
    returnsElement.InnerText = ((WsdlParamOrReturnDocumentationAttribute)returnAttrs[0]).ParamComment;
    operation.DocumentationElement.AppendChild(returnsElement);
}

// Get parameter information.
ParameterInfo[] args = operationDescription.SyncMethod.GetParameters();
for (int i = 0; i < args.Length; i++)
{
    object[] docAttrs = args[i].GetCustomAttributes(typeof(WsdlParamOrReturnDocumentationAttribute), false);
    if (docAttrs.Length == 1)
    {
        // <param name="Int1">Text.</param>
        XmlElement newParamElement = owner.CreateElement("param");
        XmlAttribute paramName = owner.CreateAttribute("name");
        paramName.Value = args[i].Name;
        newParamElement.InnerText = ((WsdlParamOrReturnDocumentationAttribute)docAttrs[0]).ParamComment;
        newParamElement.Attributes.Append(paramName);
        operation.DocumentationElement.AppendChild(newParamElement);
    }
}

The sample then publishes metadata in the standard way, using the following configuration file.

<services>
  <service
      name="Microsoft.ServiceModel.Samples.CalculatorService"
      behaviorConfiguration="CalculatorServiceBehavior">
    <!-- ICalculator is exposed at the base address provided by host: http://localhost/servicemodelsamples/service.svc  -->
    <endpoint address=""
              binding="wsHttpBinding"
              contract="Microsoft.ServiceModel.Samples.ICalculator" />
    <!-- the mex endpoint is exposed at http://localhost/servicemodelsamples/service.svc/mex -->
    <endpoint address="mex"
              binding="mexHttpBinding"
              contract="IMetadataExchange" />
  </service>
</services>

<!--For debugging purposes set the includeExceptionDetailInFaults attribute to true-->
<behaviors>
  <serviceBehaviors>
    <behavior name="CalculatorServiceBehavior">
      <serviceMetadata httpGetEnabled="True"/>
      <serviceDebug includeExceptionDetailInFaults="False" />
    </behavior>
  </serviceBehaviors>
</behaviors>

Svcutil client

This sample does not use Svcutil.exe. The contract is provided in the generatedClient.cs file so that after the sample demonstrates custom WSDL import and code generation, the service can be invoked. To use the following custom WSDL importer for this example, you can run Svcutil.exe and specify the /svcutilConfig option, giving the path to the client configuration file used in this sample, which references the WsdlDocumentation.dll library. To load the WsdlDocumentationImporter, however, Svuctil.exe must be able to locate and load the WsdlDocumentation.dll library, which means either that it is registered in the global assembly cache, in the path, or is in the same directory as Svcutil.exe. For a basic sample such as this, the easiest thing to do is to copy Svcutil.exe and the client configuration file into the same directory as WsdlDocumentation.dll and run it from there.

The Custom WSDL Importer

The custom IWsdlImportExtension object WsdlDocumentationImporter also implements IContractBehavior and IOperationBehavior to be added to the imported ServiceEndpoints and IServiceContractGenerationExtension and IOperationContractGenerationExtension to be invoked to modify the code generation when the contract or operation code is being created.

First, in the ImportContract(WsdlImporter, WsdlContractConversionContext) method, the sample determines whether the WSDL annotation is at the contract or operation level, and adds itself as a behavior at the appropriate scope, passing the imported annotation text to its constructor.

public void ImportContract(WsdlImporter importer, WsdlContractConversionContext context)
{
    // Contract Documentation
    if (context.WsdlPortType.Documentation != null)
    {
        // System examines the contract behaviors to see whether any implement IWsdlImportExtension.
        context.Contract.Behaviors.Add(new WsdlDocumentationImporter(context.WsdlPortType.Documentation));
    }
    // Operation Documentation
    foreach (Operation operation in context.WsdlPortType.Operations)
    {
        if (operation.Documentation != null)
        {
            OperationDescription operationDescription = context.Contract.Operations.Find(operation.Name);
            if (operationDescription != null)
            {
                // System examines the operation behaviors to see whether any implement IWsdlImportExtension.
                operationDescription.Behaviors.Add(new WsdlDocumentationImporter(operation.Documentation));
            }
        }
    }
}

Then, when the code is generated, the system invokes the GenerateContract(ServiceContractGenerationContext) and GenerateOperation(OperationContractGenerationContext) methods, passing the appropriate context information. The sample formats the custom WSDL annotations and inserts them as comments into the CodeDom.

public void GenerateContract(ServiceContractGenerationContext context)
{
    Debug.WriteLine("In generate contract.");
    context.ContractType.Comments.AddRange(FormatComments(text));
}

public void GenerateOperation(OperationContractGenerationContext context)
{
    context.SyncMethod.Comments.AddRange(FormatComments(text));
    Debug.WriteLine("In generate operation.");
}

The Client Application

The client application loads the custom WSDL importer by specifying it in the application configuration file.

<client>
  <endpoint address="http://localhost/servicemodelsamples/service.svc"
  binding="wsHttpBinding"
  contract="ICalculator" />
  <metadata>
    <wsdlImporters>
      <extension type="Microsoft.ServiceModel.Samples.WsdlDocumentationImporter, WsdlDocumentation"/>
    </wsdlImporters>
  </metadata>
</client>

Once the custom importer has been specified, the WCF metadata system loads the custom importer into any WsdlImporter created for that purpose. This sample uses the MetadataExchangeClient to download the metadata, the WsdlImporter properly configured to import the metadata using the custom importer the sample creates, and the ServiceContractGenerator to compile the modified contract information into both Visual Basic and C# client code that can be used in Visual Studio to support Intellisense or compiled into XML documentation.

/// From WSDL Documentation:
///
/// <summary>The ICalculator contract performs basic calculation
/// services.</summary>
///
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(Namespace="http://Microsoft.ServiceModel.Samples", ConfigurationName="ICalculator")]
public interface ICalculator
{

    /// From WSDL Documentation:
    ///
    /// <summary>The Add operation adds two numbers and returns the
    /// result.</summary><returns>The result of adding the two arguments
    /// together.</returns><param name="n1">The first value to add.</param><param
    /// name="n2">The second value to add.</param>
    ///
    [System.ServiceModel.OperationContractAttribute(Action="http://Microsoft.ServiceModel.Samples/ICalculator/Add", ReplyAction="http://Microsoft.ServiceModel.Samples/ICalculator/AddResponse")]
    double Add(double n1, double n2);

    /// From WSDL Documentation:
    ///
    /// <summary>The Subtract operation subtracts the second argument from the
    /// first.</summary><returns>The result of the second argument subtracted from the
    /// first.</returns><param name="n1">The value from which the second is
    /// subtracted.</param><param name="n2">The value that is subtracted from the
    /// first.</param>
    ///
    [System.ServiceModel.OperationContractAttribute(Action="http://Microsoft.ServiceModel.Samples/ICalculator/Subtract", ReplyAction="http://Microsoft.ServiceModel.Samples/ICalculator/SubtractResponse")]
    double Subtract(double n1, double n2);

    /// From WSDL Documentation:
    ///
    /// <summary>The Multiply operation multiplies two values.</summary><returns>The
    /// result of multiplying the first and second arguments.</returns><param
    /// name="n1">The first value to multiply.</param><param name="n2">The second value
    /// to multiply.</param>
    ///
    [System.ServiceModel.OperationContractAttribute(Action="http://Microsoft.ServiceModel.Samples/ICalculator/Multiply", ReplyAction="http://Microsoft.ServiceModel.Samples/ICalculator/MultiplyResponse")]
    double Multiply(double n1, double n2);

    /// From WSDL Documentation:
    ///
    /// <summary>The Divide operation returns the value of the first argument divided
    /// by the second argument.</summary><returns>The result of dividing the first
    /// argument by the second.</returns><param name="n1">The numerator.</param><param
    /// name="n2">The denominator.</param>
    ///
    [System.ServiceModel.OperationContractAttribute(Action="http://Microsoft.ServiceModel.Samples/ICalculator/Divide", ReplyAction="http://Microsoft.ServiceModel.Samples/ICalculator/DivideResponse")]
    double Divide(double n1, double n2);
}

To set up, build, and run the sample

  1. Ensure that you have performed the One-Time Setup Procedure for the Windows Communication Foundation Samples.

  2. To build the C# or Visual Basic .NET edition of the solution, follow the instructions in Building the Windows Communication Foundation Samples.

  3. To run the sample in a single- or cross-machine configuration, follow the instructions in Running the Windows Communication Foundation Samples.