How to pass XML as POST to an ActionResult in ASP MVC .NET

纵饮孤独 提交于 2019-11-30 07:03:22

@Freddy - liked your approach and improved on it with the following code to simplify stream reading:

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

Then in the Controller you can access the xml as a string:

[RestAPIAttribute]    
public ActionResult MyActionResult(string message)    
{         

}

This could be accomplished by using the ActionFilterAttribute. Action Filters basically intersects the request before or after the Action Result. So I just built a custom action filter attribute for POST Action Result. Here is what I did:

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

Then on the action result method on your controller you should do something like this:

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

Hope this helps somebody else, if you think there are more elegant ways to do it, let me know.

I know you can create a custom value provider factory. This will let you also validate your models when they are posted before attempting to save them. Phil Haack has a blog post about a JSON version of this same concept. The only problem is that I don't know how to implement one this same sort of thing for XML.

IMO the best way to accomplish this is to write a custom value provider, this is a factory that handles the mapping of the request to the forms dictionary. You just inherit from ValueProviderFactory and handle the request if it is of type “text/xml” or “application/xml.”

More Info:

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

Why can they not pass the xml as a string in the form post?

Example:

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

You could create a form post and send it in a single form field.

Aaron Newman

I like the answer from @Freddy and improvement from @Bowerm. It is concise and preserves the format of form-based actions.

But the IsPostNotification check will not work in production code. It does not check the HTTP verb as the error message seems to imply, and it is stripped out of HTTP context when compilation debug flag is set to false. This is explained here: HttpContext.IsPostNotification is false when Compilation debug is false

I hope this saves someone a 1/2 day of debugging routes due to this problem. Here is the solution without that check:

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)
    {
...
Custódio

Nice!,

What object I got in my controller method to manipulate the Xml?

I'm using this way:

On actionFilter, I populate the model with:

        .
        .

        string xmlBody = sb.ToString();

        filterContext.Controller.ViewData.Model = xmlBody;

And on my controller method, I get the Model as:

        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();

Is this a correct implementation?

Thanks.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!