views:

359

answers:

3

We've observed that when we expose a WCF service which uses classes decorated with various xml serialisation attributes, despite the fact that we use the XmlSerializerFormat attribute on the interface any XmlRoot attribute on any of the operation's parameters gets completely ignored. The namespace of the parameters is always that of the service and not what we specify.

This is causing us problems as it does not seem to be backwards compatible with ASMX and also because we're using BizTalk, and need to have tighter control over the shape of the XML's exchanged.

A few questions then -

  1. Anybody knows what is the rationale behind this decision?
  2. Anybody knows how this is happening? I was under the impressions that WCF, with the XmlSerializerFormat attribute, uses the XmlSerialiser to serialise the types, which would suggest XmlRoot should be taken into account, how come this is not the case? (is it only due to the fact that, taking the SOAP envelope into account, the parameter is not root?)
  3. Most importantly - anybody knows if there's a way to 'force the issue' - i.e. get the parameters to be of the namespace of our choosing?

I've seen this post, but I don't believe it is relevant to my question -

As per Wagner Silveira's request - the contracts I used to test this are -

[ServiceContract(Namespace="http://servicecontract"), XmlSerializerFormat(Style=OperationFormatStyle.Document)]
public interface ITestService
{
    [OperationContract]
    MyOtherType MyTestMethod(MyType obj);
}

// Composite class for DCS and XMLS
[Serializable, XmlType, XmlRoot(Namespace = "http://datacontract")] 
public class MyType
{
    [XmlAttribute]
    public string StringValue { get; set; }
}

// Composite class for DCS and XMLS
[Serializable, XmlType, XmlRoot(Namespace = "http://datacontract")]
public class MyOtherType
{
    [XmlAttribute]
    public string OtherStringValue { get; set; }
}
A: 

Hi, can you post a sample of your contracts, so we understand better what you are trying?

Wagner Silveira
This should really be a comment, not an answer.
John Saunders
+1  A: 

I don't know why WCF ignores XmlRoot, so I can't answer that part of your question. But I do have a couple ways to solve the problem.

  1. start with WSDL first.
    If you have a particular set of XML namespaces you would like to apply to the messages that get sent and receieved, use WSDL and XML Schema to explicitly specify them.

    Then, generate the Server-side stub code, or the client-side proxy code, directly from that WSDL via the svcutil.exe tool.

  2. use a custom ServiceHost
    The other option open to you, described at this link, is to use a custom ServiceHost that overrides WCF's decision to disregard the XmlRoot or XmlType attributes on message types.


If you choose to go for the WSDL-First approach, the WSDL should look like this:

<?xml version="1.0" encoding="utf-8" ?>

<definitions
    xmlns="http://schemas.xmlsoap.org/wsdl/"
    targetNamespace="urn:The-Service-namespace"
    xmlns:tns="urn:The-Service-namespace"
    xmlns:s="http://www.w3.org/2001/XMLSchema"
    xmlns:n0="urn:The-Request-namespace"
    xmlns:n1="urn:The-Response-namespace"
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    elementFormDefault= "unqualified"
  >

    <types>
      <s:schema targetNamespace="urn:The-Request-namespace" >
        <s:complexType name="Type1">
          <s:sequence>
            <s:element name="x" minOccurs="1" maxOccurs="1" type="s:string"/>
          </s:sequence>
        </s:complexType>
        <s:element name="Type1" type="n0:Type1" />
      </s:schema>


      <s:schema targetNamespace="urn:The-Response-namespace" >
        <s:complexType name="Type2">
          <s:sequence>
            <s:element name="x" minOccurs="1" maxOccurs="1" nillable="false" type="s:string"/>
            <s:element name="y" minOccurs="1" maxOccurs="1" nillable="false" type="s:int"/>
            <s:element name="z" minOccurs="1" maxOccurs="1" nillable="false" type="s:boolean" />
          </s:sequence>
        </s:complexType>
        <s:element name="Type2" type="n1:Type2" />
      </s:schema>

    </types>



<message name="RequestMessage">
   <part name="inPart1" element="n0:Type1" />
</message>
<message name="ResponseMessage">
   <part name="outPart1" element="n1:Type2" />
</message>



<portType name="PortTypeName">
  <operation name="Method1">
      <input message="tns:RequestMessage" />
      <output message="tns:ResponseMessage" />
   </operation>
</portType>



<binding name="InterfaceName" type="tns:PortTypeName">
    <soap:binding
       transport="http://schemas.xmlsoap.org/soap/http"
       style="rpc" />

    <operation name="Method1">
        <soap:operation soapAction="" style="document" />
        <input>  <soap:body use="literal" /> </input>
        <output> <soap:body use="literal" /> </output>
    </operation>
</binding>

</definitions>

This WSDL is very simple - it defines a single operation, with a single request message and a single response message.

Notice there are three xml namespaces:

  • urn:The-Service-namespace
    used for the element that wraps the request and response - the first element inside the <SOAP:body>
  • urn:The-Request-namespace
    used for the element wrapped inside that request wrapper, which gets deserialized into an instance of Type1.
  • urn:The-Response-namespace
    used for the element wrapped inside that response wrapper, which gets deserialized into an instance of Type2.

If your web services interface is more complicated, has more operations and consequently more request and response message types, you can add more namespaces, if you like, for all those additional types.

Cheeso
Thanks Cheeso. I am familiar with the custom host approach, and in fact - this is what we've done in a couple of places, but this seems to be a big ask, for a technology that carries the flag of interoperability.Anyway - the purpose of my question is to understand the behaviour.Appreciate you taking the time to answer.
Yossi Dahan
A: 

I assume you're using SOAP as the message format. In this case, the object you're serializing is not the root of the XML, the soap envelope is. So it makes sense that the XmlRoot would be ignored. By default WCF will create a message contract for you and name the response and it has the namespace of the service. What you can do is create your own message contract to have full control over SOAP.

Create the following two classes:

[MessageContract]
public class MyTestMethodRequest
{
    [MessageBodyMember( Namespace = "http://datacontract" )]
    public MyType MyType;
}

[MessageContract]
public class MyTestMethodResponse
{
    [MessageBodyMember( Namespace = "http://datacontract" )]
    public MyOtherType MyOtherType;
}

Then change the signature of your service operation to the following.

[OperationContract]
public MyTestMethodResponse MyTestMethod( MyTestMethodRequest request )
{
    return new MyTestMethodResponse {
        MyOtherType = new MyOtherType {
            OtherStringValue = "bar"
        }
    };
}

Now if you example the SOAP messages you should see the following:

Request

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&gt;
  <s:Header>
    <Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none"&gt;http://servicecontract/TestService/MyTestMethod&lt;/Action&gt;
  </s:Header>
  <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"&gt;
    <MyTestMethodRequest xmlns="http://servicecontract"&gt;
      <MyType StringValue="foo" xmlns="http://datacontract" />
    </MyTestMethodRequest>
  </s:Body>
</s:Envelope>

Response

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&gt;
  <s:Header />
  <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"&gt;
    <MyTestMethodResponse xmlns="http://servicecontract"&gt;
      <MyOtherType OtherStringValue="bar" xmlns="http://datacontract" />
    </MyTestMethodResponse>
  </s:Body>
</s:Envelope>
Josh Einstein
Thanks JohnWe figured the MessageContract option, it just seems like a lot of hassle for something that just worked in ASMX, which is also why I wasn't sure about the not-the-root node argument, but I guess that it makes sense. Personally I think that's quite a blow to backwards compatibility.
Yossi Dahan