Model always null on XML POST
Asked Answered
A

5

47

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?

Arsenical answered 28/12, 2012 at 10:44 Comment(5)
how are you calling the Post method from the client side? Are you sure its an HTTP POST?Caird
Fiddler. Also, WebApi defaults the POST calls to the POST methods, GET to GET methods,...Arsenical
Yes Peter but did you select POST in the dropdown in fiddler? (it defaults to GET)Caird
Of course I did :) Like I said, the call is made to the POST method.Arsenical
A tip for figuring out what went wrong (because Web API is completely unhelpful in this regard) is simply grabbing the request content via Request.Content.ReadAsStringAsync() and trying to deserialize the XML yourself. If something is up, rather than just returning null like Web API does, XmlSerializer will throw an exception telling you why it can't deserialize. In my case, this is how I figured out that my XML declaration stated a UTF-16 encoding while the request itself was UTF-8 encoded. This is something I never would've figured out without just doing the deserialization myself.Kilah
H
74

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>
    
Hakim answered 28/12, 2012 at 12:48 Comment(7)
Awesome! The UseXmlSerializer worked! I was putting it in the Global.asax.cs file, as part of the global configuration, but it looks like it really has to be in App_Start\WebApiConfig.cs.Arsenical
I didn't know the WebAPI defaults to using DataContractSerializer - thanks for the tip!!!Pinchhit
Thanks, spent an age looking for the solution to this.Destitution
Ran into the same issue with Json. I had a model decorated with DataContract/Member attributes. Verified I could de/serialize in a unit test. Was able to POST to a WebApi controller with only HttpRequestMessage in the signature and read and deserialize the class there. The missing piece to be able to use just Post([FromBody] MyClass myClass) was to set config.Formatters.JsonFormatter.UseDataContractJsonDeserializer = true in my WebApiConfig.cs . Thank you!Selector
I've done all of this and it's not working for me...what else could be the issue?Hardi
@Hardi You might be sending a primitive type like a string or an integer array, which isn't going to work without a DTO to contain them in.Blakeslee
@Hakim I'm not sure if I made mistakes, I found the XML data actually should not contain namespace if UseXmlSerializer is set to true, not just "don't need".Aplite
P
61

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.

Pinchhit answered 20/7, 2013 at 23:42 Comment(4)
Killer answer. Amazed at how difficult it is to send/receive XML when that should be the point of a REST service!!Marissamarist
Two points - 1. I needed to add a reference to System.Net.Http.Formatting in order to set UseXmlSerializer to true. 2. I needed to add "Accept: application/xml" in my request header to be able to get XML back from the request (looks like it defaults to JSON).Marissamarist
You should get paid for this answer. Is it against SO rules to put BTC donate info in the answer?Lumpy
@Setinel - thanks for the compliment! Don't know about the SO rules. Rather than receive donated money I like the idea of 'Pay it forward'.Pinchhit
N
3

Once you make sure that you setup the Content-Type header to application/xml and set config.Formatters.XmlFormatter.UseXmlSerializer = true; in the Register method of the WebApiConfig.cs, it is important that you will not need any versioning or encoding at the top of your XML document.

This last piece was getting me stuck, hope this helps somebody out there and saves your time.

Namesake answered 3/1, 2017 at 17:34 Comment(0)
G
2

I was trying to solve this for two days. Eventually I found out the outer tag needs to be the type name, not the variable name. Effectively, with the POST method as

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

I was providing the body

<model><Output>Sito</Output></model>

instead of

<TestModel><Output>Sito</Output></TestModel>
Gormley answered 1/12, 2017 at 7:28 Comment(0)
E
0

For me it was having more than one xmlFormatter added to the config.

While debugging I discovered the list of formatters, with a duplicate one.

config.Formatters.Add(new XmlMediaTypeFormatter());

Removed that line and it worked.

Files to check for that line

  • Global.acax.cs
  • WebApiConfig.cs
Elect answered 10/11, 2020 at 11:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.