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.
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.
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:
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.
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(); }
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.
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 try
…catch
blocks will trap errors and print them.
Console.WriteLine("State: {0}", connection.State);
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)); } }
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.
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:
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.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.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.