1

Accordingly to MSDN docs:

You can also serialize an array as a flat sequence of XML elements by applying a XmlElementAttribute to the field returning the array as follows.

Desired XML schema:

<xs:element minOccurs="0" maxOccurs="unbounded" name="ResponseLineTest" type="OrderLn" />

C#:

 [XmlElementAttribute("ResponseLineTest")]
 public OrderLn[] ResponseLine { get; set; }

However: Using .NET 4.51 I get this schema:

 <xs:element minOccurs="0" name="ResponseLine" nillable="true" type="tns:ArrayOfOrderLn"/>

https://learn.microsoft.com/en-us/dotnet/standard/serialization/controlling-xml-serialization-using-attributes#serializing-an-array-as-a-sequence-of-elements

HOW do I mark my C# classes and properties, so the WSDL output looks as above (and the documentation) ?

dbc
  • 104,963
  • 20
  • 228
  • 340
Christian
  • 394
  • 2
  • 17
  • Is any wsdl with xsd definitions available? PHP is a bit tricky with it 's own SoapClient class. Often you have to work with PHP SoapVar Objects, to get the right xml structure. – Marcel Jan 09 '20 at 08:54
  • There are XSD's from the worldstandard creators (ediWheel btw), but as I have learned using xsd2code - these xsd's can be interpretated in a great many ways. Just to clarify. I'm not the one doing the PHP part. Our clients are. I return the strongly typed DataClass. I test using WSDLBrowser.com – Christian Jan 09 '20 at 08:57
  • or did you ask if I the WSDL is available somewhere? if so yes. you can even get it yourself from my published webservice :) – Christian Jan 09 '20 at 09:00
  • Is the above shown PHP output a request your clients are creating or is it a response your service sends back to your client? – Marcel Jan 09 '20 at 09:04
  • I'm focusing on the response. However; I tried calling my SOAP service with and without the *parent* **ORDERLINE**. Works fine either way. – Christian Jan 09 '20 at 09:17
  • I'm so desperate I'm looking at the source code for PHP SaveXML() – Christian Jan 10 '20 at 07:44
  • Hey Christian, normally it is not necessary using the DomDocument::saveXML() method to create the xml structure for the php soap client. It is recommended working with entities (objects) and their SoapVar counterparts. The soap client will translate it into valid xml. Can you give us your wsdl file, so that I can give you a small valid php example on how to work with the SoapClient class? – Marcel Jan 10 '20 at 07:58
  • The thing is Marcel; I'm not the one doing the PHP part - our clients are. I'm strictly C# here and trying to figure out how to deliver the XML in the right format, so the PHP writer/savexml/toXml/** on their end intepretates it correct. Clients tell me they *just* print out the response. – Christian Jan 10 '20 at 08:34
  • Bad example, t'is better: https://wsdlbrowser.com/p/2f356287286ea2eb58018861d3a0185f – Christian Jan 10 '20 at 08:55
  • For clarification !!!! I did not build the wsdlbrowser - its a random test page I found, that reproduces the errors I'm TOLD our clients are experiencing. this is the only place I can reproduce the error. – Christian Jan 10 '20 at 08:56
  • *However: Using .NET 4.51 I get this:* -- how are you generating that? Is it the WSDL for a [tag:wcf] service you are writing? – dbc Jan 13 '20 at 19:47
  • I cleaned up the question yesterday, but accident I deleted the test link. Youcan see the result here: https://wsdlbrowser.com/p/c1434add9d01af73dab232db5231d658 – Christian Jan 14 '20 at 07:29

2 Answers2

1

Necessary changes in your webservice definition

First of all the definition of the necessary complex type PlaceOrder does not mention an element of the type tns:OrderLine. I guess you have to change the definition of the element. Because your webservice is very loosely defined, it works like you 've shown in your question.

Here 's the current definition of the PlaceOrder request. This says, that the PlaceOrder element is required as request parameter.

<wsdl:message name="IService_PlaceOrder_InputMessage">
    <wsdl:part name="parameters" element="tns:PlaceOrder"/>
</wsdl:message>

The current definition of the PlaceOrder complex type shows, that there is no OrderLine element.

<xs:element name="PlaceOrder">
    <xs:complexType>
        <xs:sequence>
            <xs:element minOccurs="0" name="userToken" nillable="true" type="xs:string"/>
            <xs:element minOccurs="0" name="ediOrder" nillable="true" type="q1:order"/>
        </xs:sequence>
    </xs:complexType>
</xs:element>

This means, that you can send everything in addition. Your webservice does not know an OrderLine element in the PlaceOrder context because it is not defined here. You have to change the definition of the PlaceOrder element into the following notation.

<xs:element name="PlaceOrder">
    <xs:complexType>
        <xs:sequence>
            <xs:element minOccurs="0" name="userToken" nillable="true" type="xs:string"/>
            <xs:element minOccurs="0" name="ediOrder" nillable="true" type="q1:order"/>
            <xs:element minOccurs="0" name="OrderLine" nillable="true" type="q1:ArrayOfOrderLine"/>
        </xs:sequence>
    </xs:complexType>
</xs:element>

The definition of ArrayOfOrderLine is defined as follows:

<xs:complexType name="ArrayOfOrderLine">
    <xs:sequence>
        <xs:element minOccurs="0" maxOccurs="unbounded" name="OrderLine" nillable="true" type="tns:OrderLine"/>
    </xs:sequence>
</xs:complexType>

This definition says, that you want the OrderLine complex types with a parent node OrderLine. So the parent node occurs exactly as defined in your wsdl file. To omit the parent node you have to redefine the PlaceOrder complex type as follows:

<xs:element name="PlaceOrder">
    <xs:complexType>
        <xs:sequence>
            <xs:element minOccurs="0" name="userToken" nillable="true" type="xs:string"/>
            <xs:element minOccurs="0" name="ediOrder" nillable="true" type="q1:order"/>
            <xs:element minOccurs="0" maxOccurs="unbounded" name="OrderLine" nillable="true" type="tns:OrderLine"/>
        </xs:sequence>
    </xs:complexType>
</xs:element>

This new definition shows that the "OrderLine" element cannot be named or can be named more than once. The parent node in this case is PlaceOrder.

A possible PHP example

Soap follows a strictly object-oriented approach. Based on this understanding, you also have to work with objects in PHP. First you need value objects (sometimes called entities) based on your xsd/wsdl definition. Keep in mind, that this example uses the redefined PlaceOrder definition.

<?php
namespace Webservice\Entity;
use ArrayObject;
use SoapVar;

class PlaceOrder
{
    protected $userToken;
    protected $ediOrder;
    protected $OrderLine;

    public function __construct()
    {
        $this->OrderLine = new ArrayObject();
    }

    public function getUserToken(): ?SoapVar
    {
        return $this->userToken;
    }

    public function setUserToken(SoapVar $userToken): self
    {
        $this->userToken = $userToken;
        return $this;
    }

    public function getEdiOrder() : ?SoapVar
    {
        return $this->ediOrder;
    }

    public function setEdiOrder(SoapVar $ediOrder): self
    {
        $this->ediOrder = $ediOrder;
        return $this;
    }

    public function getOrderLine(): ArrayObject
    {
        return $this->OrderLine;
    }

    public function attachOrderLine(SoapVar $orderLine): self
    {
        $this->orderLine->append($orderLine);
        return $this;
    }

    public function setOrderLine(ArrayObject $orderLine): self
    {
        $this->OrderLine = $orderLine;
        return $this;
    }
}

The above shown PHP code shows the PlaceOrder value object. As you can see all elements, which are defined in your webservice definition, occur as properties of this class. This class is an exact php implementation of the PlaceOrder complex type. You can say that all complex types are always PHP classes. Further the accepted method parameters are mainly SoapVar instances. This is important for the soap client because this guarantees the right xml structure at the end.

The OrderLine value object ...

<?php
namespace Webservice\Entity;

class OrderLine 
{
    protected $AdditionalCustomerReferenceNumber;
    protected $LineID;
    protected $OrderedArticle;
    protected $PortalReference;

    // getters and setters here
}

With this two classes one can do a full webservice call with PHP. The following example is not testet and shows how to work with the PHP SoapClient class. The class is sometimes a bit deceptive and it takes a bit of work to get the right result at the end. But mainly this is the way how to work it.

<?php
namespace Wesbervice;
use Webservice\Entity\Order;
use Webservice\Entity\OrderLine;
use Webservice\Entity\PlaceOrder;
use SoapFault;
use SoapVar;

try {
    // this url contains the wrong defined PlaceOrder complex type
    $wsdl = 'https://uat-salesapi.ndias.com/service.svc?singlewsdl&version=27';

    $client = new SoapClient($wsdl, [
        'cache_wsdl' => WSDL_CACHE_NONE, // as long as you work on your wsdl
        'encoding' => 'utf-8',
        'exceptions' => true,
        'soap_version' => SOAP_1_1,
        'trace' => true, // enables tracing and __getLastRequest() / __getLastResponse()
        'classmap' => [
            'order' => Order::class,
            'OrderLine' => OrderLine::class,
            'PlaceOrder' => PlaceOrder::class,    
        ],
    ]);

    // user token
    $usertoken = new SoapVar('bla', XSD_STRING, '', '', 'userToken', 'http://tempuri.org/');

    // edi order
    $order = (new Order())
        ->setBlanketOrderReference(new SoapVar(...))
        ->setBuyerParty(new SoapVar(...);

    $order = new SoapVar($order, SOAP_ENC_OBJECT, '', '', 'ediOrder', 'http://tempuri.org/');

    // compile our request parameter
    $parameter = (new PlaceOrder())
        ->setUserToken($usertoken)
        ->setEdiOrder($order);

    // order list objects
    $orderLine1 = (new OrderLine())
        ->setAdditionalCustomerReferenceNumber(new SoapVar(...))
        ->setLineID(new SoapVar(...));

    $orderLine1 = new SoapVar($orderLine1, SOAP_ENC_OBJECT, '', '', 'OrderLine', 'http://tempuri.org/');

    $parameter->attachOrderLine($orderLine1);

    $orderLine2 = (new OrderLine())
        ->setAdditionalCustomerReferenceNumber(new SoapVar(...))
        ->setLineID(new SoapVar(...));

    $orderLine2 = new SoapVar($orderLine2, SOAP_ENC_OBJECT, '', '', 'OrderLine', 'http://tempuri.org/');

    $parameter->attachOrderLine($orderLine2);

    $parameter = new SoapVar($parameter, SOAP_ENC_OBJECT, '', '', 'PlaceOrder', 'http://tempuri.org/');

    // the client knows the PlaceOrder method from the wsdl
    $result = $client->PlaceOrder($parameter);

    // the result is a stdClass structure, als long as the classmap parameter does not contain definitions of type to php entity classes
    echo "<pre>";
    var_dump($result);
    echo "</pre>";
} catch (SoapFault $fault) {
    echo "<pre>";
    var_dump($fault);
    echo "</pre>";
}

Conclusion

Your web service is very imprecisely defined. For this reason, you should simply rethink the definitions for the parameters and define them more precisely in the WSDL file. Then it works with PHP, too. PHP uses strictly web standards in its soap extension.

Marcel
  • 4,854
  • 1
  • 14
  • 24
  • Oh dear; I'm afraid I've been fooling around with that WSDL all day, there have been many versions from minute to minute, some with orderline defined, some without to debug different scenarios. Orderline is there now. I have for debugging, added a very simple method called PlaceOrderTestNoInput. Could you check if this method is having same imprecise definitions? – Christian Jan 10 '20 at 12:36
  • You took it in a bad state, I apologise for this. I see your suggestions in there on orderline. just as you describe, I'm missing something obvious. – Christian Jan 10 '20 at 12:40
  • I just went through the newest WSDL. I'd state PlaceOrder right now is exactly as you describe. To no avail though. – Christian Jan 10 '20 at 12:43
  • Well, in your example you want the `OrderLine` node could have the following states: nillable (not mentioned in the request), zero or n-times occurances. Therefore you can add the following attributes in the xsd definition: `<... nillable="true">` (element can be empty), `<... minOccurs="0">` (element must appear 0 times) and `<... maxOccurs="unbounded">` (element can appear n-times). Do not change the maxOccurs attribute to 0. That would mean that the element may appear a maximum of 0 times. – Marcel Jan 10 '20 at 13:07
  • Thank you, you have helped me a lot, by confirming how the WSDL should look like and this has put my mind at ease. It has not changed the output on the PHP SOAP client side. We are, as they say, back to where we started. – Christian Jan 13 '20 at 06:25
  • In that case it would be nice, how the PHP SoapClient was implemented. I guess, that the implementation of the webservice on the client side is the real problem. – Marcel Jan 13 '20 at 08:04
  • It cant be the Client - I think PHP has a bug on handling some of the details in the WSDL. I have two different clients using soap client with same result. – Christian Jan 13 '20 at 08:12
  • I'm leaning towards .NET having a bug here. If I add [XmlElementAttribute] to the array property, it is, accordingly to the docs, supposed to *flatten* the array: https://learn.microsoft.com/en-us/dotnet/standard/serialization/controlling-xml-serialization-using-attributes#serializing-an-array-as-a-sequence-of-elements – Christian Jan 13 '20 at 09:29
  • Adding [XmlElement] does not change property type from ArrayOfOrderLine to OrderLine in the WSDL. – Christian Jan 13 '20 at 10:33
1

TL;DR Your problem is that .NET has two separate XML serializers, XmlSerializer and DataContractSerializer, and you are creating a WCF service, which uses DataContractSerializer by default. You need to switch to using XmlSerializer by applying XmlSerializerFormatAttribute to your service contract.

Details as follows. Say you have the following WCF service contract (not shown in your question):

public class Output
{
    [XmlElementAttribute("ResponseLineTest")]
    public OrderLn[] ResponseLine { get; set; }
}

public class OrderLn
{
    public string Order { get; set; }
}

[ServiceContract(Namespace = "Question59659046")]
[XmlSerializerFormat]
public interface IQuestion59659046Service
{
    [OperationContract]
    Output GetOutput(string input);
}

You would like the XML generated by this service to have a schema that includes a repeating sequence of <ResponseLineTest> elements, e.g. like the following:

  <xs:complexType name="Output">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="unbounded" name="ResponseLineTest" type="tns:OrderLn" />
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="OrderLn">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="1" name="Order" type="xs:string" />
    </xs:sequence>
  </xs:complexType>

And so you apply the XmlElement attribute to ResponseLine to force it to be serialized to XML in this manner. But, when you generate a WSDL for your service, you instead get a schema that looks like:

  <xs:complexType name="Output">
    <xs:sequence>
      <xs:element minOccurs="0" name="ResponseLine" nillable="true" type="tns:ArrayOfOrderLn" />
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="ArrayOfOrderLn">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="unbounded" name="OrderLn" nillable="true" type="tns:OrderLn" />
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="OrderLn">
    <xs:sequence>
      <xs:element minOccurs="0" name="Order" nillable="true" type="xs:string" />
    </xs:sequence>
  </xs:complexType>
  <xs:element name="OrderLn" nillable="true" type="tns:OrderLn" />

The schema includes an extra intermediate type ArrayOfOrderLn and the overridden element name "ResponseLineTest" was not used. Apparently the [XmlElementAttribute("ResponseLineTest")] attribute was completely ignored. Why might this be?

As it turns out, this behavior is documented in Using the XmlSerializer Class, which explains that your service is not using XmlSerializer at all, but rather a different serializer that ignores the [XmlElement] attribute:

By default WCF uses the DataContractSerializer class to serialize data types.

<<snip>>

At times, you may have to manually switch to the XmlSerializer. This happens, for example, in the following cases:

  • When precise control over the XML that appears in messages is important...

In this case, precise control over the XML is required, specifically to force the ResponseLine property to be serialized without an outer container element. However, serializing a collection without an outer container element is not supported by DataContractSerializer, so you must switch to using XmlSerializer by applying [XmlSerializerFormat] to your service contract:

[ServiceContract(Namespace = "Question59659046")]
[XmlSerializerFormat]
public interface IQuestion59659046Service
{
    [OperationContract]
    Output GetOutput(string input);
}

Now the WSDL generated for your service will be as required.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Man; you saved my day, week and probably the entire month too. I've been staring at that problem for more than a week now. Questioned my sanity etc. It works of course. I'm getting all sorts of other more natural namespace problems now. But these are all fixable. Especially now that it behaves accordingly to docs! – Christian Jan 14 '20 at 07:27