Click here to Skip to main content
Click here to Skip to main content
Technical Blog

Tagged as

How to build self descriptive web API [part I]

, 20 Feb 2013 Ms-PL
Rate this:
Please Sign up or sign in to vote.
Some time ago I spoke on Microsoft user group about subject oriented programming and web services which speaking 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.

Some time ago I spoke on Microsoft user group about subject oriented programming and web services which speaking 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 following

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

All this means is 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 by various ways, but one of 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.

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 already there, let’s make the service to be consumed from other origins too. Just add OPTIONS method handling and we done.

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);      
     }       
    }      
  }      
}

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.

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.

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, next task is to “humanize” method names and parameters. Following method 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.

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)

Share

About the Author

Tamir Khason
Architect Better Place
Israel Israel
Hello! My name is Tamir Khason, and I am software architect, project manager, system analyst and [of course] programmer. In addition to writing big amount of documentation, I also write code, a lot of code. I used to work as a freelance architect, project manager, trainer, and consultant here, in Israel, but recently join the company with extremely persuasive idea - to make a world better place. I have very pretty wife and 3 charming kids, but unfortunately almost no time for them.
 
To be updated within articles, I publishing, visit my blog or subscribe RSS feed. Also you can follow me on Twitter to be up to date about my everyday life.

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141223.1 | Last Updated 20 Feb 2013
Article Copyright 2013 by Tamir Khason
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid