Model always null on XML POST
Two things:
-
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
-
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 yourWebApiConfig.Register
:config.Formatters.XmlFormatter.UseXmlSerializer = true;
Then you don't need the namespace in your XML data:
<TestModel><Output>Sito</Output></TestModel>
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.