Yes, you can do this by creating a self-hosted SignalR Hub instead of having SignalR be hosted like it normally is through IIS. With the self-hosted solution both the Hub and the client can entirely be in C#.
In this explanation my Hub will be a Windows Service but you could also use a WPF desktop, Winforms desktop, or a Console application.
First in Visual Studio create a Windows Service, ensuring you are using the .NET Framework 4.5 or greater:

Then type this in the package manager console:
PM> Install-Package Microsoft.AspNet.SignalR.SelfHost
PM> Install-Package ServiceProcess.Helpers
PM> Install-Package Microsoft.Owin.Cors
Notice that the latter Microsoft.Owin.Cors
is required for cross-domain support. Next add this to your app.config file:
<runtime>
<loadFromRemoteSources enabled="true" />
</runtime>
Then add this to your Program.cs
file:
using ServiceProcess.Helpers;
using System;
using System.Collections.Generic;
using System.Data;
using System.ServiceProcess;
namespace SignalRBroadcastServiceSample
{
static class Program
{
private static readonly List<ServiceBase> _servicesToRun = new List<ServiceBase>();
/// <summary>
/// The main entry point for the application.
/// </summary>
static void Main()
{
_servicesToRun.Add(CurrencyExchangeService.Instance);
if (Environment.UserInteractive)
{
_servicesToRun.ToArray().LoadServices();
}
else
{
ServiceBase.Run(_servicesToRun.ToArray());
}
}
}
}
Next add a class library which will be the domain. Now add a Startup
class containing this to your service project:
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(SignalRBroadcastServiceSample.Startup))]
namespace SignalRBroadcastServiceSample
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
// Branch the pipeline here for requests that start with "/signalr"
app.Map("/signalr", map =>
{
// Setup the CORS middleware to run before SignalR.
// By default this will allow all origins. You can
// configure the set of origins and/or http verbs by
// providing a cors options with a different policy.
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
// You can enable JSONP by uncommenting line below.
// JSONP requests are insecure but some older browsers (and some
// versions of IE) require JSONP to work cross domain
// EnableJSONP = true
EnableDetailedErrors = true,
EnableJSONP = true
};
// Run the SignalR pipeline. We're not using MapSignalR
// since this branch already runs under the "/signalr"
// path.
map.RunSignalR(hubConfiguration);
});
}
}
}
Now add this to your domain project:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SignalrDomain
{
public class Currency
{
private decimal _usdValue;
public string CurrencySign { get; set; }
public decimal Open { get; private set; }
public decimal Low { get; private set; }
public decimal High { get; private set; }
public decimal LastChange { get; private set; }
public decimal RateChange
{
get
{
return USDValue - Open;
}
}
public double PercentChange
{
get
{
return (double)Math.Round(RateChange / USDValue, 4);
}
}
public decimal USDValue
{
get
{
return _usdValue;
}
set
{
if (_usdValue == value)
{
return;
}
LastChange = value - _usdValue;
_usdValue = value;
if (Open == 0)
{
Open = _usdValue;
}
if (_usdValue < Low || Low == 0)
{
Low = _usdValue;
}
if (_usdValue > High)
{
High = _usdValue;
}
}
}
}
}
Finally create your SignalR hub here where the WebApp.Start
call is the key part of the SignalR self-hosting:
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Microsoft.Owin;
using SignalrDomain;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SignalRBroadcastServiceSample
{
public class CurrencyExchangeHub : Hub
{
private readonly CurrencyExchangeService _currencyExchangeHub;
public CurrencyExchangeHub() :
this(CurrencyExchangeService.Instance)
{
}
public CurrencyExchangeHub(CurrencyExchangeService currencyExchange)
{
_currencyExchangeHub = currencyExchange;
}
public IEnumerable<Currency> GetAllCurrencies()
{
return _currencyExchangeHub.GetAllCurrencies();
}
public string GetMarketState()
{
return _currencyExchangeHub.MarketState.ToString();
}
public bool OpenMarket()
{
_currencyExchangeHub.OpenMarket();
return true;
}
public bool CloseMarket()
{
_currencyExchangeHub.CloseMarket();
return true;
}
public bool Reset()
{
_currencyExchangeHub.Reset();
return true;
}
}
}
Also add this to your CurrencyExchangeService.cs
file:
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Microsoft.Owin.Hosting;
using SignalrDomain;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ServiceProcess;
using System.Threading;
namespace SignalRBroadcastServiceSample
{
public partial class CurrencyExchangeService : ServiceBase
{
private Thread mainThread;
private bool isRunning = true;
private Random random = new Random();
protected override void OnStart(string[] args)
{
WebApp.Start("http://localhost:8083"); // Must be
//@"http://+:8083" if you want to connect from other computers
LoadDefaultCurrencies();
// Start main thread
mainThread = new Thread(new ParameterizedThreadStart(this.RunService));
mainThread.Start(DateTime.MaxValue);
}
protected override void OnStop()
{
mainThread.Join();
}
public void RunService(object timeToComplete)
{
DateTime dtTimeToComplete = timeToComplete != null ?
Convert.ToDateTime(timeToComplete) : DateTime.MaxValue;
while (isRunning && DateTime.UtcNow < dtTimeToComplete)
{
Thread.Sleep(15000);
NotifyAllClients();
}
}
// This line is necessary to perform the broadcasting to all clients
private void NotifyAllClients()
{
Currency currency = new Currency();
currency.CurrencySign = "CAD";
currency.USDValue = random.Next();
BroadcastCurrencyRate(currency);
Clients.All.NotifyChange(currency);
}
#region "SignalR code"
// Singleton instance
private readonly static Lazy<CurrencyExchangeService>
_instance = new Lazy<CurrencyExchangeService>(
() => new CurrencyExchangeService
(GlobalHost.ConnectionManager.GetHubContext<CurrencyExchangeHub>().Clients));
private readonly object _marketStateLock = new object();
private readonly object _updateCurrencyRatesLock = new object();
private readonly ConcurrentDictionary<string,
Currency> _currencies = new ConcurrentDictionary<string, Currency>();
// Currency 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);
public TimeSpan UpdateInterval
{
get { return _updateInterval; }
}
private readonly Random _updateOrNotRandom = new Random();
private Timer _timer;
private volatile bool _updatingCurrencyRates;
private volatile MarketState _marketState;
public CurrencyExchangeService(IHubConnectionContext<dynamic> clients)
{
InitializeComponent();
Clients = clients;
}
public static CurrencyExchangeService Instance
{
get
{
return _instance.Value;
}
}
private IHubConnectionContext<dynamic> Clients
{
get;
set;
}
public MarketState MarketState
{
get { return _marketState; }
private set { _marketState = value; }
}
public IEnumerable<Currency> GetAllCurrencies()
{
return _currencies.Values;
}
public bool OpenMarket()
{
bool returnCode = false;
lock (_marketStateLock)
{
if (MarketState != MarketState.Open)
{
_timer = new Timer(UpdateCurrencyRates, null, _updateInterval, _updateInterval);
MarketState = MarketState.Open;
BroadcastMarketStateChange(MarketState.Open);
}
}
returnCode = true;
return returnCode;
}
public bool CloseMarket()
{
bool returnCode = false;
lock (_marketStateLock)
{
if (MarketState == MarketState.Open)
{
if (_timer != null)
{
_timer.Dispose();
}
MarketState = MarketState.Closed;
BroadcastMarketStateChange(MarketState.Closed);
}
}
returnCode = true;
return returnCode;
}
public bool Reset()
{
bool returnCode = false;
lock (_marketStateLock)
{
if (MarketState != MarketState.Closed)
{
throw new InvalidOperationException
("Market must be closed before it can be reset.");
}
LoadDefaultCurrencies();
BroadcastMarketReset();
}
returnCode = true;
return returnCode;
}
private void LoadDefaultCurrencies()
{
_currencies.Clear();
var currencies = new List<Currency>
{
new Currency { CurrencySign = "USD", USDValue = 1.00m },
new Currency { CurrencySign = "CAD", USDValue = 0.85m },
new Currency { CurrencySign = "EUR", USDValue = 1.25m }
};
currencies.ForEach(currency => _currencies.TryAdd(currency.CurrencySign, currency));
}
private void UpdateCurrencyRates(object state)
{
// This function must be re-entrant as it's running as a timer interval handler
lock (_updateCurrencyRatesLock)
{
if (!_updatingCurrencyRates)
{
_updatingCurrencyRates = true;
foreach (var currency in _currencies.Values)
{
if (TryUpdateCurrencyRate(currency))
{
BroadcastCurrencyRate(currency);
}
}
_updatingCurrencyRates = false;
}
}
}
private bool TryUpdateCurrencyRate(Currency currency)
{
// Randomly choose whether to update this currency or not
var r = _updateOrNotRandom.NextDouble();
if (r > 0.1)
{
return false;
}
// Update the currency price by a random factor of the range percent
var random = new Random((int)Math.Floor(currency.USDValue));
var percentChange = random.NextDouble() * _rangePercent;
var pos = random.NextDouble() > 0.51;
var change = Math.Round(currency.USDValue * (decimal)percentChange, 2);
change = pos ? change : -change;
currency.USDValue += 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 BroadcastCurrencyRate(Currency currency)
{
Clients.All.updateCurrencyRate(currency);
}
}
public enum MarketState
{
Closed,
Open
}
#endregion
}
Next in your Client Console application, add this NuGet package:
PM> Install-Package Microsoft.AspNet.SignalR.Client
Also, add a CommmunicationHandler
class:
using Microsoft.AspNet.SignalR.Client;
using SignalrDomain;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Client
{
public static class CommunicationHandler
{
public static string ExecuteMethod(string method, string args, string serverUri, string hubName)
{
var hubConnection = new HubConnection("http://localhost:8083");
IHubProxy currencyExchangeHubProxy = hubConnection.CreateHubProxy("CurrencyExchangeHub");
// This line is necessary to subscribe for broadcasting messages
currencyExchangeHubProxy.On<Currency>("NotifyChange", HandleNotify);
// Start the connection
hubConnection.Start().Wait();
var result = currencyExchangeHubProxy.Invoke<string>(method).Result;
return result;
}
private static void HandleNotify(Currency currency)
{
Console.WriteLine("Currency " + currency.CurrencySign + ", Rate = " + currency.USDValue);
}
}
}
Here is the Program class in the client:
using System;
using System.Diagnostics;
using System.Net;
namespace Client
{
class Program
{
static void Main(string[] args)
{
var state = CommunicationHandler.ExecuteMethod("GetMarketState",
"", IPAddress.Any.ToString(), "CurrencyExchangeHub");
Console.WriteLine("Market State is " + state);
if (state == "Closed")
{
var returnCode = CommunicationHandler.ExecuteMethod
("OpenMarket", "", IPAddress.Any.ToString(), "CurrencyExchangeHub");
Debug.Assert(returnCode == "True");
Console.WriteLine("Market State is Open");
}
Console.ReadLine();
}
}
}
Now run the service and start it, then when you run the Console client application you should see currency rates being updated periodically.
The original article is here: https://www.codeproject.com/Articles/892634/Using-SignalR-Broadcasting-and-Notifications-with
You can download the full source code here: http://www.sandbox.ca/~rmoore/export/codeproject/CodeProjectSelfHostedBroadcastServiceSignalRSample.zip
Also, instead of using the old way of hosting and installing a Windows service as shown above you could update the application using the helpful Topshelf NuGet package: https://github.com/Topshelf/Topshelf