模型在XML POST上始终为null

时间:2012-12-28 10:44:14

标签: c# .net asp.net-web-api http-post

我目前正致力于系统之间的集成,我决定使用WebApi,但我遇到了一个问题......

假设我有一个模特:

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

,POST方法是:

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

我用Fiddler创建了一个标题:

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

和身体:

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

方法model中的Post参数始终为null,我不知道为什么。 有没有人有线索?

5 个答案:

答案 0 :(得分:67)

两件事:

  1. 您不需要在内容类型周围引用""并接受Fiddler中的标头值:

    User-Agent: Fiddler
    Content-Type: application/xml
    Accept: application/xml
    
  2. Web API默认使用DataContractSerializer进行xml序列化。因此,您需要在xml中包含类型的命名空间:

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

    或者您可以配置Web API以在XmlSerializer中使用WebApiConfig.Register

    config.Formatters.XmlFormatter.UseXmlSerializer = true;
    

    然后,您不需要XML数据中的命名空间:

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

答案 1 :(得分:54)

虽然已经给出了答案,但我发现了其他一些值得考虑的细节。

XML文章的最基本示例是由visual studio自动生成的新WebAPI项目的一部分,但此示例使用字符串作为输入参数。

Visual Studio生成的简化示例WebAPI控制器

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

这不是很有帮助,因为它没有解决手头的问题。大多数POST Web服务具有相当复杂的类型作为参数,并且可能是复杂类型作为响应。我将扩充上面的例子,以包含复杂的请求和复杂的响应......

简化样本但添加了复杂类型

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; }
    }
}

此时,我可以用fiddler调用..

提琴请求详情

请求标题:

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

请求正文:

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

...当在我的控制器中放置断点时,我发现请求对象为空。这是因为有几个因素......

  • WebAPI默认使用DataContractSerializer
  • Fiddler请求未指定内容类型或charset
  • 请求正文不包含XML声明
  • 请求正文不包含名称空间定义。

不对Web服务控制器进行任何更改,我可以修改fiddler请求以使其正常工作。密切关注xml POST请求正文中的命名空间定义。此外,请确保XML声明包含在与请求标头匹配的正确UTF设置中。

修复了Fiddler请求主体使用复杂数据类型

请求标题:

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

请求正文:

<?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>

请注意请求中的名称空间如何引用我的C#控制器类(种类)中的相同名称空间。因为我们没有将此项目更改为使用除DataContractSerializer之外的序列化程序,并且因为我们没有使用特定名称空间修饰我们的模型(类MyRequest或MyResponse),所以它假定与WebAPI控制器本身具有相同的名称空间。这不是很清楚,而且非常令人困惑。更好的方法是定义特定的命名空间。

要定义特定的命名空间,我们修改控制器模型。需要添加对System.Runtime.Serialization的引用才能使其正常工作。

将命名空间添加到模型

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; }
    }
}

现在更新Fiddler请求以使用此命名空间...

使用自定义命名空间的Fiddler请求

<?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>

我们可以更进一步。如果在模型上将空字符串指定为命名空间,则fiddler请求中不需要命名空间。

具有空字符串命名空间的控制器

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请求

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

其他问题

请注意,DataContractSerializer期望默认情况下按字母顺序排序XML有效内容中的元素。如果XML有效负载乱序,您可能会发现某些元素为空(或者如果数据类型是整数,则它将默认为零,或者如果它是bool,则默认为false)。例如,如果未指定订单并且提交了以下xml ...

元素排序错误的XML正文

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

... Age的值将默认为零。如果发送几乎相同的xml ......

具有正确元素排序的XML正文

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

然后WebAPI控制器将正确序列化并填充Age参数。如果您希望更改默认顺序,以便可以按特定顺序发送XML,则添加“订单”。元素到DataMember属性。

指定属性订单的示例

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; }
    }
}

在此示例中,xml正文必须在Age元素正确填充之前指定Name元素。

<强>结论

我们看到的是,格式错误或不完整的POST请求主体(从DataContractSerializer的角度来看)不会引发错误,而只会导致运行时问题。如果使用DataContractSerializer,我们需要满足序列化程序(特别是在名称空间周围)。我发现使用测试工具是一种很好的方法 - 我将XML字符串传递给使用DataContractSerializer反序列化XML的函数。如果不能进行反序列化,则会抛出错误。下面是使用DataContractSerializer测试XML字符串的代码(再次请记住,如果实现了这一点,则需要添加对System.Runtime.Serialization的引用)。

用于评估DataContractSerializer反序列化的示例测试代码

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;
}

选项

正如其他人所指出的,DataContractSerializer是使用XML的WebAPI项目的默认设置,但还有其他XML序列化程序。您可以删除DataContractSerializer,而是使用XmlSerializer。 XmlSerializer对格式错误的命名空间内容更加宽容。

另一个选择是限制使用JSON而不是XML的请求。我没有进行任何分析来确定在JSON反序列化期间是否使用DataContractSerializer,以及JSON交互是否需要DataContract属性来装饰模型。

答案 2 :(得分:2)

我试图解决这个问题两天。最后我发现外部标签需要是类型名称,而不是变量名称。有效地,使用POST方法

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

我正在提供身体

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

而不是

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

答案 3 :(得分:1)

确保将Content-Type标头设置为application/xml并在config.Formatters.XmlFormatter.UseXmlSerializer = true;的{​​{1}}方法中设置Register后,重要的是您不需要在XML文档的顶部进行任何版本控制或编码。

这最后一块让我陷入困境,希望这可以帮助那里的人,节省你的时间。

答案 4 :(得分:0)

对我来说,它已经在配置中添加了多个xmlFormatter。

在调试时,我发现了格式化程序列表,其中有一个重复的。

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

删除了该行,就可以了。

要检查该行的文件

  • Global.acax.cs
  • WebApiConfig.cs