Implementing XML-RPC services with ASP.NET MVC

I recently needed to expose some functionality using XML-RPC services and looking around the web, the most popular .Net library is www.xml-rpc.net which looks pretty good. However they implement their services using a custom HTTP Handler, and I had really hoped to find one based on ASP.NET MVC.  Why would I would to do this? I’d like to be able to use some the infrastructure I already have in place with MVC, e.g. caching, action filters, dependency injection via the controller factory etc. Well, I couldn’t find anyone doing anything like this, so I hacked together something of my own.

So just a quick recap, an example of an XML-RPC request looks like this:

<methodCall>
    <methodName>test.foo</methodName>
    <params>
        <param>
            <value>
                <string>Parameter 1</string>
            </value>
        </param>
        <param>
            <value>
                <int>Parameter 2</int>
            </value>
        </param> 
    </params>
</methodCall>

My aim was to get such a request to be mapped to a controller and action pair like so:

public class TestController : Controller
{
    public ActionResult Foo(string someParam1, int someParam2)
    {
    }
}

First up is routing, normally the URL is where the controller/action values are extracted from, but we want to extract them from the XML in the POST body, specifically from the /methodCall/methodName node, and use the convention that the name before the period is the controller, and the name after is the action. We can do this with a custom route by inheriting from System.Web.Routing.Route and overriding the default GetRouteData method to set the RouteData for the target controller and action:

public class XmlRpcRoute : Route
{
    public XmlRpcRoute(string url, IRouteHandler routeHandler) : base(url, routeHandler) { }
    public XmlRpcRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler) : base(url, defaults, routeHandler) { }
    public XmlRpcRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler) : base(url, defaults, constraints, routeHandler) { }
    public XmlRpcRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler) : base(url, defaults, constraints, dataTokens, routeHandler) { }

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        RouteData routeData = base.GetRouteData(httpContext);

        if (routeData == null) return null; //If route data is null, the current URL was not matched against that configured for this route

        if (httpContext.Request.InputStream != null && httpContext.Request.InputStream.Length > 0)
        {
            XDocument xml = XDocument.Load(httpContext.Request.InputStream);
            string methodName = xml.Document.Element("methodCall").Element("methodName").Value;                               
            var methodNameParts = methodName.Split('.');
            routeData.Values["controller"] = methodNameParts[0];
            routeData.Values["action"] = methodNameParts[1];
        }
            
        return routeData;
    }
}

To be able to use this route, we need to bind it to an end-point URL. Routes are normally added via an extension method, so to be able to do this:

routes.MapXmlRpcRoute(		// XmlRpc specific route
    "XmlRpcEndPoint",  		// Route name
    "api/xml-rpc"     		// XML-RPC end-point URL              
);

I added this extension method:

public static class XmlRpcRouteCollectionExtensions
{
    public static XmlRpcRoute MapXmlRpcRoute(this RouteCollection routes, string name, string url)
    {
        if (routes == null) throw new ArgumentNullException("routes");            
        if (url == null) throw new ArgumentNullException("url");
            
        XmlRpcRoute route = new XmlRpcRoute(url, new MvcRouteHandler())
        {
            Defaults = new RouteValueDictionary(),
            DataTokens = new RouteValueDictionary()                
        };

        routes.Add(name, route);

        return route;
    }
}

At this point, XML-RPC requests will be routed to the correct controller and action, but the parameters now need to be bound to the target method. I was originally planning to use a custom ModelBinder, but I didn’t want to add a global model binder, nor did I want to override the default model binder for each parameter in the target method. So a simpler approach was just to use a custom ActionFilterAttribute. This attribute inherits from System.Web.Mvc.ActionFilterAttribute, which means its OnActionExecuting method is called before the action is executed. This gives us a chance to set the parameters of the method. To do this, we need to deserialise the XML-RPC parameters into their appropriate CLR types. All we need to do is apply this attribute to our controller classes. Here’s the code:

public class XmlRpcServiceAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        //Set the parameter values from the XML-RPC request XML
        if (filterContext.HttpContext.Request.InputStream != null)
        {
            filterContext.HttpContext.Request.InputStream.Seek(0, System.IO.SeekOrigin.Begin); //seek to the begining of the stream
            XDocument xml = XDocument.Load(filterContext.HttpContext.Request.InputStream);
            var xmlParams = xml.Document.Element("methodCall").Element("params").Elements("param").ToArray();
            int index = 0;
            foreach (ParameterDescriptor paramDescriptor in filterContext.ActionDescriptor.GetParameters())
            {
                var node = xmlParams[index].Element("value").Elements().First();
                filterContext.ActionParameters[paramDescriptor.ParameterName] = DeserialiseXmlRpcData(node, paramDescriptor.ParameterType);
                index++;
            }
        }

        base.OnActionExecuting(filterContext);
    }

    private object DeserialiseXmlRpcData(XElement data, Type targetType)
    {
        string dataType = data.Name.LocalName;            
        if (dataType == "string")
        {                
            return data.Value;
        }
        else if (dataType == "int")
        {
            return int.Parse(data.Value);
        }
        else if (dataType == "double")
        {
            return double.Parse(data.Value);
        }
        else if (dataType == "boolean")
        {
            return data.Value == "1";
        }
        else if (dataType == "dateTime.iso8601")
        {
            return DateTime.Parse(data.Value);
        }
        else if (dataType == "array") {
            var values = data.Element("data").Elements("value");
            Array targetArray = Array.CreateInstance(targetType.GetElementType(), values.Count());
            int index = 0;
            foreach (var value in values)
            {
                var propertyValue = DeserialiseXmlRpcData(value.Elements().First(), targetType.GetElementType());
                targetArray.SetValue(propertyValue, index);
                index++;
            }
            return targetArray;
        }
        else if (dataType == "struct")
        {
            var members = data.Elements("member");
            object targetObject = Activator.CreateInstance(targetType);
            var propertyInfos = targetType.GetProperties();
            foreach (var member in members)
            {
                var propertyInfo = propertyInfos.SingleOrDefault(x => x.Name == member.Element("name").Value);
                if (propertyInfo != null) //target type may not contain the property name from the xml-rpc data, simply ignore rather than throw exception
                {
                    var propertyValue = DeserialiseXmlRpcData(member.Element("value").Elements().First(), propertyInfo.PropertyType);
                    propertyInfo.SetValue(targetObject, propertyValue, null);
                }                    
            }
            return targetObject;
        }
        else
        {
            throw new ArgumentException("Could not deserialise xml-rpc data");
        }          
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        //If there are unhandled exception, convert to the response to an xml-rpc fault
        if (filterContext.Exception != null && !filterContext.ExceptionHandled)
        {
            filterContext.Result = new XmlRpcResponseResult(filterContext.Exception);
            filterContext.ExceptionHandled = true;
        }
        else
        {
            base.OnActionExecuted(filterContext);
        }
    }
}

We’re making progress now, our action method is now being invoked with it’s parameters correctly initialised to those specified in the XML-RPC request. We can now implement our action method normally. However we still need to deal with serialising any return value back into the XML-RPC response format. Ideally I would have liked to simply return an object from the action and have that serialised, however by default ASP.MVC uses a ContentResult for action methods that return values that are not ActionResult instances, meaning it just calls the ToString() method. My solution was to create an XmlRpcResponseResult type that inherits from ContentResult to serialise the return value into an XML string:

public class XmlRpcResponseResult :  ContentResult
{
    public XmlRpcResponseResult(object data) 
    {
        //Set content type to xml
        ContentType = "text/xml";

        //Serialise data into base.Content
        Content = SerialiseXmlRpcResponse(data);
    }

    private string SerialiseXmlRpcResponse(object data)
    {
        if (data is Exception)
        {
            Exception ex = data as Exception;
            return new XDocument(
                    new XElement("methodResponse",
                        new XElement("fault",
                            new XElement("value",
                                new XElement("struct",
                                    new XElement("member",
                                        new XElement("name", "faultCode"),
                                        new XElement("value", SerialiseXmlRpcData(0))),
                                    new XElement("member",
                                        new XElement("name", "faultString"),
                                        new XElement("value", SerialiseXmlRpcData(ex.Message)))))))).ToString();
        }
        else
        {
            return new XDocument(
                    new XElement("methodResponse",
                        new XElement("params",
                            new XElement("param",
                                new XElement("value",
                                    SerialiseXmlRpcData(data)))))).ToString();
        }
    }

    private XElement SerialiseXmlRpcData(object data)
    {
        if (data is IEnumerable && !IsPrimitiveXmlRpcType(data))
        {
            XElement arrayData = new XElement("data");                                    
            foreach (var dataItem in (data as IEnumerable))
            {
                arrayData.Add(new XElement("value",
                                SerialiseXmlRpcData(dataItem)));
            }
            return new XElement("array", arrayData);
        }
        else if (data.GetType().IsClass && !IsPrimitiveXmlRpcType(data))
        {
            return SerialiseXmlRpcStruct(data);
        }
        else if (IsPrimitiveXmlRpcType(data))
        {
            return SerialiseXmlRpcPrimitive(data);
        }

        throw new ArgumentException("Could not serialise xml-rpc data");
    }

    private bool IsPrimitiveXmlRpcType(object data)
    {
        return data.GetType().IsPrimitive || data is string || data is DateTime;
    }

    private XElement SerialiseXmlRpcStruct(object data)
    {
        XElement structElement = new XElement("struct");

        //Serialise all properties as a name-value pair. Value can be any supported type
        PropertyInfo[] propInfos = data.GetType().GetProperties();
        foreach (PropertyInfo propInfo in propInfos)
        {
            XElement member = new XElement("member");
            string name = propInfo.Name;
            object value = propInfo.GetValue(data, null);
            member.Add(new XElement("name", name), new XElement("value", SerialiseXmlRpcData(value)));
            structElement.Add(member);
        }

        return structElement;
    }

    private XElement SerialiseXmlRpcPrimitive(object data)
    {
        if (data is string)
        {
            return new XElement("string", data);
        }
        else if (data is int)
        {
            return new XElement("int", data.ToString());
        }
        else if (data is double)
        {
            return new XElement("double", data.ToString());
        }
        else if (data is bool)
        {
            return new XElement("boolean", (bool)data? "1": "0");
        }
        else if (data is DateTime)
        {
            return new XElement("dateTime.iso8601", ((DateTime)data).ToString("s"));
        }           

        throw new ArgumentException("Serialised for this object type is not handled");
    }
}

Before I go on, you may notice that this class knows how to serialise exceptions – it serialises them into XML-RPC faults. You may have noticed that the XmlRpcServiceAttribute filter also has an OnActionExecuted method, which executes after the action has run. It checks if an unhandled exception was thrown and changes the result so that the fault response is returned.

So with all this plumbing in place, what does a real example look like? Well all of this was to implement the MetaWeblog API for my blog, so here’s what the method looks like to get recent posts:

[XmlRpcService]
public class MetaWeblogController : Controller
{
    public ActionResult GetRecentPosts(string blogid, string username, string password, int numberOfPosts)
    {            
        var posts = /*some logic to get a list of post objects*/
        return new XmlRpcResponseResult(posts);
    }
}

I think this is quite clean, and it looks pretty much like any other controller.

I hope this has been useful to some, do bear in mind that the code above is far from being complete. There are some XML-RPC types I didn’t get around to dealing with (e.g <base64> – essentially a byte array) and I’m sure the error handling could be better. If you have any suggestions, do drop me a comment below. Thanks for reading.

Comments (7)

George
Jono, I stumbled upon this while researching how to call XML-RPC services from .NET. I saw your feedback comment on Stackoverflow. Although now I understand that your code here is for handling and responding to external XML-RPC client requests INTO your MVC app, I was initially looking for how to make XML-RPC requests from inside my MVC app to an external XML-RPC server.

For that, I would assume that you would still use the XML-RPC.net library that you referenced at the top of your post? However, I find your solution here for servicing incoming XML-RPC requests to your MVC app to be a much better and elegant fit for an MVC style app than using the XML-RPC.net approach.

I plan on trying your approach when it comes time for my app to handle and respond to XML-RPC requests during a later phase of the project.

I think you should consider making a NuGet package for this. It would help a lot of folks out in adding the handling of XML-RPC requests to their MVC apps. Great work!
Friday, 24 February 2012 21:19
Andy
@jono - thanks for posting this tutorial. I attempted this in MVC using a ValueProvider https://github.com/andybooth/instatus/blob/master/Instatus/Web/XmlRpcValueProvider.cs. I like you code sample better. I believe I based my code on http://vasters.com/clemensv/PermaLink,guid,679ca50b-c907-4831-81c4-369ef7b85839.aspx.

I'm keen to get Xml Rpc to work simply with ASP.NET MVC Web API, now that there is a Go-live license for it. This is to allow Wordpress iOS and Android mobile clients to edit and moderate a custom site.
Thursday, 01 March 2012 16:36
Edgar Allan wg
|
Wednesday, 10 April 2013 18:45
James
Great article. Thanks.

Can you please share the source code (the whole project files) so I could see where all the codes are located? Please I'm working on an asp.net MVC project with deadline.

Thanks.
Monday, 28 April 2014 11:53
Martin
I have a similar request to James.I am not sure where to add the code snippets.Whether in the model or the app_start.I am a newbie and this would go along way in enhancing my understanding.Thanks
Wednesday, 23 July 2014 08:42
Add a Comment