【问题标题】:Model always null on XML POSTXML POST 上的模型始终为空
【发布时间】:2012-12-13 15:31:17
【问题描述】:

我目前正在研究系统之间的集成,我决定使用 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>

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

【问题讨论】:

  • 如何从客户端调用Post 方法?你确定它是一个 HTTP POST 吗?
  • 提琴手。此外,WebApi 将 POST 调用默认为 POST 方法,GET 到 GET 方法,...
  • 是的,彼得,但您是否在提琴手的下拉菜单中选择了 POST? (默认为 GET)
  • 当然我做到了:) 就像我说的,调用的是 POST 方法。
  • 找出问题所在的提示(因为 Web API 在这方面完全没有帮助)只是通过 Request.Content.ReadAsStringAsync() 获取请求内容并尝试自己反序列化 XML。如果出现问题,XmlSerializer 将抛出一个异常,告诉您为什么它不能反序列化,而不是像 Web API 那样只返回 null。就我而言,这就是我发现我的 XML 声明声明了 UTF-16 编码而请求本身是 UTF-8 编码的方式。如果不自己进行反序列化,这是我永远不会想到的。

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


【解决方案1】:

对我来说,配置中添加了多个 xmlFormatter。

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

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

删除该行并且它起作用了。

要检查该行的文件

  • Global.acax.cs
  • WebApiConfig.cs

【讨论】:

    【解决方案2】:

    一旦您确定将Content-Type 标头设置为application/xml 并在WebApiConfig.csRegister 方法中设置config.Formatters.XmlFormatter.UseXmlSerializer = true;,重要的是您不需要任何版本控制或编码XML 文档的顶部。

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

    【讨论】:

      【解决方案3】:

      我试图解决这个问题两天。最终我发现外部标签需要是类型名称,而不是变量名称。实际上,使用 POST 方法作为

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

      我在提供尸体

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

      而不是

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

      【讨论】:

        【解决方案4】:

        两件事:

        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 配置为在您的 WebApiConfig.Register 中使用 XmlSerializer

          config.Formatters.XmlFormatter.UseXmlSerializer = true;
          

          那么您的 XML 数据中就不需要命名空间了:

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

        【讨论】:

        • 太棒了! UseXmlSerializer 工作!我把它放在Global.asax.cs 文件中,作为全局配置的一部分,但看起来它确实必须在App_Start\WebApiConfig.cs 中。
        • 我不知道 WebAPI 默认使用 DataContractSerializer - 感谢您的提示!!!
        • 谢谢,花了很长时间寻找解决方案。
        • Json 遇到了同样的问题。我有一个装饰有 DataContract/Member 属性的模型。已验证我可以在单元测试中反序列化。能够发布到签名中只有 HttpRequestMessage 的 WebApi 控制器,并在那里读取和反序列化类。能够仅使用 Post([FromBody] MyClass myClass) 的缺失部分是在我的 WebApiConfig.cs 中设置 config.Formatters.JsonFormatter.UseDataContractJsonDeserializer = true 。谢谢!
        • 我已经完成了所有这些,但它对我不起作用......还有什么问题?
        【解决方案5】:

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

        最基本的 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 请求未指定内容类型或字符集
        • 请求正文不包含 XML 声明
        • 请求正文不包含命名空间定义。

        无需对 Web 服务控制器进行任何更改,我就可以修改提琴手请求,使其能够正常工作。密切注意 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 负载乱序,您可能会发现某些元素为空(或者如果数据类型是整数,则默认为零,或者如果是布尔值,则默认为 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,则将“Order”元素添加到 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 属性来装饰模型。

        【讨论】:

        • 杀手级答案。惊讶于发送/接收 XML 是多么困难,而这应该是 REST 服务的重点!!
        • 两点 - 1. 我需要添加对 System.Net.Http.Formatting 的引用才能将 UseXmlSerializer 设置为 true。 2. 我需要在我的请求标头中添加“Accept: application/xml”,以便能够从请求中获取 XML(看起来它默认为 JSON)。
        • 你应该得到这个答案的报酬。将 BTC 捐赠信息放在答案中是否违反 SO 规则?
        • @Setinel - 感谢您的赞美!不知道 SO 规则。我喜欢“向前付款”的想法,而不是收到捐赠的钱。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-06-01
        • 1970-01-01
        • 1970-01-01
        • 2016-12-03
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多