Extending the Hub pipeline

Among all the services that SignalR exposes and lets us customize, there is one that is particularly interesting: IHubPipeline. This service, as its name clearly states, represents the full pipeline that underpins a Hub, and its goal is to collect a set of modules to be executed during the initialization of a Hub. Each module implements IHubPipelineModule, and each method exposed by this contract is executed during the bootstrap phase. The job of those methods is to provide a factory methods that SignalR will then invoke during specific moments in the lifetime of every instance of a Hub. There are factories to build incoming and outgoing Hub invocations; to perform connection, reconnection, and disconnection tasks; to authorize connections; and to handle whether a client can rejoin groups on reconnection.

Thanks to the dependency injection mechanism we've been analyzing earlier, we could replace the IHubPipeline service with our custom implementation, but that would be a tough task to do without having a negative impact on SignalR's behavior. There are many things at that level that need to be executed, and we cannot afford forgetting about them while implementing a custom pipeline. That's why in this case it is better to avoid starting from scratch, and the recommended workflow is to use the HubPipeline member exposed by GlobalHost, and to add our modules there. Instead of implementing IHubPipelineModule directly, modules should derive from HubPipelineModule, which SignalR makes available to simplify the task of introducing new behaviors while still keeping the overall system functional. It exposes all its factory methods as virtual, allowing us to add our logic while still relying on the base type where necessary.

In this recipe, we will illustrate a couple of methods exposed by HubPipelineModule, which allow us to intercept the construction of incoming and outgoing invocations, and we'll use them to introduce a simple discrimination logic which, based on the name of the called method, might decide to skip it entirely. We'll build a simple test application and inject such logic on top of it using the GlobalHost.HubPipeline entry point.

Getting ready

For this recipe, we'll use a new empty web application, which we'll call Recipe45.

How to do it…

Our application will expose a Hub with three similar methods—all with the same behavior but just different names—and three related callbacks. Our discriminating logic will be based on those names. We will perform the following steps:

  1. We start by creating the usual EchoHub class using the SignalR Hub Class (v2) template. The following is the code we need:
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace Recipe45
    {
        [HubName("echo")]
        public class EchoHub : Hub
        {
            public void SayHello()
            {
                Clients.All.hello();
            }
    
            public void SayGoodbye()
            {
                Clients.All.goodbye();
            }
    
            public void SayAnything()
            {
                Clients.All.anything();
            }
        }
    }

    You can clearly see that the methods are trivial, and they all behave in the same way by broadcasting a callback with a name corresponding in some way to the method that triggered it.

  2. The client page we'll need to test this hub is fairly simple too. Let's create our usual index.html page and put some HTML controls in it as follows:
        <button id="sayHello">Say hello!</button>
        <button id="sayGoodbye">Say goodbye!</button>
        <button id="sayAnything">Say anything!</button>
        <ul id="messages"></ul>

    We need a few buttons, one for each method on the hub, and an unordered list to collect the answers. Let's then add some JavaScript code to bind the buttons to server-side calls, and to define callbacks which will append specific messages to the list:

        <script src="Scripts/jquery-2.1.0.js"></script>
        <script src="Scripts/jquery.signalR-2.0.2.js"></script>
        <script src="/signalr/hubs"></script>
        <script>
    
            $(function() {
                var hub = $.connection.hub,
                    echo = $.connection.echo,
                    message = function(m) {
                        $('#messages').append($('<li/>').text(m));
                    };
    
                echo.client.hello = function() {
                    message('Hello!'),
                };
                echo.client.goodbye = function () {
                    message('Goodbye!'),
                };
                echo.client.anything = function () {
                    message('Anything!'),
                };
    
                hub.start().done(function () {
                    $('#sayHello').click(function() {
                        echo.server.sayHello();
                    });
                    $('#sayGoodbye').click(function () {
                        echo.server.sayGoodbye();
                    });
                    $('#sayAnything').click(function () {
                        echo.server.sayAnything();
                    });
                });
    
            });
    
        </script>
  3. We finally add the Startup class performing the bootstrap sequence as follows:
    using Microsoft.AspNet.SignalR;
    using Microsoft.Owin;
    using Owin;
    
    [assembly: OwinStartup(typeof(Recipe45.Startup))]
    
    namespace Recipe45
    {
        public class Startup
        {
            public void Configuration(IAppBuilder app)
            {
                ...
    
                app.MapSignalR();
            }
        }
    }

    We just placed a few dots where later on we'll be adding some more code to register our custom implementation of IHubPipelineModule.

Let's do a first test of this page, just to verify that each button actually triggers a server-side call that is then reflected in a corresponding client-side callback being invoked to print out a specific message. When everything is good, we can move on to the next set of steps, which are as follows:

  1. Let's add to our project the skeleton of a new class called EchoHubModule, and let's specify HubPipelineModule as its base class, as follows:
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace Recipe45
    {
        public class EchoHubModule : HubPipelineModule
        {
            ...
        }
    }
  2. After that, we override the BuildIncoming() and BuildOutgoing() methods using the following code:
            public override 
              Func<IHubIncomingInvokerContext, Task<object>>
              BuildIncoming(Func<IHubIncomingInvokerContext, Task<object>>invoker)
            {
                Trace.WriteLine("BuidIncoming");
                return base.BuildIncoming(invoker);
            }
    
            public override 
              Func<IHubOutgoingInvokerContext, Task>
              BuildOutgoing(Func<IHubOutgoingInvokerContext, Task> invoker)
            {
                Trace.WriteLine("BuildOutgoing");
                return base.BuildOutgoing(invoker);
            }

    BuildIncoming() is invoked to return a factory method that will be used each time an invocation towards a Hub's method reaches the server, and it will return the actual server-side call to be performed. BuildOutgoing() performs a similar task, but it's invoked whenever an invocation towards a client-side callback is happening on the server. Our overrides are pretty basic. They just call into the base implementation to keep the base behavior, but just before that they send a message containing the name of the method to the Trace device.

  3. Let's go back to the Startup class to add the module registration code as follows:
    GlobalHost.HubPipeline.AddModule(new EchoHubModule());

    If we reload our page and press the buttons, we will experience the same behavior as before, but if we take a look at the Trace console in Visual Studio, we will notice that the two messages, BuildIncoming and BuildOutgoing, are printed out, once each, when the first page connects and the EchoHub instance is initialized by SignalR's runtime for the first time. That confirms the fact that these methods are invoked just once at the start-up of the application.

  4. Back to our module, let's make our methods more interesting by replacing their return statement with something more useful. We start with BuildIncoming() as follows:
                return base.BuildIncoming(ctx =>
                {
                    Trace.Write(string.Format("I might call {0}...",ctx.MethodDescriptor.Name));
    
                    if (ctx.MethodDescriptor.Name == "SayHello")
                    {
                        Trace.WriteLine(" but I won't :(");
                        return null;
                    }
    
                    Trace.WriteLine(" and I will!");
                    return invoker(ctx);
                });

    This code needs some comments:

    • We call the base class method again, but this time we supply a lambda expression whose goal is to decide whether we should actually perform the invocation or not. The signature of the lambda expression must, of course, match the one of the incoming invoker() parameter.
    • Inside the expression, we first print out the name of the method that is going to be called.
    • Then we check the method's name to decide what happens next. If that's equal to SayHello we want to block its execution, which is as simple as having our expression returning a null value. In any other case, we call the invoker() parameter we received and return its exit value, which will be a task representing the actual execution of the Hub's method requested by the client.
    • In both execution branches, we send a message describing what happened to the Trace device.
  5. Let's do something similar with BuildOutgoing(), as follows:
                return base.BuildOutgoing(ctx =>
                {
                    Trace.Write(string.Format("I might call back {0}...",ctx.Invocation.Method));
    
                    if (ctx.Invocation.Method == "goodbye")
                    {
                        Trace.WriteLine(" but I won't :(");
                        return null;
                    }
    
                    Trace.WriteLine(" and I will!");
                    return invoker(ctx);
                });

    What happens here is similar to what we did with BuildIncoming()—the differences being the following:

    • We are checking a remote callback invocation, which would be initiated on the server but executed on the client
    • The context object (ctx) has a slightly different shape, but it's still bringing similar information about the requested invocation
    • Here we check the method's name against the goodbye string, with the goal of blocking any call with that name

Our last test session will eventually show a different outcome. The only button fully working would be the Say Anything one, whereas the SayHello() method would get completely blocked and the SayGoodbye() one would run on the server, but with no client callback being triggered. We have been able to both discriminate the execution of specific methods according to a custom execution workflow and print out diagnostic messages while checking each invocation. All this has been possible without having to add a single line of code inside the methods themselves.

There's more…

This sample showed how SignalR allows us to hook code into its own machinery to customize its behavior or add specific behaviors. These kinds of capabilities are generally used to solve orthogonal infrastructural aspects, such as logging, caching, or authorization, which are usually known as cross-cutting concerns. If you are curious about it, you can dig more around the discipline of Aspect Oriented Programming, maybe starting with this link: http://en.wikipedia.org/wiki/Aspect-oriented_programming.

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

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