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

7

11

I am trying to provide a simple RESTful API to my ASP MVC project. I will not have control of the clients of this API, they will be passing an XML via a POST method that will contain the information needed to perform some actions on the server side and provide back an XML with the result of the action. I don't have problems sending back XMLs, the problem is receiving XML via a POST. I have seen some JSON examples, but since I will not control my clients (it could be even a telnet from my point of view) I don't think JSON will work. Am I correct?

I have seen examples where clients simply construct the correct form format as part of the body of the request and then the ASP parse the message, and data is available as FormCollection (?param1=value1&param2=value2&,etc). However, I want to pass pure XML as part of the message body.

thanks for your help,

Keratose answered 10/7, 2009 at 20:41 Comment(0)
K
8

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.

Keratose answered 13/7, 2009 at 20:38 Comment(4)
I don't understand where the model is defined in this example. Can someone explain?Tumultuous
The conversion of a byte array to a string really sticks out as being inefficient... System.Text.Encoding.UTF8.GetString(byteArray) could be better.Bilek
@Keratose How do you access the XML from the request?Sexless
@Demodave, bowerm's answer better illustrates how to get the XML using ActionParameters["message"] = xmlBody, which seems to be missing from Freddy's answerPreparator
S
10

@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)    
{         

}
Steelman answered 5/7, 2011 at 9:7 Comment(1)
It's the message parameter of the action shown above.Steelman
K
8

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.

Keratose answered 13/7, 2009 at 20:38 Comment(4)
I don't understand where the model is defined in this example. Can someone explain?Tumultuous
The conversion of a byte array to a string really sticks out as being inefficient... System.Text.Encoding.UTF8.GetString(byteArray) could be better.Bilek
@Keratose How do you access the XML from the request?Sexless
@Demodave, bowerm's answer better illustrates how to get the XML using ActionParameters["message"] = xmlBody, which seems to be missing from Freddy's answerPreparator
N
4

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.

Nancee answered 11/7, 2009 at 15:12 Comment(8)
In that case the client(s) should be aware on how the server is going to process the request. It will not be XML, it will be a Form with an input text that happens to contain XML. That will not be a RESTful solution.Keratose
Well, your client needs to be aware of which url they are going to send the request to, so it's not that much of a leap to tell them which form field to use.Nancee
There should be no forms, just plain XML. Is not about "which form field to use".Keratose
Then maybe something like this: aleembawany.com/2009/03/27/… Although it looks like you may have something similar already. :)Nancee
"but it's not RESTful" is a kind of obsession with dogma, to the detriment of the ability to accomplish even the simplest things. The client isn't "aware" of anything, you program the client, regardless of whatever protocols you're using. "In that case the client(s) should be aware on how the server is going to process the request. " - as if, there's otherwise some magical specification, that can cause these components to become sentient. In ALL cases the client needs to be programmed to work with what it needs to work with...Lemming
with REST, the fact that you're adding another context (POST/GET/PUT/DELETE/etc.) with which to identify and specify the action, the endpoint, doesn't actually add any value, as you can do everything using just one context with an additional segment in your endpoint name, and/or with an additional variable for "operation". it's incredibly ignorant to latch onto REST and pretend it's the end-all be-all as if it somehow solves this problem and doesn't just give it a name and shuffle a few things around while enforcing a CRUD-like mentality, and then double down when it gets in the way!Lemming
@Lemming My answer was written 12 years ago. Regarding your silly magical sentience comment, you're aware this question is regarding an API, yes? It's not only reasonable but expected to enforce such requirements when designing APIs as you are in control of how consuming clients will communicate with you. That's all I'll say on the subject.Nancee
@DanAtkinson Didn't know it was that old, neither do I think the timeline is important. Regarding the "silly magical sentience", wasn't it you who started it 12 years ago with "be aware" Also, I fail to see the epistemological basis or any self-evident reason for the behavior of "consuming clients and you". Logically it's not some kind of duality, but homogenous nodes communicating. "reasonable and expected" I think that perhaps things turned out this way only because some people felt emotional about it, and your "expected" is an appeal to authority in a philosophical debate.Lemming
P
3

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.

Proser answered 29/7, 2010 at 20:29 Comment(0)
P
3

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;
    }
}
Pollypollyanna answered 23/1, 2012 at 19:9 Comment(0)
A
2

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://mcmap.net/q/671977/-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)
    {
...
Appendant answered 19/1, 2016 at 16:47 Comment(0)
B
1

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.

Backache answered 10/7, 2009 at 20:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.