在 NET Core Web API 中使用 application/x-www-form-urlencoded content-type 但 XML body 处理请求

Handle Request with application/x-www-form-urlencoded content-type but XML body in NET Core Web API

我有一个 .NET Core 2.1 Web API 控制器和方法应该使用 POST XML 请求 通过 HTTP 从外部服务。 下面是控制器动作的方法header。

    [HttpPost]
    [Produces("application/xml")]
    public async Task<IActionResult> PostReceivedMessage([FromBody] ReceivedMessage receivedMessage)

我编写了一个自定义 XML 输入格式化程序来处理 XML 请求,它在以下情况下工作正常 我 post 从 Postman 到应用程序控制器操作的示例 XML 请求。 但是当服务发送类似的请求时,应用程序的响应状态为 400,错误请求。

经过一些调试,我发现请求进来的是

Content-Type: application/x-www-form-urlencoded

而不是人们可能期望的 application/xml 或 text/xml。 如果我更改 header 以匹配 外部服务发送的请求。

我假设 x-www-form-urlencoded 用于表单数据,因为模型绑定不起作用 当我将操作 header 更改为:

public async Task<IActionResult> PostReceivedMessage([FromForm] ReceivedMessage receivedMessage)

由于我无法控制外部服务,我应该如何使控制器操作能够处理 x-www-form-urlencoded 作为 content-type 的 XML 请求?

更新: 以下是一个示例请求:

POST /check/api/receivedmessages HTTP/1.1
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.7.0_45
Host: xxx.xxx.xxx.xxx
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-type: application/x-www-form-urlencoded
Content-Length: 270

<Request><requestId>95715274355861000</requestId><msisdn>345678955041</msisdn><timeStamp>2019/10/20 02:23:55</timeStamp><keyword>MO</keyword><dataSet><param><id>UserData</id><value>VHVqrA==</value></param><param><id>DA</id><value>555</value></param></dataSet></Request>

获取字符串并将xml字符串转换为C#对象很简单:

[HttpPost]
[Produces("application/xml")]      
public async Task<IActionResult> PostReceivedMessage([FromForm]string receivedMessage)
    {

        XmlSerializer serializer = new XmlSerializer(typeof(ReceivedMessage));
        ReceivedMessage data;
        using (TextReader reader = new StringReader(receivedMessage))
        {
             data = (ReceivedMessage)serializer.Deserialize(reader);
        }


        return Ok(data);
    }

我最终采纳了评论中提供的 @Xing Zou 建议并实施了 自定义模型活页夹。 我不确定这是否矫枉过正,但至少它保持了控制器的动作 "lean".

自定义模型活页夹如下所示:

public class ReceivedMessageEntityBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var request = bindingContext.HttpContext.Request;

            var firstKey = request.Form.Keys.First();
            StringValues formValue = "";

            request.Form.TryGetValue(firstKey, out formValue);

            var requestBody = firstKey + "=" + formValue;

            bindingContext.Result = ModelBindingResult.Success(FromXmlString(requestBody));

            return Task.CompletedTask;  
        }

        private ReceivedMessage FromXmlString(string requestBody)
        {
            XElement request = XElement.Parse(requestBody);

            var receivedMessage = new ReceivedMessage();

            receivedMessage.RequestId = (string)
                                        (from el in request.Descendants("requestId")
                                         select el).First();

            receivedMessage.Msisdn = (string)
                                        (from el in request.Descendants("msisdn")
                                         select el).First();


            receivedMessage.Timestamp = DateTime.Parse(
                                        (string)
                                        (from el in request.Descendants("timeStamp")
                                         select el).First());


            receivedMessage.Keyword = (string)
                                        (from el in request.Descendants("keyword")
                                         select el).First();


            IEnumerable<XElement> dataSet = from el in request.Descendants("param")
                                            select el;

            foreach (var param in dataSet)
            {
                var firstNode = param.Descendants().First();

                switch (firstNode.Value)
                {
                    case "UserData":
                        receivedMessage.UserData = (firstNode.NextNode as XElement).Value;
                        break;

                    case "DA":
                        receivedMessage.Da = (firstNode.NextNode as XElement).Value;
                        break;
                }
            }

            return receivedMessage;
        }
    }

并且模型现在已装饰,以便可以使用自定义模型活页夹绑定到它。

[ModelBinder(BinderType = typeof(ReceivedMessageEntityBinder))]
    public class ReceivedMessage
    {
        public long Id { get; set; }

        [StringLength(12)]
        public string Msisdn { get; set; }
        public string RequestId { get; set; }
        public DateTime Timestamp { get; set; }
        public string Keyword { get; set; }
        public string UserData { get; set; }
        public string Da { get; set; }
    }

有一点要注意。外部服务发送的 XML 包含一个 base64 编码的值。这意味着有两个“=”符号,我的猜测是这导致正文被解释为具有 1 个键和 1 个值的表单。例如:

[key]<Request><requestId>95715274355861000</requestId><msisdn>345678955041</msisdn><timeStamp>2019/10/20 02:23:55</timeStamp><keyword>MO</keyword><dataSet><param><id>UserData</id><value>VHVqrA
[value]=</value></param><param><id>DA</id><value>555</value></param></dataSet></Request>

因此,我的模型绑定器笨拙地提取请求正文的方式的原因 成一个字符串。

我想如果不是这种情况,只需阅读表格中的第一个(也是唯一一个键)就可以获得正文。