SignalR Coding Best Practices


ASP.NET SignalR is a library for adding real-time web functionality to applications. It allows server-side code to push content to connected clients as it happens, in real-time.

We are seeing more and more SignalR applications come along. Whether it is the preferred technology or not, I think most .NET web-app developers will have to get comfortable with it.

There are a few things that I have come across in a recent project that are worth a mention and that should be considered when using SignalR. They are:

  1. Exception handling
  2. Using groups
  3. Server-side security
  4. Use of client-side .done() and .fail() callbacks

Whilst some of them are fairly basic so familiar to a casual user, and others are more advanced, they are all lessons learned from real developers so someone has made the mistake of not doing these.


Exception handling

When a server-side method throws an exception – the exception is not caught by default. An error may get returned and caught by the client-side but it is not logged or processed in any way. Just remember, any code that accesses a file, or a database will probably end up throwing an exception at some point or another! There are two ways to solve the problem of exception handling:

  1. Add a try/catch to every method (dull and copy/paste error prone)
  2. Have a single method that catches all exceptions.

The simplest way to implement the latter that I have found works is to make a HubPipelineModule:

public class ExceptionPipelineModule : HubPipelineModule
{
    TraceSource trace = new TraceSource("HubExceptions");
    protected override void OnIncomingError(ExceptionContext exceptionContext,
    IHubIncomingInvokerContext invokerContext)
    {
        //1. log
        try
        {
            MethodDescriptor method = invokerContext.MethodDescriptor;
            trace.TraceError("Exception thrown by: {0}.{1}({2}): 
            3}",method.Hub.Name, method.Name,
            String.Join(", ", invokerContext.Args), exceptionContext.Error);
        }
        catch
        {
        }

        //2. inform client of exception if you want
        try
        {
            invokerContext.Hub.Clients.Caller.notifyOfException();
        }
        catch
        {
        }

        //3. propagate error back the normal way base.
        OnIncomingError(exceptionContext, invokerContext)<
    }

    //... and then somewhere on startup 
    // (such as HttpApplication.Application_Start) call:

    Microsoft.AspNet.SignalR.GlobalHost.HubPipeline.AddModule
        (new ExceptionPipelineModule());

That works quite nicely.

Using groups

Using a chat room implemented with SignalR as an example, each group can be used as a different room. But with web applications Sinara would generally have groups assigned on the basis of some sort of secure requirement which a chatroom is not. The difference is: when we reconnect, we need to check that the groups the client claims to be accessing are indeed groups they are allowed to access. What we need is another HubPipelineModule that overrides the BuildRejoiningGroups method to either check the list of groups or just ignore them and overwrite them. An example of overriding the BuildRejoiningGroups method is available here:

public class RejoingGroupPipelineModule : HubPipelineModule
{
    public override Func<HubDescriptor, IRequest, IList<string>, 
       IList<string>> BuildRejoiningGroups(Func<HubDescriptor, IRequest, 
       IList<string>, IList<string>>rejoiningGroupsFunc)
    {
        rejoiningGroupsFunc = (hb, request, wantedListOfGroups) =>
        {
            List<string> assignedGroups = new List<string>();
            string username = request.User.Identity.Name;

            // you should now populate assignedGroups from the database or  
            // some other store etc.

            // wantedListOfGroups is the list of groups that the client 
            // wanted to use, but remember - it cannot be trusted 
            // (from a security point of view)!

            return assignedGroups;
         };
         return rejoiningGroupsFunc;
    }
}

//... and then somewhere on startup 
// (such as HttpApplication.Application_Start) call:

Microsoft.AspNet.SignalR.GlobalHost.HubPipeline.AddModule
    (new RejoingGroupPipelineModule

Server-side security A bit of an obvious one here. Each hub and also each public hub method should be analysed to determine what roles should have access to the function, and limit access appropriately. An example of this might be:

[Microsoft.AspNet.SignalR.Authorize(Roles = "MyRole")] 
//only allow members of the role: MyRole to execute it 
public void MyHubMethod() { ... }

Roles can then be mapped to the relevant source such as Active Directory, or some sort of database etc using the web.config.

Permissions should generally be enforced on the client-side as well by disabling a feature or a button.

Use of client-side .done() and .fail() callbacks

When invoking a SignalR hub-method, you should consider whether the call expects a synchronous response or not – and most do. Most methods will be synchronous (even something that just updates the database – the client might expect feedback of whether the update succeeds or fails etc), in which case you should be using .done() (and possibly .fail()). I guess what I am saying is:

  1. Do not be lazy by assuming that a synchronous method will run successfully (and promptly)
  2. Do not use two asynchronous calls to simulate a synchronous one: I really do not like the idea of invoking the server-method without a .done() callback – as if it was an asynchronous method – and then the server’s code then calls a separate client-method with the response. This is asking for trouble – you will need to make sure every code-branch calls the relevant client-method otherwise the client may be in a waiting state forever.

A quick reminder about .done() and .fail():

  • .done() is called when the method completes successfully (and will return the relevant return value of the call)
  • .fail() is called when an exception is thrown from the method (and propagated to the client)
  • .always() is always called (regardless of whether it succeeds or fails)

An example use of both methods might be (which includes an AJAX-styled spinner icon):

startSpinner();
self.hub.server.myHubMethod().done(function(returnVal) {
        //do something with return value
    }).always(function() { 
        stopSpinner(); 
    }).fail(function(error) { 
        showErrorMessage(error);
    });

If you follow the Exception Handling section such that you have a special client-method for handling server errors/exception, then you might be able to get rid of the .fail() callback (especially true if all .fail() methods simply call showErrorMessage or some similar function).

Note: The $.connection.hub.error handler is not a hub-wide fail callback sadly. That would have been nice but no. It seems that .error is more useful for catching connection failures.

Share