Wednesday, June 25, 2008

RESTful WCF Web Services

I spent some time looking at how you could remove the ".svc" from WCF web services and looking at it last year i got quite close but just not close enough. Well, i'm back writing WCF services again to expose JSON for my REST API, and this time i was determined to spend time looking for a solution. I found some people who did a lot of great work but the solutions were all missing the extra i really needed. I needed an extensible method of re-writing any kind of URL.

The solution was to combine the ideas here and here using a mixture of IHttpModule and regular expressions to provide a solution allowing any URL to be mapped in the configuration file. The code is below and is made up of RestModule.cs and some rules in the web.config.

This code will actually work for any URL you wish to make RESTful but my target was WCF.

RestModule.cs




using System;
using System.Configuration;
using System.Web;
using System.Xml;
using System.Text.RegularExpressions;
using System.Xml.Xsl;

public class RestModule : IHttpModule
{
public void Dispose()
{ }

public void Init(HttpApplication app)
{
app.BeginRequest += delegate
{
HttpContext ctx = HttpContext.Current;
string path = ctx.Request.AppRelativeCurrentExecutionFilePath;

if (ctx.Request.Url.Query.Length > 0) path = path + ctx.Request.Url.Query;
string zSubst = ReWriter.Process(path);

//did we have a new path match?
if (!zSubst.Equals(path))
{
int i = zSubst.IndexOf('/', 2);
if (i > 0)
{
string svc = null;
string rest = null;
string qs = null;

svc = zSubst.Substring(0, i);
rest = zSubst.IndexOf('?') > -1 ? zSubst.Substring(i, zSubst.Length - i).Split('?')[0] : zSubst.Substring(i, zSubst.Length - i);
qs = zSubst.IndexOf('?') > -1 ? zSubst.Split('?')[1] : null;

ctx.RewritePath(svc, rest, qs, false);
}
}
};

}
}

public class ReWriter : IConfigurationSectionHandler
{
protected XmlNode _oRules = null;

public string GetSubstitution(string zPath)
{
Regex oReg;

foreach (XmlNode oNode in _oRules.SelectNodes("rule"))
{
oReg = new Regex(oNode.SelectSingleNode("url/text()").Value, RegexOptions.IgnoreCase);
Match oMatch = oReg.Match(zPath);

if (oMatch.Success)
{
return oReg.Replace(zPath, oNode.SelectSingleNode("rewrite/text()").Value);
}
}

return zPath;
}

public static string Process(string requestpath)
{
ReWriter oRewriter = (ReWriter)System.Configuration.ConfigurationManager.GetSection("system.web/urlrewrites");

return oRewriter.GetSubstitution(requestpath);
}

#region Implementation of IConfigurationSectionHandler
public object Create(object parent, object configContext, XmlNode section)
{
_oRules = section;

// TODO: Compile all Regular Expressions

return this;
}
#endregion
}



Add a new section group to your web.config ...





<sectionGroup name="system.web">
<section name="urlrewrites" type="ReWriter"/>
</sectionGroup>



Finally add some rules in the web.config in the system.web section ...





<urlrewrites>
<rule>
<url>/MapService/GetLocation\?(.*)</url>
<rewrite>/MapService.svc/GetLocation?$1</rewrite>
</rule>
<rule>
<url>/MapService/GetLocation/(.*)</url>
<rewrite>/MapService.svc/GetLocation?target={"Name":"$1"}</rewrite>
</rule>
</urlrewrites>




Now you can have ...

http://site.com/WebSite/MapService/GetLocation?target={"Name":"Starbucks%20Manhattan"}

or ....

http://site.com/WebSite/MapService/GetLocation/Starbucks%20Manhattan

Neat heh?

2 comments:

Ankur said...

GetLocation as a name for a resource is not very RESTful. I suggest you change that to something like Locations. Anyway, for renaming, you should check out the video posted here: http://msstudios.vo.llnwd.net/o21/mix08/08_WMVs/T01.wmv

weblivz said...

I think GetLocation is a good name for a RESTful service as it's very much unambiguous for a start.

As a real world example, check the Flickr API - possibly the most successful on the web.

http://www.flickr.com/services/api/

Thanks for the vid pointer.