47

我目前正在研究系统之间的集成,我决定使用 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,我不知道为什么。有人有线索吗?

4

5 回答 5

74

两件事情:

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

    User-Agent: Fiddler
    Content-Type: application/xml
    Accept: application/xml
    
  2. Web APIDataContractSerializer默认使用 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>
    
于 2012-12-28T12:48:49.290 回答
61

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

最基本的 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 请求以使用此命名空间...

带有自定义命名空间的提琴手请求

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

没有声明命名空间的提琴手请求

<?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 属性来装饰模型。

于 2013-07-20T23:42:36.123 回答
3

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

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

于 2017-01-03T17:34:05.303 回答
2

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

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

我在提供身体

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

代替

<TestModel><Output>Sito</Output></TestModel>
于 2017-12-01T07:28:33.443 回答
0

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

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

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

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

检查该行的文件

  • 全球.acax.cs
  • WebApiConfig.cs
于 2020-11-10T11:30:12.313 回答