Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

SignalR with Self-hosted Windows Service

4.94/5 (35 votes)
24 Jul 2019Apache8 min read 108.8K   713  
Two SignalR Demo Projects: Self-Hosted Windows Service and Broadcasting Application

Introduction

Please note that this article is derived from and based primarily on the article, Tutorial: Server Broadcast with SignalR 2 by Tom Dykstra and Tom FitzMacken where in this article, we are going to build a chat application.

This article covers introductory information on a self-hosted service using SignalR. Please see my previous article on SignalR with web applications here that also contain valuable introductory information.

SignalR is normally hosted in an ASP.NET application in IIS, but it can also be self-hosted in a Console, WPF or Windows Service application. If you want to create a WPF or Console SignalR application, then it must be Self-Hosted. SignalR is built on top of OWIN (Open Web Interface for .NET), which defines an abstraction layer between .NET web servers and web applications.

This application will be built using Topshelf so that we don't need to understand the complexities of Windows Service classes, perform installation using InstallUtil.exe. It will also allow us to debug the application as simply as debugging a Console application.  If you actually want to install the application as a Windows service, then Topshelf allows you to simply type this in a command prompt that you have run as administrator.

C:\Users\myUserId> SignalRSelfHostedService install

Please download the sample project source code here and here.

Background

Originally, the internet worked by a user putting in a URL into their browser and it downloaded the web page content. Nothing changed on the client page until the user performed a refresh and made another request to the Web server.

Then came AJAX which allowed the web page to be updated asynchronously so the user doesn't need to reload an entire web page if only a small part needs to be updated.  It does so by using an XMLHttpRequest object.  The problem with this methodology is that the server cannot initiate contact with the client, while with SignalR, the server can.

Also, technologies were developed called polling and long polling.  This caused the client to periodically make requests to the server to update the web page contents.  The problem with this methodology is that it makes inefficient use of computing resources to repeatedly perform these polling actions.

Another methodology used to improve the responsiveness of a web browser is Server Sent Events (SSE) but the problem with this is that it is only supported by a few browsers.

SignalR uses multiple technologies, including WebSockets and JavaScript, and it will even use polling and long polling if WebSockets are not available on a particular browser.  It abstracts away these details and lets the developer focus on the logic of the application.

Creating the Server

Start by creating a Console Application application (which can be done if you are using Topshelf), or if you are not using Topshelf, then create a Windows service in Visual Studio, ensuring that your project uses .NET 4.5 or greater:

SelfHostedService

Then type this at the package manager console:

PM> Install-Package Microsoft.AspNet.SignalR.SelfHost 
PM> Install-Package TopShelf 
PM> Install-Package TopShelf.NLog
PM> Install-Package Microsoft.Owin.Cors 

The latter is required for cross-domain support, for the case where applications host SignalR and a web page in different domains--in this example, the SignalR server and client will be on different ports.

Ensure that your Program.cs has the following code, which allows you to debug the service from within Visual Studio or run it like a normal service when installed:

C#
using System;
using System.Collections.Generic;
using System.Data;
using Topshelf;

namespace SelfHostedServiceSignalRSample
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        static void Main()
        {
            HostFactory.Run(serviceConfig =>
            {
                serviceConfig.Service<SignalRServiceChat>(serviceInstance =>
                {
                    serviceConfig.UseNLog();

                    serviceInstance.ConstructUsing(
                        () => new SignalRServiceChat());

                    serviceInstance.WhenStarted(
                        execute => execute.OnStart(null));

                    serviceInstance.WhenStopped(
                        execute => execute.OnStop());
                });

                TimeSpan delay = new TimeSpan(0, 0, 0, 60);
                serviceConfig.EnableServiceRecovery(recoveryOption =>
                {
                    recoveryOption.RestartService(delay);
                    recoveryOption.RestartService(delay);
                    recoveryOption.RestartComputer(delay, 
                       System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + 
                       " computer reboot"); // All subsequent failures
                });

                serviceConfig.SetServiceName
                  (System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
                serviceConfig.SetDisplayName
                  (System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
                serviceConfig.SetDescription
                  (System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + 
                   " is a simple web chat application.");

                serviceConfig.StartAutomatically();
            });
        }
    }
}

In your OnStart method, add the following code:

C#
string url = "http://localhost:8090"; WebApp.Start(url); 

Also add these two classes (this code is modified from the article, Tutorial: Getting Started with SignalR 2):

C#
using Microsoft.Owin.Cors;
using Owin;

namespace SelfHostedServiceSignalRSample
{
    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseCors(CorsOptions.AllowAll);
            app.MapSignalR();
        }
    }
}
    
using Microsoft.AspNet.SignalR;

namespace SelfHostedServiceSignalRSample
{
    public class MyHub : Hub
    {
        public void Send(string name, string message)
        {
            Clients.All.addMessage(name, message);
        }
    }  
}      

Where the Startup class contains the configuration for the SignalR server and the call to map SignalR, which creates routes for any Hub objects in the project.

Here is the C# source code for the actual service itself:

C#
using System;
using Microsoft.Owin;
using Microsoft.Owin.Hosting;
using Topshelf.Logging;

[assembly: OwinStartup(typeof(SelfHostedServiceSignalRSample.Startup))]
namespace SelfHostedServiceSignalRSample
{
    public partial class SignalRServiceChat : IDisposable
    {
        public static readonly LogWriter Log = HostLogger.Get<SignalRServiceChat>();

        public SignalRServiceChat()
        {
        }

        public void OnStart(string[] args)
        {
            Log.InfoFormat("SignalRServiceChat: In OnStart");

            // This will *ONLY* bind to localhost, if you want to bind to all addresses
            // use http://*:8080 to bind to all addresses. 
            // See http://msdn.microsoft.com/en-us/library/system.net.httplistener.aspx 
            // for more information.
            string url = "http://localhost:8090";
            WebApp.Start(url);
        }

        public void OnStop()
        {
            Log.InfoFormat("SignalRServiceChat: In OnStop");
        }

        public void Dispose()
        {
        }
    }
}

Creating the JavaScript Client

Here, the client may not be at the same address as the connection URL, so it needs to be indicated explicitly. Create a new ASPNET Web Application, and select the Empty template.

881511/AspProject.png

Then, add the following using the package manager console, ensuring that the Default Project is set to Client.

PM> Install-Package Microsoft.AspNet.SignalR.JS 

Now add an HTML page with this code (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):

HTML
<!DOCTYPE html>
<html>
<head>
    <title>SignalR Simple Chat</title>
    <style type="text/css">
        .container {
            background-color: #99CCFF;
            border: thick solid #808080;
            padding: 20px;
            margin: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <input type="text" id="message" />
        <input type="button" id="sendmessage" value="Send" />
        <input type="hidden" id="displayname" />
        <ul id="discussion"></ul>
    </div>
    <!--Script references. -->
    <!--Reference the jQuery library. -->
    <script src="Scripts/jquery-1.6.4.min.js"></script>
    <!--Reference the SignalR library. -->
    <script src="Scripts/jquery.signalR-2.1.0.min.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="http://localhost:8080/signalr/hubs"></script>
    <!--Add script to update the page and send messages.-->
    <script type="text/javascript">
        $(function () {
        //Set the hubs URL for the connection
            $.connection.hub.url = "http://localhost:8080/signalr";
            
            // Declare a proxy to reference the hub.
            var chat = $.connection.myHub;
            
            // Create a function that the hub can call to broadcast messages.
            chat.client.addMessage = function (name, message) {
                // Html encode display name and message.
                var encodedName = $('<div />').text(name).html();
                var encodedMsg = $('<div />').text(message).html();
                // Add the message to the page.
                $('#discussion').append('<li><strong>' + encodedName
                    + '</strong>:  ' + encodedMsg + '</li>');
            };
            // Get the user name and store it to prepend to messages.
            $('#displayname').val(prompt('Enter your name:', ''));
            // Set initial focus to message input box.
            $('#message').focus();
            // Start the connection.
            $.connection.hub.start().done(function () {
                $('#sendmessage').click(function () {
                    // Call the Send method on the hub.
                    chat.server.send($('#displayname').val(), $('#message').val());
                    // Clear text box and reset focus for next comment.
                    $('#message').val('').focus();
                });
            });
        });
    </script>
</body>
</html>

If you choose to create a Windows Service instead of a Console Application with Topshelf, then this is how you would install the Windows Service:

Microsoft Windows [Version 6.3.9600] (c) 2013 Microsoft Corporation. All rights reserved.

C:\> installutil SelfHostedServiceSignalRSample.exe

Please note that if you choose to debug the Windows service instead of running it from the Services window, it is better to first start up just the service project and make sure it is running, and then start the Client project in another instance of Visual Studio.

The following call is what actually asynchronously starts the SignalR server in your Windows Service:

C#
WebApp.Start(url); 

Here is what the chat application should look like when running:

Chat Client

Server Broadcast Functionality

The above code uses peer-to-peer communication functionality, where communications sent to clients are initiated by one or more of the clients. If you want to have communications pushed to clients that are initiated by the server, then you need to add Server Broadcast functionality.

For this part of the article, I will build on the first peer-to-peer demo application, so to make it more clear, please take a look at the second demo application called SignalRBroadcastSample.

First, create an empty ASP.NET Website project.

Add the following Stock.cs file and two JavaScript files to the SignalRBroadcastSample project (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Client
{
    public class Stock
    {
        private decimal _price;

        public string Symbol { get; set; }

        public decimal Price
        {
            get
            {
                return _price;
            }
            set
            {
                if (_price == value)
                {
                    return;
                }

                _price = value;

                if (DayOpen == 0)
                {
                    DayOpen = _price;
                }
            }
        }

        public decimal DayOpen { get; private set; }

        public decimal Change
        {
            get
            {
                return Price - DayOpen;
            }
        }

        public double PercentChange
        {
            get
            {
                return (double)Math.Round(Change / Price, 4);
            }
        }
    }
}      

Add SignalR.StockTicker.js (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):

JavaScript
/// <reference path="../Scripts/jquery-1.10.2.js">
/// <reference path="../Scripts/jquery.signalR-2.1.1.js">

/*!
    ASP.NET SignalR Stock Ticker Sample
*/

// Crockford's supplant method (poor man's templating)
if (!String.prototype.supplant) {
    String.prototype.supplant = function (o) {
        return this.replace(/{([^{}]*)}/g,
            function (a, b) {
                var r = o[b];
                return typeof r === 'string' || typeof r === 'number' ? r : a;
            }
        );
    };
}

// A simple background color flash effect that uses jQuery Color plugin
jQuery.fn.flash = function (color, duration) {
    var current = this.css('backgroundColor');
    this.animate({ backgroundColor: 'rgb(' + color + ')' }, duration / 2)
        .animate({ backgroundColor: current }, duration / 2);
};

$(function () {

    var ticker = $.connection.stockTicker, // the generated client-side hub proxy
        up = '?',
        down = '?',
        $stockTable = $('#stockTable'),
        $stockTableBody = $stockTable.find('tbody'),
        rowTemplate = '{Symbol}{Price}{DayOpen}{DayHigh}{DayLow}{Direction} 
                       {Change}{PercentChange}',
        $stockTicker = $('#stockTicker'),
        $stockTickerUl = $stockTicker.find('ul'),
        liTemplate = '<li data-symbol="{Symbol}">{Symbol} {Price} 
                      {Direction} {Change} ({PercentChange})</li>';

    function formatStock(stock) {
        return $.extend(stock, {
            Price: stock.Price.toFixed(2),
            PercentChange: (stock.PercentChange * 100).toFixed(2) + '%',
            Direction: stock.Change === 0 ? '' : stock.Change >= 0 ? up : down,
            DirectionClass: stock.Change === 0 ? 'even' : stock.Change >= 0 ? 'up' : 'down'
        });
    }

    function scrollTicker() {
        var w = $stockTickerUl.width();
        $stockTickerUl.css({ marginLeft: w });
        $stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
    }

    function stopTicker() {
        $stockTickerUl.stop();
    }

    function init() {
        return ticker.server.getAllStocks().done(function (stocks) {
            $stockTableBody.empty();
            $stockTickerUl.empty();
            $.each(stocks, function () {
                var stock = formatStock(this);
                $stockTableBody.append(rowTemplate.supplant(stock));
                $stockTickerUl.append(liTemplate.supplant(stock));
            });
        });
    }

    // Add client-side hub methods that the server will call
    $.extend(ticker.client, {
        updateStockPrice: function (stock) {
            var displayStock = formatStock(stock),
                $row = $(rowTemplate.supplant(displayStock)),
                $li = $(liTemplate.supplant(displayStock)),
                bg = stock.LastChange < 0
                        ? '255,148,148' // red
                        : '154,240,117'; // green

            $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
                .replaceWith($row);
            $stockTickerUl.find('li[data-symbol=' + stock.Symbol + ']')
                .replaceWith($li);

            $row.flash(bg, 1000);
            $li.flash(bg, 1000);
        },

        marketOpened: function () {
            $("#open").prop("disabled", true);
            $("#close").prop("disabled", false);
            $("#reset").prop("disabled", true);
            scrollTicker();
        },

        marketClosed: function () {
            $("#open").prop("disabled", false);
            $("#close").prop("disabled", true);
            $("#reset").prop("disabled", false);
            stopTicker();
        },

        marketReset: function () {
            return init();
        }
    });

    // Start the connection
    $.connection.hub.start()
        .then(init)
        .then(function () {
            return ticker.server.getMarketState();
        })
        .done(function (state) {
            if (state === 'Open') {
                ticker.client.marketOpened();
            } else {
                ticker.client.marketClosed();
            }

            // Wire up the buttons
            $("#open").click(function () {
                ticker.server.openMarket();
            });

            $("#close").click(function () {
                ticker.server.closeMarket();
            });

            $("#reset").click(function () {
                ticker.server.reset();
            });
        });
});    

In the above code, the $.connection refers to SignalR proxies. It gets a reference to the proxy for the StockTickerHub class and puts it in the ticker variable, where the proxy name is what is found in the [HubName{"stockTickerMini")] attribute (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):

HTML
var ticker = $.connection.stockTickerMini

Add StockTicker.css:

CSS
  body {
    font-family: 'Segoe UI', Arial, Helvetica, sans-serif;
    font-size: 16px;
}

#stockTable table {
    border-collapse: collapse;
}

    #stockTable table th, #stockTable table td {
        padding: 2px 6px;
    }

    #stockTable table td {
        text-align: right;
    }

#stockTable .loading td {
    text-align: left;
}

#stockTicker {
    overflow: hidden;
    width: 450px;
    height: 24px;
    border: 1px solid #999;
}

    #stockTicker .inner {
        width: 9999px;
    }

    #stockTicker ul {
        display: inline-block;
        list-style-type: none;
        margin: 0;
        padding: 0;
    }

    #stockTicker li {
        display: inline-block;
        margin-right: 8px;   
    }
  • {Symbol}{Price}{PercentChange}
  • #stockTicker .symbol { font-weight: bold; } #stockTicker .change { font-style: italic; }

Add StockTicker.html (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):

HTML
    <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>ASP.NET SignalR Stock Ticker</title>
    <link href="StockTicker.css" rel="stylesheet" />
</head>
<body>
    <h1>ASP.NET SignalR Stock Ticker Sample</h1>

    <input type="button" id="open" value="Open Market" />
    <input type="button" id="close" value="Close Market" disabled="disabled" />
    <input type="button" id="reset" value="Reset" />

    <h2>Live Stock Table</h2>
    <div id="stockTable">
        <table border="1">
            <thead>
                <tr><th>Symbol</th><th>Price</th><th>Open</th>
                <th>High</th><th>Low</th><th>Change</th><th>%</th></tr>
            </thead>
            <tbody>
                <tr class="loading"><td colspan="7">loading...</td></tr>
            </tbody>
        </table>
    </div>

    <h2>Live Stock Ticker</h2>
    <div id="stockTicker">
        <div class="inner">
            <ul>
                <li class="loading">loading...</li>
            </ul> 
        </div>
    </div>

    <script src="jquery-1.10.2.min.js"></script>
    <script src="jquery.color-2.1.2.min.js"></script>
    <script src="../Scripts/jquery.signalR-2.2.0.js"></script>
    <script src="../signalr/hubs"></script>
    <script src="SignalR.StockTicker.js"></script>
</body>
</html>    

For each stock, you need to add the symbol (e.g., MSFT for Microsoft) and the Price.

Create the StockTicker and StockTickerHub Classes

Add StockTicker.cs, which keeps stock data, updates prices, broadcasts the price updates, and runs a timer to periodically trigger updates independently of client connections (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):

C#
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;

namespace SelfHostedServiceSignalRSample
{
    public class StockTicker
    {
        // Singleton instance
        private readonly static Lazy<stockticker> _instance = new Lazy<stockticker>(
            () => new StockTicker
             (GlobalHost.ConnectionManager.GetHubContext<stocktickerhub>().Clients));

        private readonly object _marketStateLock = new object();
        private readonly object _updateStockPricesLock = new object();

        private readonly ConcurrentDictionary<string, 
        stock=""> _stocks = new ConcurrentDictionary<string, stock="">();

        // Stock can go up or down by a percentage of this factor on each change
        private readonly double _rangePercent = 0.002;
        
        private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
        private readonly Random _updateOrNotRandom = new Random();

        private Timer _timer;
        private volatile bool _updatingStockPrices;
        private volatile MarketState _marketState;

        private StockTicker(IHubConnectionContext<dynamic> clients)
        {
            Clients = clients;
            LoadDefaultStocks();
        }

        public static StockTicker Instance
        {
            get
            {
                return _instance.Value;
            }
        }

        private IHubConnectionContext<dynamic> Clients
        {
            get;
            set;
        }

        public MarketState MarketState
        {
            get { return _marketState; }
            private set { _marketState = value; }
        }

        public IEnumerable<stock> GetAllStocks()
        {
            return _stocks.Values;
        }

        public void OpenMarket()
        {
            lock (_marketStateLock)
            {
                if (MarketState != MarketState.Open)
                {
                    _timer = new Timer(UpdateStockPrices, null, 
                                       _updateInterval, _updateInterval);

                    MarketState = MarketState.Open;

                    BroadcastMarketStateChange(MarketState.Open);
                }
            }
        }

        public void CloseMarket()
        {
            lock (_marketStateLock)
            {
                if (MarketState == MarketState.Open)
                {
                    if (_timer != null)
                    {
                        _timer.Dispose();
                    }

                    MarketState = MarketState.Closed;

                    BroadcastMarketStateChange(MarketState.Closed);
                }
            }
        }

        public void Reset()
        {
            lock (_marketStateLock)
            {
                if (MarketState != MarketState.Closed)
                {
                    throw new InvalidOperationException
                            ("Market must be closed before it can be reset.");
                }
                
                LoadDefaultStocks();
                BroadcastMarketReset();
            }
        }

        private void LoadDefaultStocks()
        {
            _stocks.Clear();

            var stocks = new List<stock>
            {
                new Stock { Symbol = "MSFT", Price = 41.68m },
                new Stock { Symbol = "AAPL", Price = 92.08m },
                new Stock { Symbol = "GOOG", Price = 543.01m }
            };

            stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
        }

        private void UpdateStockPrices(object state)
        {
            // This function must be re-entrant as it's running as a timer interval handler
            lock (_updateStockPricesLock)
            {
                if (!_updatingStockPrices)
                {
                    _updatingStockPrices = true;

                    foreach (var stock in _stocks.Values)
                    {
                        if (TryUpdateStockPrice(stock))
                        {
                            BroadcastStockPrice(stock);
                        }
                    }

                    _updatingStockPrices = false;
                }
            }
        }

        private bool TryUpdateStockPrice(Stock stock)
        {
            // Randomly choose whether to update this stock or not
            var r = _updateOrNotRandom.NextDouble();
            if (r > 0.1)
            {
                return false;
            }

            // Update the stock price by a random factor of the range percent
            var random = new Random((int)Math.Floor(stock.Price));
            var percentChange = random.NextDouble() * _rangePercent;
            var pos = random.NextDouble() > 0.51;
            var change = Math.Round(stock.Price * (decimal)percentChange, 2);
            change = pos ? change : -change;

            stock.Price += change;
            return true;
        }

        private void BroadcastMarketStateChange(MarketState marketState)
        {
            switch (marketState)
            {
                case MarketState.Open:
                    Clients.All.marketOpened();
                    break;
                case MarketState.Closed:
                    Clients.All.marketClosed();
                    break;
                default:
                    break;
            }
        }

        private void BroadcastMarketReset()
        {
            Clients.All.marketReset();
        }

        private void BroadcastStockPrice(Stock stock)
        {
            Clients.All.updateStockPrice(stock);
        }
    }

    public enum MarketState
    {
        Closed,
        Open
    }
}

The StockTicker.cs class must be threadsafe which is accomplished by the Lazy initialization.

Add StockTickerHub.cs, which derives from the SignalR Hub class and will handle receiving connections and method calls from clients (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):

C#
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SelfHostedServiceSignalRSample
{
    [HubName("stockTicker")]
    public class StockTickerHub : Hub
    {
        private readonly StockTicker _stockTicker;

        public StockTickerHub() :
            this(StockTicker.Instance)
        {

        }

        public StockTickerHub(StockTicker stockTicker)
        {
            _stockTicker = stockTicker;
        }

        public IEnumerable<stock> GetAllStocks()
        {
            return _stockTicker.GetAllStocks();
        }

        public string GetMarketState()
        {
            return _stockTicker.MarketState.ToString();
        }

        public void OpenMarket()
        {
            _stockTicker.OpenMarket();
        }

        public void CloseMarket()
        {
            _stockTicker.CloseMarket();
        }

        public void Reset()
        {
            _stockTicker.Reset();
        }
    }
}

The Hub class above is used to define methods on the server that clients can call.

If any of the methods require waiting, then you could specify, for example, Task<IEnumerable<Stock>> as the return value to enable async processing. For details, please see here.

The HubName attribute indicates how the Hub will be referenced in the JavaScript code on the client.

Each time a client connects to the server, a new instance of the StockTickerHub class running on a separate thread gets the StockTicker singleton.

Also, update your jQuery package:

PM> Install-Package jQuery -Version 1.10.2 

Finally, add a Startup class, which tells the server which URL to intercept and direct to SignalR (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):

C#
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(Microsoft.AspNet.SignalR.StockTicker.Startup))]
namespace Microsoft.AspNet.SignalR.StockTicker
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // For more information on how to configure your application using OWIN startup, 
            // visit http://go.microsoft.com/fwlink/?LinkID=316888
            app.MapSignalR();
        }
    }
}      

Get SignalR Context so StockTicker Class Can Broadcast to Clients

This is the key code so that the StockTicker class can broadcast to all clients (this code is taken directly from the article Tutorial: Getting Started with SignalR 2):

C#
private readonly static Lazy<stockticker> _instance =
    new Lazy<stockticker>(() => 
    new StockTicker(GlobalHost.ConnectionManager.GetHubContext<stocktickerhub>().Clients));

private StockTicker(IHubConnectionContext<dynamic> clients)
{
    Clients = clients;

    // Remainder of ctor ...
}

private IHubConnectionContext<dynamic> Clients
{
    get;
    set;
}

private void BroadcastStockPrice(Stock stock)
{
    Clients.All.updateStockPrice(stock);
} 

Since the price changes originate in the StockTicker object, this object needs to call an updateStockPrice method on all connected clients. In the Hub class, there is an API for calling client methods, but StockTicker does not derive from the Hub class, and does not have any reference to a Hub object. That is why the StockTicker class has to get the SignalR context instance for the StockTickerHub class, so that it can call methods on the clients.

In the above code, the StockTicker class gets a reference to the SignalR context when it creates the singleton class, and then passes that reference to its constructor, which stores that in the Clients property.

Notice also that the call to updateStockPrice in the code above calls the function of that name in the SignalR.StockTicker.js JavaScript file.

Clients.All means to send to all clients. To learn how to specify which clients or groups of clients, see here.

Next, press F5 to test the application.

Conclusion

In this article, I discussed creating a Windows Service that demonstrated peer-to-peer communication using SignalR, and the ability in SignalR to also provide broadcasting from the server to all clients on a separate demo project. In my next article, I plan on demonstrating how to put that broadcasting SignalR functionality into the Windows Service application.

Acknowledgements

Please note that I got most of the ideas for this article from Patrick Fletcher's article here and Tom Dykstra and Tom FitzMacken's article here and from a pluralsight course on Topshelf.

History

  • 2019-06-04: Updated application to use Topshelf
  • 2015-03-02: Added details on using SignalR for broadcasting from the server to all clients
  • 2019-07-10: Added details on how to use Topshelf which provides a simpler way to get started in winservice development and also allows for debugging as a console app

References

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0