【问题标题】:How to pass XML as POST to an ActionResult in ASP MVC .NET如何将 XML 作为 POST 传递给 ASP MVC .NET 中的 ActionResult
【发布时间】:2009-07-10 20:41:09
【问题描述】:

我正在尝试为我的 ASP MVC 项目提供一个简单的 RESTful API。我将无法控制此 API 的客户端,它们将通过 POST 方法传递 XML,该方法将包含在服务器端执行某些操作所需的信息,并返回带有操作结果的 XML。我发回 XML 没有问题,问题是通过 POST 接收 XML。我看过一些 JSON 示例,但由于我不会控制我的客户端(从我的角度来看,它甚至可能是一个 telnet)我认为 JSON 不会起作用。我对么?

我见过一些示例,其中客户端只需将正确的表单格式构建为请求正文的一部分,然后 ASP 解析消息,并且数据可作为 FormCollection (?param1=value1&param2=value2& 等)。但是,我想将纯 XML 作为消息正文的一部分传递。

感谢您的帮助,

【问题讨论】:

    标签: asp.net-mvc xml json json.net actionresult


    【解决方案1】:

    @Freddy - 喜欢您的方法并使用以下代码对其进行了改进以简化流读取:

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            HttpContextBase httpContext = filterContext.HttpContext;
            if (!httpContext.IsPostNotification)
            {
                throw new InvalidOperationException("Only POST messages allowed on this resource");
            }
    
            Stream httpBodyStream = httpContext.Request.InputStream;
            if (httpBodyStream.Length > int.MaxValue)
            {
                throw new ArgumentException("HTTP InputStream too large.");
            }
    
            StreamReader reader = new StreamReader(httpBodyStream, Encoding.UTF8);
            string xmlBody = reader.ReadToEnd();
            reader.Close();
    
            filterContext.ActionParameters["message"] = xmlBody;
    
            // Sends XML Data To Model so it could be available on the ActionResult
            base.OnActionExecuting(filterContext);
        }
    

    然后在Controller中可以将xml作为字符串访问:

    [RestAPIAttribute]    
    public ActionResult MyActionResult(string message)    
    {         
    
    }
    

    【讨论】:

    • 就是上面显示的action的message参数。
    【解决方案2】:

    这可以通过使用 ActionFilterAttribute 来完成。 Action Filters 基本上与 Action Result 之前或之后的请求相交。所以我刚刚为 POST Action Result 构建了一个自定义操作过滤器属性。这是我所做的:

    public class RestAPIAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            HttpContextBase httpContext = filterContext.HttpContext;
            if (!httpContext.IsPostNotification)
            {
                throw new InvalidOperationException("Only POST messages allowed on this resource");
            }
            Stream httpBodyStream = httpContext.Request.InputStream;
    
            if (httpBodyStream.Length > int.MaxValue)
            {
                throw new ArgumentException("HTTP InputStream too large.");
            }
    
            int streamLength = Convert.ToInt32(httpBodyStream.Length);
            byte[] byteArray = new byte[streamLength];
            const int startAt = 0;
    
            /*
             * Copies the stream into a byte array
             */
            httpBodyStream.Read(byteArray, startAt, streamLength);
    
            /*
             * Convert the byte array into a string
             */
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < streamLength; i++)
            {
                sb.Append(Convert.ToChar(byteArray[i]));
            }
    
            string xmlBody = sb.ToString();
    
            //Sends XML Data To Model so it could be available on the ActionResult
    
            base.OnActionExecuting(filterContext);
        }
    }
    

    然后在控制器上的 action result 方法上,您应该执行以下操作:

        [RestAPIAttribute]
        public ActionResult MyActionResult()
        {
            //Gets XML Data From Model and do whatever you want to do with it
        }
    

    希望这对其他人有所帮助,如果您认为有更优雅的方法可以做到这一点,请告诉我。

    【讨论】:

    • 我不明白这个例子中模型是在哪里定义的。谁能解释一下?
    • 字节数组到字符串的转换确实效率低下...System.Text.Encoding.UTF8.GetString(byteArray) 可能会更好。
    • @Freddy 如何从请求中访问 XML?
    • @Demodave,bowerm 的回答更好地说明了如何使用 ActionParameters["message"] = xmlBody 获取 XML,这似乎从 Freddy 的回答中丢失了
    【解决方案3】:

    为什么他们不能在表单 post 中将 xml 作为字符串传递?

    例子:

    public ActionResult SendMeXml(string xml)
    {
      //Parse into a XDocument or something else if you want, and return whatever you want.
      XDocument xmlDocument = XDocument.Parse(xml);
    
      return View();
    }
    

    您可以创建表单帖子并将其发送到单个表单字段中。

    【讨论】:

    • 在这种情况下,客户端应该知道服务器将如何处理请求。它不会是 XML,而是带有输入文本的表单,而输入文本恰好包含 XML。这不是一个 RESTful 解决方案。
    • 好吧,您的客户需要知道他们要将请求发送到哪个 url,所以告诉他们使用哪个表单字段并不是一个飞跃。
    • 应该没有表单,只有普通的 XML。不是关于“使用哪个表单域”。
    • 那么可能是这样的:aleembawany.com/2009/03/27/… 虽然看起来你可能已经有类似的东西了。 :)
    • "but it's not RESTful" 是一种对教条的痴迷,损害了完成即使是最简单的事情的能力。客户端不“知道”任何事情,您对客户端进行编程,无论您使用什么协议。 “在这种情况下,客户端应该知道服务器将如何处理请求。” - 好像有一些神奇的规范可以导致这些组件变得有感知。在所有情况下,客户端都需要被编程为使用它需要使用的东西......
    【解决方案4】:

    我知道您可以创建自定义值提供者工厂。这将允许您在尝试保存模型之前在发布模型时对其进行验证。 Phil Haack 有一篇关于同一概念的 JSON 版本的博客文章。唯一的问题是我不知道如何为 XML 实现同样的事情。

    【讨论】:

      【解决方案5】:

      IMO 最好的方法是编写一个自定义值提供程序,这是一个处理请求到表单字典的映射的工厂。如果请求是“text/xml”或“application/xml”类型,您只需从 ValueProviderFactory 继承并处理请求。

      更多信息:

      Phil Haack

      My blog

      MSDN

      protected override void OnApplicationStarted()
      {
          AreaRegistration.RegisterAllAreas();
      
          RegisterRoutes(RouteTable.Routes);
      
          ValueProviderFactories.Factories.Add(new JsonValueProviderFactory());
          ValueProviderFactories.Factories.Add(new XmlValueProviderFactory());
      }
      

      XmlValueProviderFactory

      using System;
      using System.Collections.Generic;
      using System.Globalization;
      using System.Web.Mvc;
      using System.Xml;
      using System.Xml.Linq;
      
      public class XmlValueProviderFactory : ValueProviderFactory
      {
      
          public override IValueProvider GetValueProvider(ControllerContext controllerContext)
          {
              var deserializedXml = GetDeserializedXml(controllerContext);
      
              if (deserializedXml == null) return null;
      
              var backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
      
              AddToBackingStore(backingStore, string.Empty, deserializedXml.Root);
      
              return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
      
          }
      
          private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, XElement xmlDoc)
          {
              // Check the keys to see if this is an array or an object
              var uniqueElements = new List<String>();
              var totalElments = 0;
              foreach (XElement element in xmlDoc.Elements())
              {
                  if (!uniqueElements.Contains(element.Name.LocalName))
                      uniqueElements.Add(element.Name.LocalName);
                  totalElments++;
              }
      
              var isArray = (uniqueElements.Count == 1 && totalElments > 1);
      
      
              // Add the elements to the backing store
              var elementCount = 0;
              foreach (XElement element in xmlDoc.Elements())
              {
                  if (element.HasElements)
                  {
                      if (isArray)
                          AddToBackingStore(backingStore, MakeArrayKey(prefix, elementCount), element);
                      else
                          AddToBackingStore(backingStore, MakePropertyKey(prefix, element.Name.LocalName), element);
                  }
                  else
                  {
                      backingStore.Add(MakePropertyKey(prefix, element.Name.LocalName), element.Value);
                  }
                  elementCount++;
              }
          }
      
      
          private static string MakeArrayKey(string prefix, int index)
          {
              return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
          }
      
          private static string MakePropertyKey(string prefix, string propertyName)
          {
              if (!string.IsNullOrEmpty(prefix))
                  return prefix + "." + propertyName;
              return propertyName;
          }
      
          private XDocument GetDeserializedXml(ControllerContext controllerContext)
          {
              var contentType = controllerContext.HttpContext.Request.ContentType;
              if (!contentType.StartsWith("text/xml", StringComparison.OrdinalIgnoreCase) &&
                  !contentType.StartsWith("application/xml", StringComparison.OrdinalIgnoreCase))
                  return null;
      
              XDocument xml;
              try
              {
                  var xmlReader = new XmlTextReader(controllerContext.HttpContext.Request.InputStream);
                  xml = XDocument.Load(xmlReader);
              }
              catch (Exception)
              {
                  return null;
              }
      
              if (xml.FirstNode == null)//no xml.
                  return null;
      
              return xml;
          }
      }
      

      【讨论】:

        【解决方案6】:

        我喜欢@Freddy 的回答和@Bowerm 的改进。它简洁并保留了基于表单的操作的格式。

        但 IsPostNotification 检查不适用于生产代码。它不检查 HTTP 动词,因为错误消息似乎暗示,并且当编译调试标志设置为 false 时,它​​会从 HTTP 上下文中剥离。这在这里解释: HttpContext.IsPostNotification is false when Compilation debug is false

        由于这个问题,我希望这可以为某人节省 1/2 天的路由调试时间。这是没有该检查的解决方案:

        public class XmlApiAttribute : ActionFilterAttribute
        {
            public override void OnActionExecuting(ActionExecutingContext filterContext)
            {
                HttpContextBase httpContext = filterContext.HttpContext;
                // Note: for release code IsPostNotification stripped away, so don't check it!
                // https://stackoverflow.com/questions/28877619/httpcontext-ispostnotification-is-false-when-compilation-debug-is-false            
        
                Stream httpBodyStream = httpContext.Request.InputStream;
                if (httpBodyStream.Length > int.MaxValue)
                {
                    throw new ArgumentException("HTTP InputStream too large.");
                }
        
                StreamReader reader = new StreamReader(httpBodyStream, Encoding.UTF8);
                string xmlBody = reader.ReadToEnd();
                reader.Close();
        
                filterContext.ActionParameters["xmlDoc"] = xmlBody;
        
                // Sends XML Data To Model so it could be available on the ActionResult
                base.OnActionExecuting(filterContext);
            }
        }
        ...
        public class MyXmlController 
        { ...
            [XmlApiAttribute]
            public JsonResult PostXml(string xmlDoc)
            {
        ...
        

        【讨论】:

          【解决方案7】:

          不错!,

          我在控制器方法中获得了什么对象来操作 Xml?

          我是这样用的:

          在 actionFilter 上,我使用以下内容填充模型:

                  .
                  .
          
                  string xmlBody = sb.ToString();
          
                  filterContext.Controller.ViewData.Model = xmlBody;
          

          在我的控制器方法上,我得到的模型是:

                  string xmlUserResult = ViewData.Model as string;
          
                  XmlSerializer ser = new XmlSerializer(typeof(UserDTO));
                  StringReader stringReader = new StringReader(xmlUserResult);
                  XmlTextReader xmlReader = new XmlTextReader(stringReader);
                  UserDTO userToUpdate = ser.Deserialize(xmlReader) as UserDTO;
                  xmlReader.Close();
                  stringReader.Close();
          

          这是一个正确的实现吗?

          谢谢。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-10-05
            • 1970-01-01
            • 2020-05-31
            • 1970-01-01
            • 2014-03-01
            相关资源
            最近更新 更多