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,

Freddy
  • 3,064
  • 3
  • 26
  • 27

7 Answers7

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

}
bowerm
  • 151
  • 1
  • 9
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.

Freddy
  • 3,064
  • 3
  • 26
  • 27
  • 1
    I don't understand where the model is defined in this example. Can someone explain? – Matt Dell Feb 20 '13 at 09:50
  • The conversion of a byte array to a string really sticks out as being inefficient... [System.Text.Encoding.UTF8.GetString(byteArray)](http://stackoverflow.com/questions/1003275/converting-byte-to-string-in-c-sharp) could be better. – BJury Jan 07 '14 at 12:24
  • 1
    @Freddy How do you access the XML from the request? – Demodave Apr 23 '15 at 15:55
  • @Demodave, bowerm's answer better illustrates how to get the XML using `ActionParameters["message"] = xmlBody`, which seems to be missing from Freddy's answer – OutstandingBill Apr 20 '17 at 04:52
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.

Dan Atkinson
  • 11,391
  • 14
  • 81
  • 114
  • 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. – Freddy Jul 12 '09 at 23:50
  • 1
    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. – Dan Atkinson Jul 16 '09 at 13:09
  • There should be no forms, just plain XML. Is not about "which form field to use". – Freddy Jul 16 '09 at 13:18
  • Then maybe something like this: http://aleembawany.com/2009/03/27/aspnet-mvc-create-easy-rest-api-with-json-and-xml/ Although it looks like you may have something similar already. :) – Dan Atkinson Jul 16 '09 at 13:38
  • "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... – fartwhif Dec 14 '21 at 15:27
  • 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! – fartwhif Dec 14 '21 at 15:51
  • @fartwhif 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. – Dan Atkinson Dec 15 '21 at 20:33
  • @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. – fartwhif Dec 15 '21 at 21:28
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.

Justin
  • 1,428
  • 1
  • 13
  • 29
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;
    }
}
Aaron
  • 1,031
  • 10
  • 23
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://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)
    {
...
Community
  • 1
  • 1
Aaron Newman
  • 549
  • 1
  • 5
  • 27
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.

Custodio
  • 8,594
  • 15
  • 80
  • 115