Get in touch
Thank you
We will get back to you as soon as possible

22.4.2010

6 min read

Building a custom JSON web service with ASP.NET MVC

This article describes how to build JSON service based on ASP.NET MVC application. Also here you can see example of advanced routing based on web-forms. Error-handling for almost all errors included.

Part I:  Routing, request verification and error handling

Description and requirements

As you know, we do a lot of .NET development here at Binary Studio. We had a task to create JSON service based on posted web pages which corresponds to specified format. Purpose of this task was to connect some legacy sites to one network with single data storage. Corresponding to that we have already /*specified*/ protocol specification.

Request specification:

Calls to service were implemented as posted web form requests with specified HTTP headers:

HTTP ReaderDescriptionExample
Content-TypeQuery type - form parametersform-data
SiteName of site, where request come from, some sites can have different methodstest-site.com
Request-TimestampQuery sending time25.10.2009 16:47:07

Web service should be able to detect request forgery. Here in the article we'll use a simplified check: timestamp should not differ from current server time more than specified time. Default value- one minute.

Request parameters passes as POST parameters as multipart/form-data.

Name of service call method passes as post parameter named "call"

User identifier (in DB) passes as parameter "user"

Other site always waiting for result returned as JSON object with two fields:

  • "Error" (string) –contains error messages
  • "Result"(object) – contains response data

As you can see, we have such tasks:

  1. Creating route based request on form parameters and HttpHeaders
  2. Validate request
  3. Return data in specified JSON format, even if request was wrong or something happens on server (of course, if server not down :)

And additional requirement:

4. Good test coverage, which allow us to check everything before server will go to production (this should be “hot replace”)

I will describe how we solved first three tasks here. The test story will be covered in the next article.

We found an elegant solution for this task with ASP.NET MVC.  It has everything we need:

  1. Good routing customization
  2. Ability of request headers parsing and automatically pass this data to controller methods
  3. Additional modules, which can be added on different stages of request processing
  4. Easy  work with  JSON

Advanced routing and parsing form data

I start from solving our first problem: We needed to route not standard http request, which have fixed URL, but pass needed params from headers and form data. As I already told, MVC has good routing possibilities and we need to add our custom route there. We will take data from HttpContext.Request, it has all needed data already parsed and stored in collections. HttpHeaders we can get from

httpContext.Request.Headers[] collection

and  form data with

httpContext.Request.Form.Get() method.

Сreation of our custom routing  will take 2 steps:

1. Create custom route class which get request data and put it to “controller” and “action”. Just add this class to Global.asax

public class CallRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var site = httpContext.Request.Headers["Site"];
string call = httpContext.Request.Form.Get("_call_");
if (call != null)
{
RouteData returnValue = new RouteData(this, new MvcRouteHandler());
returnValue.Values.Add("controller", site);
returnValue.Values.Add("action", call);
return returnValue;
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}

2. Add our new class to RegisterRoutes method in Global.asax:

routes.Add(new CallRoute());

As you can see, MVC routing is a powerful tool which allows you not only configure url as you want, but handle more difficult cases like http headers, form data and whatever you need.

Request validation

Next thing we do is request validation. For that purpose we used custom HTTPModule, which allowed us check request and return error messages (if needed) even before routing.

This task can be solved in two steps too:

1. Create class of http module. Http modules are a powerful tool, which will allow you to catch a lot of application events.

This class binds to BeginRequest event handler. Now we can check our request at first stage (even before routing). We just get http header from request, as we do that in routing, and verify data as we want

public class CheckRequestModule : IHttpModule
{
public void Init(HttpApplication application)
{
application.BeginRequest +=
(BeginRequestCheck);
}
public void Dispose()
{
}
private void BeginRequestCheck(Object source, EventArgs e)
{
HttpApplication app = (HttpApplication)source;
HttpContext ctx = app.Context;
 
try
{
// Avoid checking for other info for non-mvc request (such as images, script files etc.)
if ((HttpContext.Current.Handler is System.Web.DefaultHttpHandler))
{
return;
}
string dateStr = ctx.Request.Headers["Request-Timestamp"];
DateTime sendDate = Convert.ToDateTime(dateStr);
DateTime currTime = DateTime.UtcNow;
TimeSpan mins = (currTime - sendDate);
if (Math.Abs(mins.TotalSeconds) > 60)
{
WriteError(ctx, "Request time differs too much");
return;
}
//You can add here a lot of other checks, like password, hash from request params etc.
}
catch (Exception exc)
{
//sometimes exception from closing thread raises, so, we need to filter this
if (exc.GetType() != typeof(ThreadAbortException))
{
WriteError(ctx, "Error in request");
}
}
}
private void WriteError(HttpContext ctx, string errorText)
{
ctx.Response.Write("{ \"error\" : \"" + errorText + "\" }");
ctx.Response.End();
}
}

2. We need to register module in web-server. Add this string to section in Web.config

<add name="CheckRequestModule" type="CheckRequestModule" />

Now every request to your site will be checked before processing. This can be useful if you need to build highly loaded sites protected from unauthorized access.

Error handling

Our third task was to return answer even if service returns error. Of course, we could write every method with error handling, but this way produces big bunch of code, which is not very good for support. Instead of this we have made handler, which get lambda expression, process it and handles errors

public class ServiceHandler
{
private static readonly ILog Log = LogManager.GetLogger(typeof(ServiceHandler));
public object ExecuteMethod(Expression<Func<object>> dataExpression)
{
Response response = new Response();
try
{
// call service method and return result as JSON
var x = dataExpression.Compile();
var result = x();
return result;
}
catch (Exception exception)
{
// get diagnostic information and write into log
string methodName = "(Unknown)";
var stackTrace = new StackTrace();
if (stackTrace.FrameCount > 1)
{
methodName = stackTrace.GetFrame(1).GetMethod().Name;
}
Log.Error(String.Format("Service method '{0}' failed with exception '{1}'", methodName, exception.Message), exception);
response.Error = "Could not return answer due to technical reasons";
}
 return response;
}
}

Now our controller methods use lambda expressions and look like this

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult TestCall(string _user_)
{
return Json(ServiceHandler.ExecuteMethod(() => new { userId = _user_ }));
}

It just a little more difficult than usual.

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult TestCall(string _user_)
{
return Json(new { userId = _user_ }));
} }

We can add a little improvement, that allow us to write more simple code. This method returns object wrapped as JsonResult

private JsonResult ServiceResult(Expression<Func<object>> dataExpression)
{
return Json(ServiceHandler.ExecuteMethod(dataExpression));
}

Add it to your controller, and write:

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult TestCall(string _user_)
{
return ServiceResult (() => new { userId = _user_ }));
}

Now we can handle all errors in service methods, but still have a hole: What will happen if wrong params will be passed to method? Or how we handle any other error? For that purpose we can create two  walls of error handling:

  1. OnException method in controller
  2. Application_Error method in MvcApplication (in Global.asax)

Often error can happen when method parameter name or type don’t matches with passed in request. For that purpose we can override OnException method in controller. Our implementation catches ArgumentException, return standart error message and log detailed information:

protected override void OnException(ExceptionContext filterContext)
{
string excType = filterContext.Exception.GetType().Name;
Response err;
if (typeof(ArgumentException).Name == excType)
{
StringBuilder routeData=new StringBuilder();
foreach (KeyValuePair<string, object> pair in RouteData.Values)
{
routeData.Append(String.Format("\n{0} : {1}",pair.Key,pair.Value));
}
Log.Error(String.Format(Resources.CouldNotResolveMethodParams+" FormData:{0} \nHeadersData: {1} \n RouteData={2}",
BuildStringFromDictionary(Request.Form),
BuildStringFromDictionary(Request.Headers),
routeData), filterContext.Exception);
Response.Clear();
err = new Response { errtext = “Could not resolve params” };
Response.Write("{errtext : \"" + err.errtext + "\" }");
filterContext.ExceptionHandled = true;
}
}
private static string BuildStringFromDictionary(NameValueCollection collection)
{
StringBuilder data = new StringBuilder();
foreach (string key in collection.AllKeys)
{
data.Append(String.Format("\n{0} : {1}", key, collection[key]));
}
return data.ToString();
}

At last stage we should handle all other errors which can happen in our server. Easiest way to do this is to add Application_Error method. We don’t have JsonResult here, so we should write answer manually. Note, that I set StatusCode and StatusDescription to “200 OK”, it helps us to return normal answer in all cases (even if you have 404 or 500 error)

protected void Application_Error(Object sender, EventArgs e)
{
HttpApplication ctx = (HttpApplication) sender;

Log.Error("Unhandled server error", ctx.Context.Error);
Response.Clear();
Response.Write("{errtext : \"Could not execute request by  technical reasons\" }");

Response.StatusCode = 200;
Response.StatusDescription = "OK";
Server.ClearError();
Response.End();
}

So, what we have in result? We have web application, that applies to all our requirements.

You can dowload sample solution here.

Denis P., .NET team, Binary Studio

0 Comments
name *
email *