Model always null on XML POST

前端 未结 5 1242
耶瑟儿~
耶瑟儿~ 2020-11-30 22:36

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:

相关标签:
5条回答
  • 2020-11-30 22:54

    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.

    0 讨论(0)
  • 2020-11-30 23:02

    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>
    
    0 讨论(0)
  • 2020-11-30 23:09

    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>
      
    0 讨论(0)
  • 2020-11-30 23:14

    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.

    0 讨论(0)
  • 2020-11-30 23:15

    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
    0 讨论(0)
提交回复
热议问题