Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Model always null on XML POST

I'm currently working on an integration between systems and I've decided to use WebApi for it, but I'm running into an issue...

Let's say I have a model:

public class TestModel {     public string Output { get; set; } } 

and the POST method is:

public string Post(TestModel model) {     return model.Output; } 

I create a request from Fiddler with the header:

User-Agent: Fiddler Content-Type: "application/xml" Accept: "application/xml" Host: localhost:8616 Content-Length: 57 

and body:

<TestModel><Output>Sito</Output></TestModel> 

The model parameter in the method Post is always null and I have no idea why. Does anyone have a clue?

like image 660
Peter Trobec Avatar asked Dec 28 '12 10:12

Peter Trobec


2 Answers

Two things:

  1. You don't need quotes "" around the content type and accept header values in Fiddler:

    User-Agent: Fiddler Content-Type: application/xml Accept: application/xml 
  2. Web API uses the DataContractSerializer by default for xml serialization. So you need to include your type's namespace in your xml:

    <TestModel  xmlns="http://schemas.datacontract.org/2004/07/YourMvcApp.YourNameSpace">      <Output>Sito</Output> </TestModel>  

    Or you can configure Web API to use XmlSerializer in your WebApiConfig.Register:

    config.Formatters.XmlFormatter.UseXmlSerializer = true; 

    Then you don't need the namespace in your XML data:

     <TestModel><Output>Sito</Output></TestModel> 
like image 200
nemesv Avatar answered Sep 19 '22 12:09

nemesv


While the answer is already awarded, I found a couple other details worth considering.

The most basic example of an XML post is generated as part of a new WebAPI project automatically by visual studio, but this example uses a string as an input parameter.

Simplified Sample WebAPI controller generated by Visual Studio

using System.Web.Http; namespace webAPI_Test.Controllers {     public class ValuesController : ApiController     {         // POST api/values         public void Post([FromBody]string value)         {         }     } } 

This is not very helpful, because it does not address the question at hand. Most POST web services have rather complex types as parameters, and likely a complex type as a response. I will augment the example above to include a complex request and complex response...

Simplified sample but with complex types added

using System.Web.Http; namespace webAPI_Test.Controllers {     public class ValuesController : ApiController     {         // POST api/values         public MyResponse Post([FromBody] MyRequest value)         {             var response = new MyResponse();             response.Name = value.Name;             response.Age = value.Age;             return response;         }     }      public class MyRequest     {         public string Name { get; set; }         public int Age { get; set; }     }      public class MyResponse     {         public string Name { get; set; }         public int Age { get; set; }     } } 

At this point, I can invoke with fiddler..

Fiddler Request Details

Request Headers:

User-Agent: Fiddler Host: localhost:54842 Content-Length: 63 

Request Body:

<MyRequest>    <Age>99</Age>    <Name>MyName</Name> </MyRequest> 

... and when placing a breakpoint in my controller I find the request object is null. This is because of several factors...

  • WebAPI defaults to using DataContractSerializer
  • The Fiddler request does not specify content type, or charset
  • The request body does not include XML declaration
  • The request body does not include namespace definitions.

Without making any changes to the web service controller, I can modify the fiddler request such that it will work. Pay close attention to the namespace definitions in the xml POST request body. Also, ensure the XML declaration is included with correct UTF settings that match the request header.

Fixed Fiddler request body to work with Complex datatypes

Request Headers:

User-Agent: Fiddler Host: localhost:54842 Content-Length: 276 Content-Type: application/xml; charset=utf-16 

Request body:

<?xml version="1.0" encoding="utf-16"?>    <MyRequest xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/webAPI_Test.Controllers">       <Age>99</Age>       <Name>MyName</Name>    </MyRequest> 

Notice how the namepace in the request refers to the same namespace in my C# controller class (kind of). Because we have not altered this project to use a serializer other than DataContractSerializer, and because we have not decorated our model (class MyRequest, or MyResponse) with specific namespaces, it assumes the same namespace as the WebAPI Controller itself. This is not very clear, and is very confusing. A better approach would be to define a specific namespace.

To define a specific namespace, we modify the controller model. Need to add reference to System.Runtime.Serialization to make this work.

Add Namespaces to model

using System.Runtime.Serialization; using System.Web.Http; namespace webAPI_Test.Controllers {     public class ValuesController : ApiController     {         // POST api/values         public MyResponse Post([FromBody] MyRequest value)         {             var response = new MyResponse();             response.Name = value.Name;             response.Age = value.Age;             return response;         }     }      [DataContract(Namespace = "MyCustomNamespace")]     public class MyRequest     {         [DataMember]         public string Name { get; set; }          [DataMember]         public int Age { get; set; }     }      [DataContract(Namespace = "MyCustomNamespace")]     public class MyResponse     {         [DataMember]         public string Name { get; set; }          [DataMember]         public int Age { get; set; }     } } 

Now update the Fiddler request to use this namespace...

Fiddler request with custom namespace

<?xml version="1.0" encoding="utf-16"?>    <MyRequest xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="MyCustomNamespace">       <Age>99</Age>       <Name>MyName</Name>    </MyRequest> 

We can take this idea even further. If a empty string is specified as the namespace on the model, no namespace in the fiddler request is required.

Controller with empty string namespace

using System.Runtime.Serialization; using System.Web.Http;  namespace webAPI_Test.Controllers {     public class ValuesController : ApiController     {         // POST api/values         public MyResponse Post([FromBody] MyRequest value)         {             var response = new MyResponse();             response.Name = value.Name;             response.Age = value.Age;             return response;         }     }      [DataContract(Namespace = "")]     public class MyRequest     {         [DataMember]         public string Name { get; set; }          [DataMember]         public int Age { get; set; }     }      [DataContract(Namespace = "")]     public class MyResponse     {         [DataMember]         public string Name { get; set; }          [DataMember]         public int Age { get; set; }     } } 

Fiddler request with no namespace declared

<?xml version="1.0" encoding="utf-16"?>    <MyRequest>       <Age>99</Age>       <Name>MyName</Name>    </MyRequest> 

Other Gotchas

Beware, DataContractSerializer is expecting the elements in the XML payload to be ordered alphabetically by default. If the XML payload is out of order you may find some elements are null (or if datatype is an integer it will default to zero, or if it is a bool it defaults to false). For example, if no order is specified and the following xml is submitted...

XML body with incorrect ordering of elements

<?xml version="1.0" encoding="utf-16"?> <MyRequest>    <Name>MyName</Name>    <Age>99</Age> </MyRequest>   

... the value for Age will default to zero. If nearly identical xml is sent ...

XML body with correct ordering of elements

<?xml version="1.0" encoding="utf-16"?> <MyRequest>    <Age>99</Age>    <Name>MyName</Name> </MyRequest>   

then the WebAPI controller will correctly serialize and populate the Age parameter. If you wish to change the default ordering so the XML can be sent in a specific order, then add the 'Order' element to the DataMember Attribute.

Example of specifying a property order

using System.Runtime.Serialization; using System.Web.Http;  namespace webAPI_Test.Controllers {     public class ValuesController : ApiController     {         // POST api/values         public MyResponse Post([FromBody] MyRequest value)         {             var response = new MyResponse();             response.Name = value.Name;             response.Age = value.Age;             return response;         }     }      [DataContract(Namespace = "")]     public class MyRequest     {         [DataMember(Order = 1)]         public string Name { get; set; }          [DataMember(Order = 2)]         public int Age { get; set; }     }      [DataContract(Namespace = "")]     public class MyResponse     {         [DataMember]         public string Name { get; set; }          [DataMember]         public int Age { get; set; }     } } 

In this example, the xml body must specify the Name element before the Age element to populate correctly.

Conclusion

What we see is that a malformed or incomplete POST request body (from perspective of DataContractSerializer) does not throw an error, rather is just causes a runtime problem. If using the DataContractSerializer, we need to satisfy the serializer (especially around namespaces). I have found using a testing tool a good approach - where I pass an XML string to a function which uses DataContractSerializer to deserialize the XML. It throws errors when deserialization cannot occur. Here is the code for testing an XML string using DataContractSerializer (again, remember if you implement this, you need to add a reference to System.Runtime.Serialization).

Example Testing Code for evaluation of DataContractSerializer de-serialization

public MyRequest Deserialize(string inboundXML) {     var ms = new MemoryStream(Encoding.Unicode.GetBytes(inboundXML));     var serializer = new DataContractSerializer(typeof(MyRequest));     var request = new MyRequest();     request = (MyRequest)serializer.ReadObject(ms);      return request; } 

Options

As pointed out by others, the DataContractSerializer is the default for WebAPI projects using XML, but there are other XML serializers. You could remove the DataContractSerializer and instead use XmlSerializer. The XmlSerializer is much more forgiving on malformed namespace stuff.

Another option is to limit requests to using JSON instead of XML. I have not performed any analysis to determine if DataContractSerializer is used during JSON deserialization, and if JSON interaction requires DataContract attributes to decorate the models.

like image 25
barrypicker Avatar answered Sep 21 '22 12:09

barrypicker