Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

RESTful SignalR Service

0.00/5 (No votes)
27 Oct 2015 1  
An article that explain hows to expose SignalR feature through an ASP.NET Web API, which helps applications that can use REST service and to broadcast a real-time message to their clients.

BackGround

The article is an another look that explains ASP.Net SignalR collaboration with ASP.NET Web API technology. You may read about the technology before and even using it. The article will be an addition to the existing articles written about the technology in CodeProject(ASP.Net SignalR). The article basically will explain how to expose SignalR feature through an ASP.NET Web API, which helps applications that can use REST service and to broadcast a real time message to their clients.

Things that will be covered in the article

  • Introduction to SignalR
  • Inversion of Control/Dependency Inject including SignalR Injection
  • Custom configuration section reader
  • SQL Server (Table, Procedure and Trigger)
  • A touch in WPF, INotifyPropertyChanged, ObservableCollection, DataGrid
  • JQuery, AJAX
  • Design principles and techniques

Introduction

It's been a while since we saw amazing interactive real time message exchange between users on some modern web applications such as Twitter, FaceBook, multi user online games and some other places. Users will exchange messages asynchronously without blocking each other in real time. SignalR makes such kind of interaction so simple that you can implement it anywhere necessary with little effort. Message interaction and persistent connection with two or muliple user/apps was not a new concepts to SignalR. It was attempted by different commuication techniques/protocols as well. To mention them Long polling, Server-Sent events WebSockets and Forever Frame. Each techniques are explained as follows.

  • Long polling - Initiated by client and stay it's persitent connection until the server send the data to the client or till the connection timeout expired. Here the connection is dedicated for only receiving data from the server. If client want to send data it will open another HTTP connection parallelly.The advantange of having is that once server send the request data, the polling will be disconnected.
  • Server-Sent events - as the name implies the server sent/response the message to the client in the form or event as soon as an update on the message is completed. The techique relies on an HTML5 API called Event Source. The communication is also initiated by client. http://caniuse.com/Server-Sent events
  • WebSockets - This is another API that helps to establish a simultanous persistent bi-direction communication between client/server at anytime needed. http://caniuse.com/WebSockets
  • Forever Frame/Comet - This is relies on a hidden iframe html tag in such a way that a long lived connection is establed to send chuck of message from the server to the client till all message content transfered completely.

So what does SignalR do differently? SignalR wraps all these communication protocols as a single(unified) framework. This makes easy for developers to concentrate on solving a problem that require a real time messaging without worrying the underlined communication between the client and the server. It also makes a best pick among the communication protocols upon initalization of the connection among client and server. The picking process is factored by the availability of the protocols on each side of the communicators. SignalR also uses a persistent connection that provides a mechanism to invoke/listen events to check if a connection is closed/opened or message is sent/received to/from clients. Once connection is established message can be sent and received synchronously/asynchronously.

Benefits of having SignalR inside Web API

As I stated earlier, the primary purpose of the article to expose SignalR capability through RESTful service so that client/server which relies on REST will have a chance to broadcast/send a message in real time. Benefit of having such implementation is that :-

  1. Database servers can broadcast/send any changes(insert/update/delete/other) in real time through REST service.
  2. Applications developed with other programming language which are capable of consuming REST service will have a chance to broadcast/send message in real time.
  3. IoT hardwares such as NetdunioPlus2, Arduino, Raspberry PI get a chance to send a real time message status regarding their states.
  4. Web applications/services that uses memory based caching mechanism will have a chance to listen/receive a real time changes to the cache before the cache expires or without restarting the application/service.
  5. Last but not list, it facilitates to design highly de-coupled systems that barely knows each other to have a capabilty of REST and SignalR technologies altogether.
RESTful SignalR Service and usage

Design and Implementation

The general idea of the solution is to define an ASP.NET Web API service that encapsulate SignalR real-time message broadcasting events. By using dependency injections(including SignalR DP), the SignalR hub context and the message broadcaster event REST API will be binded upon initialization of the service. In addition, the REST service consumers will have a ready and up running SignalR message broadcaster events without explicitly calling SignalR hub connection. The code is mainly divided into two sections, namely the RESTful SignalR Service and SignalR Broadcast Listener, a library that wraps SignalR Client library.

Defining RESTful SignalR service starts with defining IBroadCast interface and its implementer BroadCaster class which are shown below.

/// <summary>
/// IBroadCast interface
/// </summary>
public interface IBroadCast
{
    /// <summary>
    /// BroadCast messsage
    /// </summary>
    /// <param name="messageRequest">MessageRequest value</param>
    void BroadCast(MessageRequest messageRequest);

    /// <summary>
    /// Message Listener event handler
    /// </summary>
    event EventHandler<BroadCastEventArgs> MessageListened;
}
   
/// <summary>
/// BroadCaster class
/// </summary>
public class BroadCaster : IBroadCast
{
    // .........................................
    // Full code is available in the source code
    // .........................................

    /// <summary>
    /// BroadCaster class
    /// </summary>
    /// <param name="messageRequest">MessageRequest value</param>
    public void BroadCast(MessageRequest messageRequest)
    {
        EventHandler<BroadCastEventArgs> handler;
        lock (eventLocker)
        {
            handler = messageListenedHandler;
            if (handler != null)
            {
                handler(this, new BroadCastEventArgs(messageRequest));
            }
        }
    }
}

Then define Api controller class, MessageBroadCastController that will pass the broadcasted message to the SignalR Hub

/// <summary>
/// Message broadcaster ApiController class
/// </summary>
public class MessageBroadCastController : ApiController
{
    private IBroadCast _broadCast;

    // .........................................
    // Full code is available in the source code
    // .........................................

    /// <summary>
    /// Message broadcaster ApiController class
    /// </summary>
    public MessageBroadCastController(IBroadCast broadCast)
    {
        _broadCast = broadCast;
    }

    /// <summary>
    /// BroadCast message
    /// </summary>
    /// <param name="messageRequest">MessageRequested value</param>
    /// <returns>string message</returns>
    [HttpPost]
    public string BroadCast(MessageRequest messageRequest)
    {
       string response = string.Empty;
        try
        {
            _broadCast.BroadCast(messageRequest);
            response = "Message successfully broadcasted !";
        }
        catch (Exception exception)
        {
            response = "Opps got error. ";
            response = string.Concat(response, "Excepion, Message : ", exception.Message);
        }
        return response;
    }
}

Then define the BroadCastHub class, that registers the message events upon called by the clients.

/// <summary>
/// BroadCastHub class
/// </summary>
public class BroadCastHub : Hub
{
    // .........................................
    // Full code is available in the source code
    // .........................................

    /// <summary>
    /// BroadCastHub class
    /// </summary>
    public BroadCastHub(IBroadCast broadCast)
    {
        if (broadCast == null)
            throw new ArgumentNullException("BroadCast object is null !");

        BeginBroadCast(broadCast); // This will avoid calling to initialize a hub in message broadcaster client
    }

    /// <summary>
    /// Begin broadCast message
    /// </summary>
    /// <param name="broadCast">IBroadCast value</param>
    private void BeginBroadCast(IBroadCast broadCast)
    {
        // Register/Attach broadcast listener event
        broadCast.MessageListened += (sender, broadCastArgs)
            =>
            {
                RegisterMessageEvents(broadCastArgs);
            }; 
        
        // .........................................
        // Full code is available in the source code
        // .........................................
    }

    /// <summary>
    /// Register broadcasted message to SignalR events
    /// </summary>
    /// <param name="broadCastArgs">BroadCastEventArgs value</param>
    private void RegisterMessageEvents(BroadCastEventArgs broadCastArgs)
    {
        if (broadCastArgs != null)
        {
            MessageRequest messageRequest = broadCastArgs.MessageRequest;
            
            IClientProxy clientProxy = Clients.Caller;
            if (messageRequest.EventName != EventNameEnum.UNKNOWN)
            {
                clientProxy.Invoke(messageRequest.EventName.EnumDescription(), messageRequest.Message);
            }
            else
            {
                string errorMessage = "Unknown or empty event name is requested!";
                clientProxy.Invoke(EventNameEnum.ON_EXCEPTION.EnumDescription(), errorMessage); // Goes to the listener
                throw new Exception(errorMessage); // Goes to the broadcaster
            }
        }
    }
}

Once we define all the necessary classes and interfaces then register to global configuration to provide a well prepared message listener events through the service. But before that, lets define our dependency resolver that will assist the registration process.

/// <summary>
/// NInject dependency resolver class 
/// </summary>
public class NInjectDependencyResolver : NInjectScope, IDependencyResolver
{
    private readonly IKernel _kernel;

    // .........................................
    // Full code is available in the source code
    // .........................................

    /// <summary>
    /// NInject dependency resolver class
    /// </summary>
    /// <param name="container">IKernel value</param>
    public NInjectDependencyResolver(IKernel kernel)
        : base(container)
    {
        _kernel = kernel;
    }
}

/// <summary>
/// SignalR NInject dependency resolver class 
/// </summary>
public class NInjectSignalRDependencyResolver : DefaultDependencyResolver
{
    private readonly IKernel _kernel;

    // .........................................
    // Full code is available in the source code
    // .........................................

    /// <summary>
    /// SignalR NInject dependency resolver class
    /// </summary>
    /// <param name="container">IKernel value</param>
    public NInjectSignalRDependencyResolver(IKernel kernel)
    {
        _kernel = kernel;
    }
}

And finally, bind the message broadcaster along with SignalR hub connection context under owin Startup class. The important piece here is that to register the same NInject kernel instance for both of the dependency resolvers and wiring them to the global configuration.

[assembly: OwinStartup(typeof(RESTfulSignalRService.Startup))]
namespace RESTfulSignalRService
{
    /// <summary>
    /// OWIN startup class
    /// </summary>
    public class Startup
    {      
        /// <summary>
        /// Configuration value
        /// </summary>
        /// <param name="app">IAppBuilder value</param>
        public void Configuration(IAppBuilder app)
        {
            var kernel = new StandardKernel();
            
            // SignalR Hub DP resolver
            var resolver = new NInjectSignalRDependencyResolver(kernel);

            kernel.Bind(typeof(IHubConnectionContext<dynamic>)).
                    ToMethod(context =>
                    resolver.Resolve<IConnectionManager>().
                    GetHubContext<BroadCastHub>().Clients).
                    WhenInjectedInto<IBroadCast>();

            kernel.Bind<IBroadCast>().
                ToConstant<BroadCaster>(new BroadCaster());

            // IBroadcast DP resolver
            GlobalConfiguration.Configuration.DependencyResolver = new NInjectDependencyResolver(kernel);

            GlobalHost.Configuration.MaxIncomingWebSocketMessageSize = null; // Unlimited incoming message size
            
            app.Map("/signalr", map =>
            {
                map.UseCors(CorsOptions.AllowAll);
                map.RunSignalR(new HubConfiguration()
                {
                    EnableDetailedErrors = true,
                    Resolver = resolver
                });
            });
        }
    }
}

The SignalR Broadcast Listener library is nothing but a SignalR Hub event listener wrapper on top of SignalR .NET Client library.The library easies the way client listens hub events raised by the RESTful SignalR service. A major thing implemented around here is that client events will be attached to receive the message broadcasted by the RESTful SignalR service. Upon configuring the library with the required inputs(URL, hubName and event name) the broadcaster message will be transfered to the specified section on the client code. To facilitate these required inputs, we define a custom hub configuration reader class(HubConfigurationSection) that can read these inputs from a configuration (app.config/web.config) or assigning the inputs upon initialization of the class. The inputs for the custom hub configuration reader are defined as follows :
 

Configuration Name Description Example
hubURL a url where a message is broadcasted. In this case, the value is a message broadcaster REST service address. <hubUrl url="http://localhost/RESTfulSignalRService/"/>
hubName an actual hub name where the message event defined. <hubName name="BroadcastHub"/>
hubEventName an actual event name that is going to be listened. <hubEventName eventName="onMessageListened" />
hubListeningIndicator an indicator that enable/disable event listening. <hubListeningIndicator isEnabled="false"/>


Along with these configurable values, the client will pass an action(Action<object, BroadCastEventArgs>) that captures the broadcasted message fires back to the client code.So how is the library implemented? First lets see the overall class diagram of the library.

SignalR message listener class diagram

As you can see from the class diagram, there are few classes and interfaces that facilitate to gather the necessary configurations which I already explained earlier. IBroadCastListener facilitate the necessay operations for listening broadcasted message. IHubConfiguration interface helps to read a custom hub configuration from app.config/web.config file or be instantiated through it's implementer, HubConfigurationSection class. The curstom configuration looks like as follows.

<configSections>
<sectionGroup name="hubConfigurations">
    <section name="messageListenerConfiguration" 
                type="SignalRBroadCastListener.HubConfiguration.HubConfigurationSection, 
                      SignalRBroadCastListener" />
    <section name="insertListenerConfiguration" 
                type="SignalRBroadCastListener.HubConfiguration.HubConfigurationSection, 
                      SignalRBroadCastListener" />
    <!-- 
        Define more hub sections
    -->
</sectionGroup>
</configSections>
<hubConfigurations>
    <messageListenerConfiguration>
        <hubUrl url="http://localhost/RESTfulSignalRService/" />
        <hubName name="BroadcastHub" />
        <hubEventName eventName="onMessageListened" />
        <!-- <hubListeningIndicator isEnabled="false" /> --> <!-- Default is enabled -->

    </messageListenerConfiguration>
    <insertListenerConfiguration>
        <hubUrl url="http://localhost/RESTfulSignalRService/" />
        <hubName name="BroadcastHub" />
        <hubEventName eventName="onInserted" />
    </insertListenerConfiguration>
    <!-- 
    Configure more hub section 
    -->
</hubConfigurations>

Once these configurations  along with an event listener delegate passed, the BroadCastListener class will initialze the SignalR .NET client related classes upon ListenHubEvent method is called.

/// <summary>
/// BroadCastListener class
/// </summary>
public class BroadCastListener : IBroadCastListener, IDisposable
{
    // .........................................
    // Full code is available in the source code
    // .........................................

    ///  <summary>
    /// BroadCast listener class
    ///  </summary>
    ///  <param name="hubConfiguration">IHubConfiguration value </param>
    public BroadCastListener(IHubConfiguration hubConfiguration)
    {
           
    }

    /// <summary>
    /// Listen hub event and attach the message to the client event
    /// </summary>
    /// <param name="hubEvent">Client hub event</param>
    /// <returns>string value that shows status</returns>
    public string ListenHubEvent(Action<object, BroadCastEventArgs> hubEvent)
    {
        // .........................................
        // Full code is available in the source code
        // .........................................

        try
        {
            hubConnection.Start().
                ContinueWith(task
                    =>
                    {
                        if (task.IsFaulted)
                        {
                            throw task.Exception;
                        }
                        else
                        {
                            // Register broadcast events
                            if (_hubConfiguration.HubEventName != EventNameEnum.UNKNOWN)
                            {
                                 // Register/attach broadcast event
                                 lock (eventLocker)
                                 {
                                     BroadCastListenerEventHandler += (sender, broadCastArgs) 
                                                                    => hubEvent.Invoke(sender, broadCastArgs);
                                 }
                            }
                        }

                    }, TaskContinuationOptions.OnlyOnRanToCompletion).Wait();
        }
        catch (AggregateException aggregateException)
        {
            throw aggregateException;
        }

        if (hubConnection.State == ConnectionState.Connected)
            IsConnected = true;

        proxyHub.On<string>(_hubConfiguration.HubEventName.EnumDescription(),
                message =>
                {
                    _broadCastListenerEventArgs = new BroadCastEventArgs(
                        new MessageRequest()
                        {
                            Message = message,
                            EventName = _hubConfiguration.HubEventName
                        });
                    OnMessageListened(_broadCastListenerEventArgs);
                });           
    
       // .........................................
       // Full code is available in the source code
       // .........................................
    
    }

    private void OnMessageListened(BroadCastEventArgs broadCastArgs)
    {
        if (BroadCastListenerEventHandler != null)
            BroadCastListenerEventHandler(this, broadCastArgs);
    }
}

Notice the code around the ListenHubEvent method. I used TaskContinuationOptions.OnlyOnRanToCompletion to make sure all preceding tasks related to message broadcasting are completed and the appropriate message is received before firing back the content to the client code.

Client applications

1. Message Broadcaster

  • An SQL Server database table that broadcast data changes to the respsective clients in real time fashion. The store procedure below is responsible for calling the RESTful SignalR service.
    CREATE PROCEDURE [dbo].[USP_INVOKE_REST_SERVICE]
    	        @message NVARCHAR(MAX),
    	        @eventName NVARCHAR(20),
    	        @response NVARCHAR(1000) OUTPUT
    AS
    
    -- .........................................
    -- Full code is available in the source code
    -- .........................................
         
    -- Make sure the URL pointed to the right SignalR enabled service
    SET @url = CONCAT('http://localhost/restfulsignalrservice/messagebroadcast/broadcast?message=',
            @message,'&eventName=', @eventName)
    
    EXEC sp_OACreate 'MSXML2.XMLHTTP', @object OUT;
    EXEC sp_OAMethod @object, 'open', NULL, 'post', @url,'false'
    EXEC sp_OAMethod @object, 'send'
    EXEC sp_OAMethod @object, 'responseText', @response OUTPUT
     
    SELECT @response AS 'Response Text'
     
    EXEC sp_OADestroy @object
    
    Basically the store procedure invokes the service using XMLHttpRequest object and built-in SQL server store procedures such as sp_OACreate and sp_OAMethod. Then use this store procedure anywhere applicable. Suppose a database table(ConfigurationLookUp) need to broadcast/send its changes then by defining an Insert trigger we can achieve the desired functionality as shown below.
    CREATE TRIGGER [dbo].[TRG_INSERTED_CONFIGURATION_LOOKUP] ON [dbo].[ConfigurationLookUp] 
    FOR INSERT
    AS
    
    -- .........................................
    -- Full code is available in the source code
    -- .........................................
           
    -- Complex query can be applied 
    SELECT
        @id = i.ID,
        @name = i.Name,
        @value = i.Value
    FROM
        inserted i
            
    -- JSONify the message
    SET @message = CONCAT('{"ID":',CAST(@id AS NVARCHAR(20)),',"Name":"',@name,'","Value":"',@value,'"}')
    SET @eventName = 'onInserted'
    
    EXEC [dbo].[USP_INVOKE_REST_SERVICE]  @message, @eventName, @response OUTPUT
    
    Update and Delete trigger can also be implemented similar way. Two import thing to note here :
    1. Using a trigger will tell you the exact modified/changed record(row) out of the entire table records(rows) and it will broadcast this modified/changed records(rows)to the respective broadcast listner clients. This facilitates the listners to deal with only the modified/changed records(rows).
    2. A simple ADO.NET CRUD operation can invoke the service indirectly through the database and broadcast the change. This is also one example of a Virtual Message Broadcasting scenario.
  • Note: In order to work with sp_OACreate and sp_OAMethod, they should be configured by using global configuration setting store procedure called sp_configure. See https://msdn.microsoft.com/en-us/library/ms191188.aspx for how to enable them.
  • An IoT hardware such as Netdunio Plus 2 or Ardunio can call the service which enables any external app to listen the broadcasted message. A simple example that uses Netdunio Plus 2 is available in the source control.
  • A simple html client app that uses ajax post to broadcast a message.The code is also included in the source control.

2. Message Listeners

  • A Caching service that updates its cached data by listening the change source. In this case the source is a database.
    /// <summary>
    /// Memory cache manager
    /// </summary>>
    public class MemoryCacheManager
    {
        // .........................................
        // Full code is available in the source code
        // .........................................
    
        /// <summary>
        /// Get ConfigurationLookUps caches
        /// </summary>
        public static List<ConfigurationLookup> ConfigurationLookUpCaches
        {
            get
            {
                cache = MemoryCache.Default;
                _configurationLookUpCaches = cache[CONFIGURATION_LOOKUP_CACHE_KEY] as List<ConfigurationLookup>
                if (_configurationLookUpCaches == null)
                {
                    _configurationLookUpCaches = ConfigurationCacheDataAcces.GetConfigurationLookUps();
                    cache.Add(CONFIGURATION_LOOKUP_CACHE_KEY, _configurationLookUpCaches, policy);
                }
                return _configurationLookUpCaches ?? (_configurationLookUpCaches = new List<ConfigurationLookup>());
            }
        }
    
        /// <summary>
        /// IDBListener initializer method 
        /// </summary>
        /// <param name="dbListener">IDBListener value</param>
        public static void DBListener(IDBListener dbListener)
        {
            policy = new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(20) }; // A 20 min expiration policy
            HookDBListeners(dbListener);
        }
    
        /// <summary>
        /// ListenerEvent Hooker method 
        /// </summary>
        /// <param name="dbListener">IDBListener value</param>
        private static void HookDBListeners(IDBListener dbListener)
        {
            if (dbListener != null)
            {
                dbListener.InsertListener.ListenHubEvent(InsertListenerEvent);
                dbListener.UpdateListener.ListenHubEvent(UpdateListenerEvent);
                dbListener.DeleteListener.ListenHubEvent(DeleteListenerEvent);
            }
        }
    
        /// <summary>
        /// Insert event listener
        /// </summary>>
        /// <param name="sender">Sender value</param>
        /// <param name="broadCastEventArgs">BroadCastEventArgs value</param>
        void static InsertListenerEvent(object sender, BroadCastEventArgs broadCastEventArgs)
        {
            lock (_locker)
            {   
                ConfigurationLookup configurationLookUp;
                if (ConverterHelper.TryDeserialize<ConfigurationLookup>(broadCastEventArgs.MessageRequest.Message, 
                    out configurationLookUp))
                {
                    _configurationLookUpCaches.Add(configurationLookUp);
                    _configurationLookUpCaches.OrderByDescending(cl => cl.ID);
                    cache.Add(CONFIGURATION_LOOKUP_CACHE_KEY, _configurationLookUpCaches, policy);
                }         
            }
        }
    }
    
    Note : Such kind of implementation avoids unnecessary round trip to the entire database as well as a service restart action to reflect the changes made to the data.
     
  • Similarly, a WPF app that listens the database changes and reflects the change to an observable collection and an animated datagrid control. A complete code is available in the source control.
  • A simple html client app that uses SignalR JS Client to listen broadcasted message upon sent by a broadcaster.The code is also included in the source control.

Conclusion

For the past few years Microsoft Visual Studio team creates such an important technology to .NET echo system.It wasn't so easy to make client/server applications interactive in real time. Embedded plugins such as ActiveX, Flash, Silverlight was able to do the job. But they weren't elegant due to dependency on plugin that the client should enable them. Besides they are not fully supported for different environment like mobile and others.

Since SignalR introduced, too many difficult business scenarios that require real time communication are being resolved within few line of codes.It also worth to mention that latest and updated versions of browsers has lot of impact for such revolution.

References

History

  • Apr 21, 2015 : First Version
  • Updated on Apr 22, 2015 - Article format issue
  • Updated onApr 26, 2015 - Article format issue
  • Updated on May 19, 2015 - Article format issue
  • Updated on May 28, 2015 - Article format issue
  • Updated on May 29, 2015 - Article format issue
  • Updated on Oct 27, 2015  - Broken download issue

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here