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 Header

Description

Example

Content-Type

Query type - form parameters

form-data

Site

Name of site, where request come from, some sites can have different methods

test-site.com

Request-Timestamp

Query sending time

25.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 <HttpModules> 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 solutionhere.

In my next article I will describe our test experience for this project. 

 

Denis P., .NET team,
Binary Studio

2 comment(s) so far


  1. the coding standard is looking best but I am new in this site to create web service .I need some explanations about procedure in this article may be it working well but I am confused about work flow in this.

    1. defwe

Post your comment

  • Comment

Contact:


Skype call: My status binary_studio
E-mail: info@binary-studio.com

Quick Navigation:




Our clients say:


We started the cooperation in spring 2006 with a comparatively small project and are now working together in even our core-products. Approximately in a year after the beginning of the teamwork, we have twice increased the volume of the contract. This was possible, because Binary Studio provided us with motivated and capable software developers. We are aiming for a long-term cooperation.

Dr. Reiss Jurgen (Prokurist, Manager Research & Development, "Investis Flife" AG, Wurzburg, Germany)
Install MS Silverlight to have the original view.

Blog:


WPF Software Development. Model-Vie...
Applied C# Xml Documentation | Bina...
Some aspects of software localizati...
Fixed price and dedicated team (of...
How to work with AJAX log updater

News:


Photos. Binary. Transparent, open a...
Binary Studio takes part in the BBC...
Binary Studio opens office in San F...
Binary opens an office in Czech Rep...
A MSCT Certificate for Binary Devel...

LiveZilla Live Help