Error Handling in WCF WebHttp Services with WebFaultException

This is part five of a twelve part series that introduces the features of WCF WebHttp Services in .NET 4.  In this post we will cover:

  • Handling Errors with the WebFaultException class

Over the course of this blog post series, we are building a web service called TeamTask.  TeamTask allows a team to track tasks assigned to members of the team.  Because the code in a given blog post builds upon the code from the previous posts, the posts are intended to be read in-order.

Downloading the TeamTask Code

At the end of this blog post, you’ll find a link that will allow you to download the code for the current TeamTask Service as a compressed file.  After extracting, you’ll find that it contains “Before” and “After” versions of the TeamTask solution.  If you would like to follow along with the steps outlined in this post, download the code and open the "Before" solution in Visual Studio 2010.  If you aren’t sure about a step, refer to the “After” version of the TeamTask solution.

Note:   If you try running the sample code and see a Visual Studio Project Sample Loading Error that begins with “Assembly could not be loaded and will be ignored…”, see here for troubleshooting.

Getting Visual Studio 2010

To follow along with this blog post series, you will need to have Microsoft Visual Studio 2010 and the full .NET 4 Framework installed on your machine.  (The client profile of the .NET 4 Framework is not sufficient.)  At the time of this posting, the Microsoft Visual Studio 2010 Ultimate Beta 2 is available for free download and there are numerous resources available regarding how to download and install, including this Channel 9 video.

Step 1: Handling Non-Existent Users

In the very first post of this series, we implemented the GetUser() service operation.  The GetUser() service operation returns the details of a user given a username provided in the request URI. 

A precondition of the GetUser() operation is that the given username exists in the database.  But what happens when this precondition isn’t met?  Unfortunately, the implementation from the first blog post does something quite awkward: It returns a response with an HTTP status code of 200 along with an empty message body. 

Let’s improve the GetUser() operation to handle non-existent user errors in a manner more consistent with HTTP.  To do this, we’ll use the WebFaultException class to both set the HTTP status code to 404 (Not Found) and provide an indication of the error in the message body. 

  1. If you haven't already done so, download and open the “Before” solution of the code attached to this blog post.

  2. Open the TeamTaskService.cs file in the code editor.

  3. In the GetUser() operation add the code below after the LINQ query for the user.  You’ll also need to add “using System.Net;” to the code file.

        if (user == null)
        {
            throw new WebFaultException<string>(
                string.Format("There is no user with the userName '{0}'.", userName),
                HttpStatusCode.NotFound);
        }

    There are both generic and non-generic versions of the WebFaultException.  The generic version is generally the more appropriate one to use, as it allows you to specify the exception details that will be serialized into the body of the response.  In this case, the exception details are just a string, but any type that can be serialized can be used.  We’re also setting the correct HTTP status code of 404 (Not Found).

    As you can see, correctly handling errors with a WCF WebHttp Service is as simple as throwing an exception.

Helpful Tip: If you’ve used WCF to build SOAP services, you may notice that the WebFaultException is very similar to the FaultException class.  This isn't just a coincidence. The WebFaultException actually derives from the FaultException class.  Therefore in the context of a SOAP service, the WebFaultException will behave as a FaultException—the detail will be serialized to the body of the SOAP message and the HTTP status code will be ignored as it isn’t relevant with SOAP.

 

Step 2: Viewing the Error on the Client

We could verify that our improved implementation of the GetUser() operation correctly handles non-existent users with the browser, but we'll write some client code to do this instead.  This is because we want to demonstrate one of the more exciting features of the WebFaultException class—that it composes with automatic and explicit format selection

With the WebFaultException, the detail of the exception that is serialized in the body of the response message will always be in the format (XML or JSON) that the client would have received had there not been an error.  If the client was expecting XML, the client will get the exception detail serialized as XML.  Likewise, if the client was expecting JSON, the client will get the exception detail serialized as JSON.

  1. Open the Program.cs file of the TeamTask.Client project in the code editor and copy the following static method into the Program class:

        static void GetUser(HttpClient client, string userName, string accepts)
        {
            Console.WriteLine("Getting user '{0}':", userName);
            using(HttpRequestMessage request = new HttpRequestMessage("GET",
                     "Users/"+ userName))
            {
                request.Headers.Accept.AddString(accepts);
                using (HttpResponseMessage response = client.Send(request))
                {
                    Console.WriteLine(" Status Code: {0}", response.StatusCode);
                    Console.WriteLine(" Content: {0}",
                        response.Content.ReadAsString());
                }
            }
            Console.WriteLine();
        }

    The static GetUser() method is very similar to the client code we wrote in part two of this blog post series.  The only thing to notice is that we are able to set the HTTP Accept header on the request.

  2. Now we’ll implement the Main() method to call GetUser() twice: once with an XML content type in the Accept header and a second time with a JSON content type.  Replace the Main() method implementation with the following code:

        using (HttpClient client = new HttpClient("https://localhost:8080/TeamTask/"))
        {
            GetUser(client, "noSuchUser", "application/xml");
            GetUser(client, "noSuchUser", "application/json");

            Console.ReadKey();
        }

  3. Start without debugging (Ctrl+F5) to get the TeamTask service running and then start the client by right clicking on the TeamTask.Client project in the “Solution Explorer” window and selecting “Debug”—>”Start New Instance”.  The console should contain the following output:

    NoSuchUSerInConsole_sansUserNames

    Notice that there is no such user with the username “noSuchUser” and that the status code for both responses is 404 (Not Found).  With the first request, the response content is in XML.  With the second request, the response content is in JSON, which happens to be just a quoted string for this simple example.  

Step 3: Handling Errors in the UpdateTask() Operation

The UpdateTask() operation, which we added to the TeamTask service in the part three, could also benefit from some improved error handling.

First, in the current implementation of the UpdateTask() operation, we are parsing a UriTemplate path variable to get the id of the task to update.  We assume that parsing the id will always be successful, but this isn’t a valid assumption.  We’ll change the implementation of the UpdateTask() operation so that it returns an HTTP status code of 400 (Bad Request) when the request URI includes a id value that can’t be parsed as an integer.

Second, it is possible that a client could try to update a non-existent task.  In the current implementation, this error will surface as a OptimisticConcurrencyException, which is thrown from the ADO.NET Entity Framework code on trying to persist the updated task to the database.  This OptimisticConcurrencyException will result in an HTTP response with a status code of 400 (Bad Request) and HTML content that explains that the server encountered an error.  This is the default behavior for unhandled, non-WebFaultExceptions.  We’ll change this so that the UpdateTask() operation returns an HTTP status code of 404 (Not Found) when a client attempts to update a non-existent task.

  1. Open the TeamTaskService.cs file in the code editor.

  2. In the UpdateTask() operation replace the code for parsing the id with the following code:

        // The id from the URI determines the id of the task
        int parsedId;
        if(!int.TryParse(id, out parsedId))
        {
            throw new WebFaultException<string>(
                string.Format(
                    "The value '{0}' is not a valid task id. The id must be an integer.", 
                    id), HttpStatusCode.BadRequest);
        }
        task.Id = parsedId;

  3. Wrap the call to SaveChanges() on the ObjectContext with a try-catch block that will properly handle the OptimisticConcurrencyException as shown below.  You’ll also need to add “using System.Data;” to the code file.

        try
        {
            objectContext.SaveChanges();
        }
        catch (OptimisticConcurrencyException)
        {
            throw new WebFaultException<string>(
                string.Format("There is no task with the id '{0}'.", parsedId),
                HttpStatusCode.NotFound);  
        }

  4. After implementing these error handling improvements, the UpdateTask() should look similar to the code below.  We’ll leave it as an exercise for the reader to verify that these error handling improvements behave as expected.

        [Description("Allows the details of a single task to be updated.")]
        [WebInvoke(UriTemplate = "Tasks/{id}", Method = "PUT")]
        public Task UpdateTask(string id, Task task)
        {
            // The id from the URI determines the id of the task
            int parsedId;
            if (!int.TryParse(id, out parsedId))
            {
                throw new WebFaultException<string>(
                    string.Format(
    "The value '{0}' is not a valid task id. The id must be an integer.",
                         id), HttpStatusCode.BadRequest);
            }
            task.Id = parsedId;

            using (TeamTaskObjectContext objectContext =
                      new TeamTaskObjectContext())
            {
                objectContext.AttachTask(task);
                try
                {
                    objectContext.SaveChanges();
                }
                catch (OptimisticConcurrencyException)
                {
                    throw new WebFaultException<string>(
                        string.Format("There is no task with the id '{0}'.", parsedId),
                        HttpStatusCode.NotFound);  
                }
                // Because we allow partial updates, we need to refresh from the dB
                objectContext.Refresh(RefreshMode.StoreWins, task);
            }

            return task;
        }

Next Steps: Using Routes to Compose WCF WebHttp Services

The TeamTask service can now handle possible client errors such as trying to update or retrieve non-existent tasks or users.  Of course, there are still opportunities to improve the error handling of the service but we'll leave such improvements as an exercise for the reader.  

In part six of this blog post series we’ll continue to improve the TeamTask service.  We’ll refactor our single TeamTaskService class into two separate classes (TaskService and UserService) as we learn about the ability to compose WCF WebHttp Services with the new ASP.NET routes integration feature introduced in .NET 4.

Randall Tombaugh
Developer, WCF WebHttp Services

Post5-ErrorHandling.zip