Managing errors across a complex asynchronous workflow

In the last recipe of the chapter, as we did in Chapter 3, Using the JavaScript Hubs Client API, we'll learn how to handle errors occurring during all the possible SignalR operational steps and connection states.

The HubConnection class from the .NET client offers a specific Error event, which is useful to manage most of the exceptions raised by SignalR itself, whereas any server-side exception occurring during a Hub method invocation can be trapped simply by surrounding the client-side invocation in a try...catch block. We'll talk a little bit more about the latter case while commenting on the code of this recipe.

Getting ready

Before proceeding with this recipe, please remember to start the server application that we used in the homonymous recipe called Managing errors across a complex asynchronous workflow from Chapter 3, Using the JavaScript Hubs Client API. This is because our code will connect to its Hub, whose method is already randomly throwing exceptions that we'll take advantage of in order to test our error-handling strategy.

How to do it…

We first create a console application project, named Recipe24, as described in the Introduction section, and then we'll work on the source code of the Program class, applying the following steps:

  1. We first introduce the basic stuff using the following code:
    using System;
    using System.Threading.Tasks;
    using Microsoft.AspNet.SignalR.Client;
    
    static void Main(string[] args)
    {
        Do().Wait();
    }
    static async Task Do()
    {
        const string url = "http://localhost:42477";
        var connection = new HubConnection(url);
        var echo = connection.CreateHubProxy("echo");
    
        ...
    }

    As usual, the port number we specify (42477) must match the one used in the server project that we are connecting to.

  2. We add a helper method to the Program class, which we'll use to print out colorful messages easily on the standard output window:
    public static void AsColorFor(this ConsoleColor color,
        Action action)
    {
        Console.ForegroundColor = color;
        action();
        Console.ResetColor();
    }
  3. We go back to the Main() method, where we now define our Error event handler as shown in the following code:
    connection.Error += error => 
        ConsoleColor.Red.AsColorFor(() => 
            Console.WriteLine("Error from connection: {0}",error));

    The event's goal is straightforward: any error happening on the connection will trigger it. Its signature is simple. We just need to define an argument to receive the trapped exception, which we'll then print in red using our helper method.

  4. We are ready to start writing our specific workflow to simulate different error conditions. In Chapter 3, Using the JavaScript Hubs Client API, we used a pretty interactive way to simulate and handle errors from a web page. Now, we'll use a simplified strategy where a set of operations is continuously repeated inside an infinite loop. The reason for this choice is the fact that this is a book about SignalR and not about asynchronous programming in general. In order to have an exhaustive sample working properly, we would have to go through a lot of off-topic code just to correctly handle a set of tricky issues originated by the usage of nested asynchronous calls from a command-line interactive process. It's better to keep it simple and concentrate on SignalR's features. Let's go back to the Do() method and complete it with the following code:
    do
    {
        ...
    
        await Task.Delay(3000);
    } while (true);

    This simple code is just looping indefinitely, and at every iteration, it waits for three seconds before moving forward.

    The code we are going to write inside the loop is simple. Basically, it will check if we are connected to the hub, and if we're not, it will establish a connection. Then, it will check again if the connection is valid and, if that's the case, it will try to call the Hub method exposed by the server that we are using. While doing these operations, it will constantly print out messages describing how things are going. A bunch of trycatch blocks will trap errors and print them.

  5. We first print out the current state of the connection with the following code:
    Console.WriteLine("State: {0}", connection.State);
  6. After that, we test if we are currently disconnected, and if that's the case, we call Start() using the await prefix to wait for its completion. The connection attempt is done inside a try...catch block to trap networking issues; for example, in case the remote host is unreachable. The following code depicts this:
    if (connection.State == ConnectionState.Disconnected)
    {
        try
        {
            await connection.Start();
            Console.WriteLine("Connected!");
        }
        catch (Exception ex)
        {
            ConsoleColor.Yellow.AsColorFor(() => 
                Console.WriteLine("Error connecting: {0}", ex));
        }
    }
  7. Then, we adopt a similar strategy when calling the remote method; we first check if we are connected, and if that's the case, we call Invoke() to trigger the remote invocation. A try...catch block is there to protect us from errors occurring during the remote invocation (server-side errors trigger InvalidOperationException errors on the client). Again, we're using await to handle the asynchronous call and wait for its completion, as shown in the following code snippet:
    if (connection.State == ConnectionState.Connected)
    {
        try
        {
            var response = await echo.Invoke<string>("Say",
                "hello!");
            ConsoleColor.Green.AsColorFor(() =>
                Console.WriteLine("Said: {0}", response));
        }
        catch (InvalidOperationException ex)
        {
            ConsoleColor.Blue.AsColorFor(() => 
                Console.WriteLine("Error during Say: {0}", ex));
        }
    }

We are ready to launch the application and observe the messages printed on the screen. While the client is running, we can try to stop and restart the server to see how both the error-handling logic that we put in place and the SignalR connection-handling strategies work together.

How it works…

The code that we used is pretty simple. It applies a simple pattern where operations are performed inside a try...catch block, and then a success message is printed. The message would, of course, be skipped in case an exception is triggered, and the corresponding catch handler would print out the details about the error that occurred. The whole loop is repeated every three seconds, which allows us to manipulate the availability of the server while the client runs. In this way, we can observe how the client reacts to different anomalous conditions we are creating. The try...catch blocks work well along with the await keyword, making asynchronous exceptions flow into the catch blocks as if they were happening synchronously.

In order to understand better what is going on here, let's see a list of possible abnormal conditions that we can simulate:

  • Launch the application before starting the server: The process will continuously try to connect and fail until the server becomes available, printing out yellow messages from the catch handler that matches the Start() invocation. If we turn on the server while the client is still running, at some point the connection will succeed and the Connected! message will be displayed.
  • Server-side errors: When connected, the Invoke() method calls will reach the server-side Hub method, which will randomly fail because of the way it's implemented. If successful, the client will receive a hello string, and it will display it in green, whereas, in case of a remote failure, the corresponding catch block will kick in and display a blue message about the remote problem.
  • Server becomes unavailable after connection: If we now stop the server, at some point the SignalR connection will enter an invalid state (normally, Reconnecting). Therefore, none of our if statements will pass and at each iteration, nothing will happen. Behind the scenes, SignalR will be trying to reconnect, but it will fail because the server is unavailable. This kind of error is trapped by the Error handler on the connection, and it will be displayed in red. If the server does not become available before a specified time, (in which case, SignalR would successfully reconnect and things would go back to normality), SignalR will decide to stop trying to reconnect and move to the Disconnected state. In this situation, our first if block will be executed and the related connection attempts will fail, displaying yellow error messages. If we turn on the server at this point, the connection will be established successfully, and we'll go back to normality.

The actual internals of these mechanisms are complex and influenced by the actual transport strategy used by SignalR. As we already said while illustrating the corresponding recipe from the previous chapter, going too deep into technical detail about how things are handled behind the scenes would be out of the scope of this book. SignalR is simply smart enough to perform all the necessary checks and handshake tasks to supply both a natural error handling and an integrated connection lifecycle handling.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset