Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WCF Client: Forcing Global Namespaces

Tags:

c#

soap

wcf

I'm working on interfacing with a SOAP service that appears to not deal with default namespaces, but works fine with global namespaces and namespace prefixes declared at the SOAP envelope level.

The problem is that WCF doesn't create these global namespaces at the root, but rather uses explicit unprefixed default namespaces which the service apparently is choking on. Now I know that this not really WCF's fault - I believe the WCF generated messages are valid XML, but the service chokes on it nonetheless.

Using WCF the output generated looks like this:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <h:Security xmlns:h="http://docs.oasis-open.org/wss/2004/01/oasis-
          ...
    </h:Security>
  </s:Header>
  <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <cancelShipmentRequest xmlns="http://www.royalmailgroup.com/api/ship/V2">
      <integrationHeader>
        <dateTime xmlns="http://www.royalmailgroup.com/integration/core/V1">2016-03-26T01:44:37.0493801Z</dateTime>
        <version xmlns="http://www.royalmailgroup.com/integration/core/V1">2</version>
        <identification xmlns="http://www.royalmailgroup.com/integration/core/V1">
          <applicationId>RMG-API-G-01</applicationId>
          <transactionId>ozhckwej6sxg</transactionId>
        </identification>
      </integrationHeader>
      <cancelShipments>
        <shipmentNumber>TTT001908905GB</shipmentNumber>
      </cancelShipments>
    </cancelShipmentRequest>
  </s:Body>
</s:Envelope>

which doesn't work.

Using the following SOAP envelope (manually in SoapUI) does work however:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:oas="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
  xmlns:v2="http://www.royalmailgroup.com/api/ship/V2"
  xmlns:v1="http://www.royalmailgroup.com/integration/core/V1">
  <soapenv:Header>
    <h:Security xmlns:h="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       ...
    </h:Security>
  </soapenv:Header>
  <soapenv:Body>
    <v2:cancelShipmentRequest>
      <v2:integrationHeader>
        <v1:dateTime>2016-03-02T14:55:00Z</v1:dateTime>
        <v1:version>2</v1:version>
        <v1:identification>
          <v1:applicationId>RMG-API-G-01</v1:applicationId>
          <v1:transactionId>wftdaife96gv</v1:transactionId>
        </v1:identification>
      </v2:integrationHeader>
      <v2:cancelShipments>
        <v2:shipmentNumber>TTT001908905GB</v2:shipmentNumber>
      </v2:cancelShipments>
    </v2:cancelShipmentRequest>
  </soapenv:Body>
</soapenv:Envelope>

The difference between the two is that the v1 and v2 namespaces are declared globally at the top of the document and there are no local namespace declarations in the second document.

Maybe I'm missing something but to me the WCF generated XML looks to be valid and represent the same document state in terms of the namespacing.

The only difference I can tell is that the way the namespaces are declared. And although the WCF version seems to be valid and producing the same namespacing, the service complains about invalid namespace references.

Failed Schema Validation: Message failed schema validation: Schemas validity error : Element 'xmlns': This element is not expected. Expected is ( {http://www.royalmailgroup.com/api/ship/V2}integrationHeader ).

The question is, what's the best way to force the WCF to add the namespace references at the top instead of inline? The only way I've found so far is to use a message inspector and explicitly rewrite the message, but if I go through all that I might as well just manually create the messages.

Any ideas what I can try to force WCF to use explicit namespace prefixes without manually rewriting the messages?

like image 537
Rick Strahl Avatar asked Mar 26 '16 02:03

Rick Strahl


1 Answers

So the answer to this problem was to create a custom IClientMessageFormatter and Message then overriding the Message.OnWriteStartEnvelope() to explicitly write out all the namespaces at the Soap document root. The rendered document, then reuses these namespaces instead of explicitly assigning namespaces on child elements.

There are 3 classes to be created for this to work:

  • Message Implementation that handles the actual OnWriteStartEnvelope()
  • IClientMessageFormatter that is hooked into WCF
  • FormatMessageAttribute to attach to each method of the client

Here's the code for all three:

public class RoyalMailCustomMessage : Message
{
    private readonly Message message;

    public RoyalMailCustomMessage(Message message)
    {
        this.message = message;
    }
    public override MessageHeaders Headers
    {
        get { return this.message.Headers; }
    }
    public override MessageProperties Properties
    {
        get { return this.message.Properties; }
    }
    public override MessageVersion Version
    {
        get { return this.message.Version; }
    }

    protected override void OnWriteStartBody(XmlDictionaryWriter writer)
    {
        writer.WriteStartElement("Body", "http://schemas.xmlsoap.org/soap/envelope/");
    }
    protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
    {
        this.message.WriteBodyContents(writer);
    }
    protected override void OnWriteStartEnvelope(XmlDictionaryWriter writer)
    {
        writer.WriteStartElement("soapenv", "Envelope", "http://schemas.xmlsoap.org/soap/envelope/");
        writer.WriteAttributeString("xmlns", "oas", null, "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
        writer.WriteAttributeString("xmlns", "v2", null, "http://www.royalmailgroup.com/api/ship/V2");
        writer.WriteAttributeString("xmlns", "v1", null, "http://www.royalmailgroup.com/integration/core/V1");
        writer.WriteAttributeString("xmlns", "xsi", null, "http://www.w3.org/2001/XMLSchema-instance");
        writer.WriteAttributeString("xmlns", "xsd", null, "http://www.w3.org/2001/XMLSchema");            
    }
}

public class RoyalMailMessageFormatter : IClientMessageFormatter
{
    private readonly IClientMessageFormatter formatter;

    public RoyalMailMessageFormatter(IClientMessageFormatter formatter)
    {
        this.formatter = formatter;
    }

    public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)
    {
        var message = this.formatter.SerializeRequest(messageVersion, parameters);
        return new RoyalMailCustomMessage(message);
    }

    public object DeserializeReply(Message message, object[] parameters)
    {
        return this.formatter.DeserializeReply(message, parameters);
    }
}


[AttributeUsage(AttributeTargets.Method)]
public class RoyalMailFormatMessageAttribute : Attribute, IOperationBehavior
{
    public void AddBindingParameters(OperationDescription operationDescription,
        BindingParameterCollection bindingParameters)
    { }

    public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
    {
        var serializerBehavior = operationDescription.Behaviors.Find<XmlSerializerOperationBehavior>();

        if (clientOperation.Formatter == null)
            ((IOperationBehavior)serializerBehavior).ApplyClientBehavior(operationDescription, clientOperation);

        IClientMessageFormatter innerClientFormatter = clientOperation.Formatter;
        clientOperation.Formatter = new RoyalMailMessageFormatter(innerClientFormatter);
    }

    public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
    { }

    public void Validate(OperationDescription operationDescription) { }
}

Most of this is ceremony and boilerplate code. The key code pieces are OnWriteStartEnvelope where the actual namespaces are hooked up, SerializeRequest where the formatter is hooked into the WCF pipeline and ApplyClientBehavior where the message formatter is attached to the actual operation.

To hook this up, I added the attribute to client method on the service interface - in this case in my generated WCF client in Reference.cs.

    // CODEGEN: Generating message contract since the operation cancelShipment is neither RPC nor document wrapped.
    [System.ServiceModel.OperationContractAttribute(Action="cancelShipment", ReplyAction="*")]
    [System.ServiceModel.FaultContractAttribute(typeof(MarvelPress.Workflow.Business.RoyalShippingApi.exceptionDetails), Action="cancelShipment", Name="exceptionDetails")]
    [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)]
    [System.ServiceModel.ServiceKnownTypeAttribute(typeof(contactMechanism))]
    [System.ServiceModel.ServiceKnownTypeAttribute(typeof(baseRequest))]
    [RoyalMailFormatMessage()]
    MarvelPress.Workflow.Business.RoyalShippingApi.cancelShipmentResponse1 cancelShipment(MarvelPress.Workflow.Business.RoyalShippingApi.cancelShipmentRequest1 request);

Messages generated from WCF now look as expected with the namespaces all defined at the top of the document:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:oas="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:v2="http://www.royalmailgroup.com/api/ship/V2" xmlns:v1="http://www.royalmailgroup.com/integration/core/V1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <s:Header xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
    <h:Security>...</h:Security>
  </s:Header>
  <soapenv:Body>
    <v2:cancelShipmentRequest>
      <v2:integrationHeader>
        <v1:dateTime>2016-04-02T01:04:50.4122473Z</v1:dateTime>
        <v1:version>2</v1:version>
        <v1:identification>
          <v1:applicationId>RMG-API-G-01</v1:applicationId>
          <v1:transactionId>fshrxevdnc7n</v1:transactionId>
        </v1:identification>
      </v2:integrationHeader>
      <v2:cancelShipments>
        <v2:shipmentNumber>TTT001908905GB</v2:shipmentNumber>
      </v2:cancelShipments>
    </v2:cancelShipmentRequest>
  </soapenv:Body>
</soapenv:Envelope>    

For more info and a generic namespace adding formatter check out my related blog post here: http://weblog.west-wind.com/posts/2016/Apr/02/Custom-Message-Formatting-in-WCF-to-add-all-Namespaces-to-the-SOAP-Envelope

like image 85
Rick Strahl Avatar answered Oct 06 '22 00:10

Rick Strahl