views:

452

answers:

1

I am new to WCF and created a simple REST service to accept an order object (series of strings from XML file), insert that data into a database, and then return an order object that contains the results. To test the service I created a small web project and send over a stream created from an xml doc.

The problem is that even though all of the items in the xml doc get placed into the stream, the service is nullifying some of them when it receives the data. For example lineItemId will have a value but shipment status will show null. I step through the xml creation and verify that all the values are being sent. However, if I clear the datamembers and change the names around, it can work. Any help would be appreciated.

This is the interface code

 [ServiceContract(Namespace="http://companyname.com/wms/")]
public interface IShipping
{

    [OperationContract]
    [WebInvoke(Method = "POST", UriTemplate = "/Orders/UpdateOrderStatus/", BodyStyle=WebMessageBodyStyle.Bare)]
    ReturnOrder UpdateOrderStatus(Order order);
}


[DataContract(Namespace="http://companyname.com/wms/order")]
public class Order
{
    [DataMember]
    public string lineItemId { get; set; }

    [DataMember]
    public string shipmentStatus { get; set; }

    [DataMember]
    public string trackingNumber { get; set; }

    [DataMember]
    public string shipmentDate { get; set; }

    [DataMember]
    public string delvryMethod { get; set; }

    [DataMember]
    public string shipmentCarrier { get; set; }
}

[DataContract]
public class ReturnOrder
{
    [DataMember(Name = "Result")]
    public string Result { get; set; }

}

This is what I'm using to send over an Order object:

string lineId = txtLineItem.Text.Trim();
    string status = txtDeliveryStatus.Text.Trim();
    string TrackingNumber = "1x22-z4r32";
    string theMethod = "Ground";
    string carrier = "UPS";
    string ShipmentDate = "04/27/2010";

    XNamespace nsOrders = "http://tempuri.org/order";
    XElement myDoc =
        new XElement(nsOrders + "Order",
            new XElement(nsOrders + "lineItemId", lineId),
            new XElement(nsOrders + "shipmentStatus", status),
            new XElement(nsOrders + "trackingNumber", TrackingNumber),
            new XElement(nsOrders + "delvryMethod", theMethod),
            new XElement(nsOrders + "shipmentCarrier", carrier),
            new XElement(nsOrders + "shipmentDate", ShipmentDate)
    );

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://localhost:3587/Deposco.svc/wms/Orders/UpdateOrderStatus/");
    request.Method = "POST";
    request.ContentType = "application/xml";

    try
    {
        request.ContentLength = myDoc.ToString().Length;
        StreamWriter sw = new StreamWriter(request.GetRequestStream());
        sw.Write(myDoc);
        sw.Close();

        using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
        {

            StreamReader reader = new StreamReader(response.GetResponseStream());
            string responseString = reader.ReadToEnd();

            XDocument.Parse(responseString).Save(@"c:\DeposcoSvcWCF.xml");
        }

    }
    catch (WebException wEx)
    {
        Stream errorStream = ((HttpWebResponse)wEx.Response).GetResponseStream();
        string errorMsg = new StreamReader(errorStream).ReadToEnd();
    }

Bindings from Web.Config

<system.serviceModel>
    <services>
        <service behaviorConfiguration="DesposcoService.ShippingServiceBehavior" name="DesposcoService.ShippingService">
            <endpoint address="wms" binding="webHttpBinding" contract="DesposcoService.IShipping" behaviorConfiguration="REST" bindingNamespace="http://companyname.com/wms"&gt;
                <identity>
                    <dns value="localhost"/>
                </identity>
            </endpoint>
            <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
        </service>
    </services>
    <behaviors>
        <serviceBehaviors>
            <behavior name="DesposcoService.ShippingServiceBehavior">
                <!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
                <serviceMetadata httpGetEnabled="true"/>
                <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
                <serviceDebug includeExceptionDetailInFaults="true"/>
            </behavior>
        </serviceBehaviors>
        <endpointBehaviors>
            <behavior name="REST">
                <webHttp/>
            </behavior>
        </endpointBehaviors>
    </behaviors>
</system.serviceModel>
+2  A: 

I figured this out (apparently at around the same time James did).

The issue is with the DataContractSerializer, and here is a test case that reproduces it:

class Program
{
    static void Main(string[] args)
    {
        XNamespace ns = "http://tempuri.org/";
        XElement element =
            new XElement(ns + "MyRequest",
                new XElement(ns + "ID", 5),
                new XElement(ns + "Name", "Test"),
                new XElement(ns + "Description", "This is a test"));

        DataContractSerializer serializer = new
            DataContractSerializer(typeof(MyRequest));
        using (XmlReader reader = element.CreateReader())
        {
            MyRequest request = (MyRequest)serializer.ReadObject(reader);
            Console.WriteLine("ID: {0}, Name: {1}, Description: {2}",
                request.ID, request.Name, request.Description);
        }
        Console.ReadLine();
    }

    [DataContract(Name = "MyRequest", Namespace = "http://tempuri.org/")]
    public class MyRequest
    {
        [DataMember]
        public int ID { get; set; }

        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public string Description { get; set; }
    }
}

If you run this, you'll see that it comes up empty for the Description property.

This happens because the DataContractSerializer expects members to be in alphabetical order. This works fine when you are using the DataContractSerializer for both the client and service... not so great when you're manually generating XML.

If you add Order properties to the DataMember attributes, it works:

    [DataContract(Name = "MyRequest", Namespace = "http://tempuri.org/")]
    public class MyRequest
    {
        [DataMember(Order = 0)]
        public int ID { get; set; }

        [DataMember(Order = 1)]
        public string Name { get; set; }

        [DataMember(Order = 2)]
        public string Description { get; set; }
    }

This time it finds the Description and all other fields.

So to resolve the issue, you can do either of the following:

  • Add Order arguments to the DataMember attributes to match the order in which you actually plan to generate XML; or

  • Make sure you add elements in alphabetical order (by element name) on the client side.

I'm not particularly fond of either of these workarounds. They seem hackish and easy to break. I think for POX services I'd prefer to use the XmlSerializer instead of the DataContractSerializer since it's less finicky about things like that, but it doesn't quite seem to work out of the box with webHttpBinding. Something worth investigating when there's more time.

Aaronaught
I do actually have namespaces defined for the service contract and data contract. They had company info in them so I removed them for posting. After removing them all though, I get bad request 400 from the server each trime I try and submit to it.
James
@James: Can you include that information (anonymized if necessary) in the question? Also post what bindings/configurations you're using - it's hard to diagnose without a complete reproducible case.
Aaronaught
Sure, once i figure out where to go to edit the question I will add it (I'm new to stack overflow)
James
@James: It's right under the tags.
Aaronaught
ok I've updated it but replaced the company name with companyname. Thank you for your info thus far and please let me know if I can add anything.
James
@James: Able to reproduce it now. One thing I'm noticing is that if you change the `Order` to an `XElement` in the service, you do see the full XML, so the values are definitely coming across. Something's happening with the serialization.
Aaronaught
Thank you Aaron, that's great insight. I was able to see that as well. At least it takes 1 question out of the equation (is the data getting there).
James
Thanks to your info aaron I solved it. WCF deserializes in alphabetical order. Therefore it was putting deliverMethod first, etc, etc. If the xml isn't sent over in the order it expects, the values are null and it only take the values in the "correct" positions. Thank you for your help.
James
Wow interesting result! Anyone raised this as a bug with MS?
Rob Stevenson-Leggett
@Rob: Once you know what to search for (alphabetical order), it seems to be a well-known behaviour. This would have to be by design; a serializer isn't going to "accidentally" reorder all the fields alphabetically. *Why* they designed it that way is more of a mystery...
Aaronaught