Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web

How to Build Self Descriptive Web API [Part I]

5.00/5 (1 vote)
20 Feb 2013Ms-PL2 min read 7.8K  
How to build self descriptive web API

Some time ago, I spoke on Microsoft user group about subject oriented programming and web services which speak natural language. Now, when I have some time, I can explain how to build your web front API to be readable by humans, rather, than by robots. So, let’s start.

Robot is not human.

First of all, let’s decide how our API should looks like. “Usual” WCF web end looks as follows:

http://mywonderfulhost/Service.svc?op=GetUserNamesByEmailAddress&email=joe@doe.com&format=json

All this means that we have WCF service, calling operation GetUserNamesByEmailAddress with parameter of email address and output should be JSON formatted. This is the obvious way of web API. For robots to consume it. But we want to be human and show our human web façade.

http://mywonderfulhost/json/getUser?joe@doe.com

Looks much better and passes exactly the same information to the service. So how this done? First of all, let’s get rid of annoying Service.svc. This can be done in various ways, but one of the better ways is by using HttpModule.

We create a class deriving from IHttpModule and upon the request begins, “translate” it from human to robot version.

C#
public class ProxyFormatter : IHttpModule {
private const string _handler = "~/Service.svc";
public void Init(HttpApplication context) {      
     context.BeginRequest += _onBeginRequest;      
}
private void _onBeginRequest(object sender, EventArgs e) {      
     var ctx = HttpContext.Current;      
       if (!ctx.Request.AppRelativeCurrentExecutionFilePath.Contains(_handler)) {      
        if (ctx.Request.HttpMethod == "GET") {      
          var method = ctx.Request.AppRelativeCurrentExecutionFilePath.RemoveFirst("~/");      
          var args = ctx.Request.QueryString.ToString();        
          ctx.RewritePath(_handler, method, args, false);      
        }  
     }      
    }

Also, if we are already there, let’s make the service to be consumed from other origins too. Just add OPTIONS method handling and we are done.

C#
private void _onBeginRequest(object sender, EventArgs e) {     
  var ctx = HttpContext.Current;      
  ctx.Response.AddHeader("Access-Control-Allow-Origin", AllowedHosts ?? "*");      
  if (ctx.Request.HttpMethod == "OPTIONS") {      
    ctx.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");      
    ctx.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept");      
    ctx.Response.End();      
  } else {      
    if (!ctx.Request.AppRelativeCurrentExecutionFilePath.Contains(_handler)) {      
     if (ctx.Request.HttpMethod == "GET") {      
       var method = ctx.Request.AppRelativeCurrentExecutionFilePath.RemoveFirst("~/");      
       var args = ctx.Request.QueryString.ToString();        
       ctx.RewritePath(_handler, method, args, false);      
     }       
    }      
  }      
}

The next step is parse URL to extract output method and the operation required. All information we need is inside WebOperationContext.Current.IncomingRequest. All we have to do now is to parse it.

C#
var req = WebOperationContext.Current.IncomingRequest;     
if (!_getMethodInfo(req.UriTemplateMatch, out format, out method)) {      
  WebOperationContext.Current.SetError(HttpStatusCode.PreconditionFailed, 
  "Wrong request format. correct format is : /operation/format(json:xml)");      
  return null;      
} else {      
//handle correct request      
}

Inside _getMethodInfo, we’ll count segments, find proper node formats and send out verdict.

C#
private bool _getMethodInfo(UriTemplateMatch match, out NodeResultFormat format, out string method) {     
  var c = match.RelativePathSegments.Count;      
  var f = Enum.GetNames(typeof(NodeResultFormat)).FirstOrDefault
          (n => n.EqualsIgnoreCase(match.RelativePathSegments.Last()));      
  if (f.NotEmpty()) {      
    format = (NodeResultFormat)Enum.Parse(typeof(NodeResultFormat), f);      
    method = match.RelativePathSegments.Take(c – 1).ToArray().Join(".");      
    return true;      
  }      
  format = NodeResultFormat.Unknown;      
  method = string.Empty;      
  return false;      
}

Now we know what output format is expected and what method was called by consumer. So, the next task is to “humanize” method names and parameters. The following methods do exactly the same, but require different arguments to pass into query.

  • GetUserNamesByEmailAddress (select name from users where email=…)
  • GetUserNamesByLastLogin (select name from users where lastLogin=…)
  • GetUserNamesByOrganizationAndFirstAndLastName (select name from users where organization like … and firstName like … and…)
  • GetUserNamesByUserId (select name from users where uid=…)
  • GetUserNames (select name from users)

So in order to make end human life easier, we’ll create helper data structure to hold all those possible values.

C#
public class UserInfo {     
public string Email {get; set;}      
public DateTime LastLogin {get; set;}      
public string Organization {get; set;}      
…

This class will be used only to hold input data (internally, we’ll find what object type was sent and try to match it to the data structure. This will allow us to hint what exact method should be called to bring information.

In our particular case, simple regex to find “whatever@wherever” like /.+@.+\..+/I tell us to execute ________ByEmailAddress override on backend. If we’ll find something like getUsers?1232234323 or getUsers?15-2-2013, we’ll be sure that GetUserNamesByLastLogin should be used.

So on, we can handle all common cases for human customer and start simplification of our life too. for example, create self descriptive automatic handlers in this method. But… we’ll speak about it next time.

Have a nice day (or night) and be good humans.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)