Let's build a dashboard powered by .NET and JavaScript, specifically we will use F# and vanilla JavaScript with little bit of jQuery.
A while back I wrote an article that was quite popular on building a similar dashboard using Angular and C#, partly because people can't get enough dashboards and because I love this sort of thing, that article can be found on SitePoint.
This time the technology powering our dashboard will be similar, in that we are still using .NET and JavaScript and the web sockets library SignalR. The similarities will end there though, as this new dashboard will be significantly better. We will build it using a simple stateless service that is functional first. This service will power our dashboard on the server and use the Web Sockets library, SignalR, to move data in realtime to the client side.
If you would like to find out a bit more about SignalR, my colleague and friend Troy Kershaw, wrote up beautiful post that can be found here.
The source code for the dashboard we will be building can be found on GitHub.
The final product will look something like this
Some of the key benefits that will make this dashboard better in F# rather than writing it in C# and Angular are:
- It will be extremely easy to reason about this code base.
- It is almost completely stateless except for the bootstrapping needed to get Owin and SinglaR up and running.
- Perhaps the most important reason of all to use F# over any OOP language Is the amount of code required to get the same exact thing up and running will be significantly less.
- You should be able to get this up and running using completely open source and free software (Visual Studio Community edition, F#, and .NET core. You may even be able to run it on Linux, although my tutorial will be is easier to follow on Windows).
I will leave it up to you to tell me in the comments if it's better.
To be clear I am not trying to rail against one of my favorite languages, one that is near and dear to me C#, but I'm trying to prove a point that F# is really on another level of abstraction and works fantastically for general purpose programming, exactly like this sort of thing.
Let's get to it, shall we?
First thing is first, let's get an F# solution going for our server side code (Of course you can run all of this on your own Windows machine. I do know my good friend John Montaya likes to get these this sorts of things running on Linux using Mono but I haven’t tried that approach but you can, you will just have to change the code slightly to pull Linux metrics. As an IDE I will be using Visual Studio, simply because its easy enough and there is now a decent free version with Community Edition. Some of my former colleagues used to say its the Cadillac of IDEs despite what you may have heard. (I'm not really sure if anyone drives Cadillacs anymore honestly.)
Creating the projects
By Creating a Library we will have the option of referencing our code within some service or alternatively we can simply invoke it to start via it's FSX file and that’s the beauty of F#. We don’t even need a Console executable.
Let's add a few refrences for our SingalR to self host through Owin
Next up open up NuGet and add the following libraries either via command line or through the GUI.
Microsoft.AspNet.SignalR.Selfhost
Microsoft.Owin.Cors
Microsoft.AspNet.SignalR.Owin
Fsharp.Dynamic
The last library will help with adding .NET dynamic features to F#. SignalR uses this and basically the libraries overload the ? operator.
Now that our server side project is ready to go let us create a very simple bare bones ASP.NET client app to consume our SignalR F# Server. You can do this via Node as well if you prefer, basically anything that can serve up some client side JavaScript and static files will work as a client.
I will be doing it right here in Visual Studio since its nice and easy to do so, if you wish to do the same right click on the main solution file -> click add New Project
then select C# (Don't worry we wont be writing any C#!) we just need a bare bones web app with nothing in it.
Then select an empty ASP.NET project, basically something that can serve up our JavaScript and client code.
After creating the client project let's add a reference to the SignalR client using Nuget like we did before for the F# project, this will bring in the required jQuery and the rest of the JavaScript code to hook up to our F# SignalR Server. (note: you can just download the SignalR references from their site if you dont want to use asp.net as the client.)
Microsoft.AspNet.SignalR.JS
Add an index.html
file as well to the project by right click -> Add -> New Item -> select HTML page
I went ahead and renamed those default files in our F# project and it should all look something like this.
We have our project ready to go, lets write some damn code!
Next we will wire up our SignalR.
We first need to create a simple Startup.fs file that will bootstrap SignalR via Owin and start it up as a self hosted app without IIS. Normally code like this would be auto generated in most C# SignalR projects but F# we have to do this ourselves, a minor price to pay for the rest of the benefits we get.
module Startup
open Microsoft.AspNet.SignalR
open Microsoft.AspNet.SignalR.Hubs
open Microsoft.Owin.Hosting
open Microsoft.AspNet.SignalR.Owin
type public Startup() =
member public this.Configuration(app:Owin.IAppBuilder) =
let config = new HubConfiguration(EnableDetailedErrors = true)
Owin.MapExtensions.Map(app, "/signalr",
fun map ->
Owin.CorsExtensions.UseCors(map, Microsoft.Owin.Cors.CorsOptions.AllowAll) |> ignore
Owin.OwinExtensions.RunSignalR(map, config)) |> ignore
()
SignalR uses hubs so the next thing we need to create is a hub that we can use to call clients and also so that we can make calls and keep track of who is connecting, should we need to.
module MetricsHub
open System
open Microsoft.AspNet.SignalR
open Microsoft.AspNet.SignalR.Hubs
open Microsoft.Owin.Hosting
open Microsoft.AspNet.SignalR.Owin
open FSharp.Interop.Dynamic
[<HubName("metricsHub")>]
type MetricsHub() =
inherit Hub()
//if we were intrested in seeing who is connecting
//or doing something on a new connection this would be the place
override x.OnConnected() =
base.OnConnected()
// A function that can be invoked by any client since signalr uses web sockets for two way communication.
member public x.SendMessage(message : string) : unit =
base.Clients.All?addMessage (message)
The main entry of the application, our DashStart.fsx
can look like the following to spin up our SignalR server.
#I "bin"
#r "Debug\Newtonsoft.Json.dll"
#r "Debug\Microsoft.AspNet.SignalR.Core.dll"
#r "Debug\Microsoft.AspNet.SignalR.Owin.dll"
#r "Debug\Microsoft.Owin.Cors.dll"
#r "Debug\Microsoft.Owin.dll"
#r "Debug\Microsoft.Owin.Hosting.dll"
#r "Debug\Owin.dll"
#r "Debug\FSharp.Interop.Dynamic.dll"
#r "Debug\Dynamitey.dll"
#r "Debug\LouiesGuiAdventDash.dll"
open Owin
open System
open FSharp.Interop.Dynamic
open Microsoft.AspNet.SignalR
open Microsoft.AspNet.SignalR.Owin
open Microsoft.Owin.Hosting
open Microsoft.Owin.Cors
open Microsoft.AspNet.SignalR.Hubs
open Startup
open MetricsHub
open LouiesGuiAdventDash
let url = "http://localhost:8085/"
AdventDash.start url
0
Now we need to flesh out our AdventDash.fs
file which contains the meat and potatoes of our server. We can start with something like this:
namespace LouiesGuiAdventDash
module AdventDash =
open System
open System.Threading
open Owin
open FSharp.Interop.Dynamic
open Microsoft.AspNet.SignalR
open Microsoft.Owin.Hosting
open Microsoft.Owin.Cors
open Microsoft.AspNet.SignalR.Hubs
open Microsoft.AspNet.SignalR.Owin
open Startup
open MetricsHub
//keep it running
let private signal = new ManualResetEventSlim(false)
let wait() = signal.Wait()
let start (signalrEndpoint:string) =
try
use webApp = WebApp.Start<Startup>(signalrEndpoint)
let context : IHubContext = GlobalHost.ConnectionManager.GetHubContext<MetricsHub>()
for x in [ 0..1000 ] do
System.Threading.Thread.Sleep(1000)
printfn "Server Sending Value to Client %s: " (x.ToString())
context.Clients.All?addMessage (x.ToString())
printfn "running..."
printfn "listening on %s" signalrEndpoint
wait()
with
| ex ->
printfn "%A" ex
()
If we try running this via the FSX file either through visual studio or command line we will see the app start up and broadcast some dumb numbers, so we know at least we have wired everything correctly.
To push some real metrics about our server let's start pulling some Windows Specific performance counters, if you were on Linux you would have to pull those via Mono so this code would change. First let us model what that data will look like as an F# type, the serialization will automatically be handled for us. Let's add a performance model file PerfModel.fs
module PerfModel
open Newtonsoft.Json
type PerfModel = {
[<JsonProperty("machineName")>]
MachineName : string
[<JsonProperty("categoryName")>]
CategoryName : string
[<JsonProperty("counterName")>]
CounterName : string
[<JsonProperty("instanceName")>]
InstanceName : string
[<JsonProperty("value")>]
Value : double
}
Now let's re-write our AdventDash.fs
to have some REAL meat and potatoes, after all it is Christmas.
namespace LouiesGuiAdventDash
module AdventDash =
open System
open System.Threading
open Owin
open FSharp.Interop.Dynamic
open Microsoft.AspNet.SignalR
open Microsoft.Owin.Hosting
open Microsoft.Owin.Cors
open Microsoft.AspNet.SignalR.Hubs
open Microsoft.AspNet.SignalR.Owin
open Startup
open MetricsHub
open System.Diagnostics
open PerfModel
//the context
let interval = 1000
let context : IHubContext = GlobalHost.ConnectionManager.GetHubContext<MetricsHub>()
///get the processs instance name via provided process id
let getProcessInstanceName (pid: int) =
let cat = new PerformanceCounterCategory("Process");
let instances = cat.GetInstanceNames()
instances
|> Array.filter(fun i ->
use cnt = new PerformanceCounter("Process","ID Process", i, true)
let intval = (int)cnt.RawValue;
if pid = intval then
true
else false)
|> Seq.head
///get the current running process name
let getCurrentProcessInstanceName =
let proc = Process.GetCurrentProcess();
let pid = proc.Id;
getProcessInstanceName pid
///set a few service counters so we can track some basic metrics
let serviceCounters =
[
new PerformanceCounter("Processor Information", "% Processor Time", "_Total")
new PerformanceCounter("Memory", "Available MBytes")
new PerformanceCounter("Process", "% Processor Time", getCurrentProcessInstanceName, true)
new PerformanceCounter("Process", "Working Set", getCurrentProcessInstanceName, true)
]
///grab performance metric values and map them to our over the wire F# perfModel
let metricsWorkflow (context : IHubContext) = async {
let mappedCounters = serviceCounters
|> Seq.map(fun x ->
{
PerfModel.PerfModel.MachineName = x.MachineName
CategoryName = x.CategoryName
CounterName = x.CounterName
InstanceName = x.InstanceName
Value = (double)x.RawValue
}
)
//broadcast all of the metrics
mappedCounters
|> Seq.iter(fun perfModel ->
printfn "CategoryName: %s CounterName:%s Value:%A" perfModel.CategoryName perfModel.CounterName perfModel.Value
)
context.Clients.All?broadcastPerformance(mappedCounters);
}
///lets recurse infitly to broadcast our metrics
//we can do a while loop or any other mechanism we like
let rec iBroadcast() = async {
do! metricsWorkflow context
do! Async.Sleep 1000
return! iBroadcast()
}
let start (signalrEndpoint:string) =
printfn "starting..."
try
use webApp = WebApp.Start<Startup>(signalrEndpoint)
printfn "running..."
printfn "listening on %s" signalrEndpoint
iBroadcast() |> Async.RunSynchronously
with
| ex ->
printfn "%A" ex
()
We can now run the server side code and see metrics being fired at a really nice steady rate.
A quick side note.
We can skip over this section this is just a few things about F# in general that I think are useful to know, for people who are new to this.
So this is not intended to be a plug about F#, but F# has had Async computations long before C# got them. That set of code that now exists in C#, you know the Async-Await was in F# first and it's always been simple to reason about and is still one of the best things about F#.
let! fSharpIsAwesome = async {
//do asynchronous computations
//offload IO/network bound work here
//don't forget everything is immutable and that's what makes this so beautiful!
}
Although this dash dosent make much use of real asynchronous computations its really easy to imagine some of this data coming from a database, or some other backend store. At that point and time these little async units of work would be a life saver.
One of our primary reasons to go with F# is the massive parallelism and concurrency we can get out of the language without having to worry about locks or those nasty race conditions that used to keep us up at night when we used to write Java and C#.
Look it isn't a coincidence that every company doing things at internet scale is moving towards functional languages. As much as I love my OOP and SOLID principles, those languages and design patterns were built in a world with single CPU single thread in mind. Today, in the age of Internet and cloud, we need something more scalable, we need something better.
At my current employer, jet.com, F# is the answer to the scale problem, it's why we moved from 30k members to 2 million within 4 months without ever feeling it. It was because of the forward thinking technology stack, I firmly believe that anything with state would have fallen over already.
The great thing about F# is that things are immutable first so we don't have to worry about race conditions or data getting changed on us by these threads. We will only worry about this sort of thing if we explicitly make things mutable.
Back to the tutorial - let's fix up our client code.
First thing is first, let's add some CSS to this thing. My brother, Anton Bacaj, who is on this blog and is a great full stack developer wrote up a good CSS layout for a dashboard.
Figure now is a good time to open source this thing - I will let him post up the SASS files and such but for now just grab the main.css and add it to the project it can be found here.
Let us add epoch.js which is a realtime charting library in JavaScript which can be found here. This relies on D3.js which we can just grab via Nuget. The idea here is just to show the data but we can wire this up to any charting library.
CORS is also an issue that pops up with these sorts of things so lets allow it on our client app as well via the following in our web.config
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*"/>
</customHeaders>
</httpProtocol>
</system.webServer>
Next up let us create a new file in our scripts folder called signalrapp.js
, this file will house our signalr connection and we can map our JSON data to our charts.
Here is what that file may look like for us, this is a very simple wiring without much logic except to format the data. The idea here is that we could use any charting library, and in fact there are much better ones these days
$(function () {
$.connection.hub.url = "http://localhost:8085/signalr";
var perfHub = $.connection.metricsHub;
//setup some charts
var lineChart = $('#lineChart').epoch({
type: 'time.line',
data: generateLineData(),
axes: ['left', 'bottom', 'right']
});
var barChart = $('#barChart').epoch({
type: 'time.bar',
data: generateLineData(),
axes: ['bottom', 'right']
});
var areaChart = $('#areaChart').epoch({
type: 'time.area',
data: generateLineData(),
axes: ['left', 'bottom', 'right']
});
var charts = [
$('#gaugeChart').epoch({ type: 'time.gauge', value: 0.5 }),
$('#gaugeChart2').epoch({ type: 'time.gauge', value: 0.5 }),
$('#gaugeChart3').epoch({ type: 'time.gauge', value: 0.5 })
];
perfHub.client.broadcastPerformance = function (data) {
var timestamp = ((new Date()).getTime() / 1000) | 0;
var entry = [];
data.forEach(function (dataItem) {
if(dataItem.categoryName == "Processor Information")
{
console.log('processor: ');
console.log(dataItem);
entry.push({ time: timestamp, y: (dataItem.value / 1000000000000) });
charts[2].update((dataItem.value / 10000000000000))
}
else if (dataItem.categoryName == "Memory")
{
console.log(dataItem);
charts[0].update((dataItem.value / 10000))
entry.push({ time: timestamp, y: (dataItem.value / 10000) });
}
else if(dataItem.categoryName == "Process")
{
if(dataItem.counterName == "% Processor Time")
{
console.log(dataItem);
}
if(dataItem.counterName == "Working Set")
{
console.log(dataItem);
charts[1].update((dataItem.value / 1000000000))
entry.push({ time: timestamp, y: (dataItem.value / 1000000000) });
}
}
})
lineChart.push(entry);
barChart.push(entry);
areaChart.push(entry);
};
$.connection.hub.start().done();
//util function to generate some random data
function generateLineData() {
var data1 = [{ label: 'Layer 1', values: [] }];
for (var i = 0; i <= 128; i++) {
var x = 20 * (i / 128) - 10,
y = Math.cos(x) * x;
data1[0].values.push({ x: x, y: y });
}
var data2 = [
{ label: 'Layer 1', values: [] },
{ label: 'Layer 2', values: [] },
{ label: 'Layer 3', values: [] }
];
for (var i = 0; i < 256; i++) {
var x = 40 * (i / 256) - 20;
data2[0].values.push({ x: x, y: Math.sin(x) * (x / 4) });
data2[1].values.push({ x: x, y: Math.cos(x) * (x / Math.PI) });
data2[2].values.push({ x: x, y: Math.sin(x) * (x / 2) });
}
return data2;
}
});
Finally our index.html
file will look something like this. The metrics may not make much sense and we can think of much more interesting metrics to push and chart out but this is just to demonstrate the power of F# to build out these sorts of things.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Louie's Advent Dash</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700' rel='stylesheet' type='text/css'>
<link href='http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css' rel='stylesheet' type='text/css'>
<link href="css/main.css" rel="stylesheet" />
<link rel="stylesheet" href="css/epoch.min.css" />
<script src="Scripts/jquery-2.1.4.min.js"></script>
<script src="Scripts/jquery.signalR-2.2.0.min.js"></script>
<script src="http://localhost:8085/signalr/hubs" type="text/javascript"></script>
<script src="Scripts/d3/d3.min.js"></script>
<script src="Scripts/epoch.min.js"></script>
</head>
<body>
<div class="container-full">
<header>
<div class="grid">
<div class="menu column sm-col-12">
<ul class="clear-list inline-list">
<li>
<a href="#menu" class="text-medium"><i class="ion ion-grid text-blue"></i></a>
</li>
</ul>
<div class="logo">
<h5>Advent Dash</h5>
</div>
<ul class="clear-list inline-list pull-right">
<li>
<a href="#menu" class="text-medium"><i class="ion ion-android-calendar text-blue"></i></a>
</li>
</ul>
</div>
</div>
</header>
<div class="body">
<div class="charts js-charts">
<div class="grid">
<div class="column md-col-6 lg-col-4">
<div class="chart-container ">
<ul class="chart-header clear-list inline-list text-center ">
<li class="pull-left">
<a href="#" class="text-blue"><i class="ion align ion-more"></i></a>
</li>
<li>
<h5>Overall CPU Meters</h5>
</li>
<li class="pull-right">
<a href="#" class="text-blue js-close"><i class="ion align ion-backspace-outline"></i></a>
</li>
</ul>
<div class="chart-body">
<div id="gaugeChart3" class="epoch gauge-large" style="width: 400px; height: 300px;"></div>
</div>
</div>
</div>
<div class="column md-col-6 lg-col-4">
<div class="chart-container ">
<ul class="chart-header clear-list inline-list text-center ">
<li class="pull-left">
<a href="#" class="text-blue"><i class="ion align ion-more"></i></a>
</li>
<li>
<h5>Overall Memory Usage</h5>
</li>
<li class="pull-right">
<a href="#" class="text-blue js-close"><i class="ion align ion-backspace-outline"></i></a>
</li>
</ul>
<div class="chart-body">
<div id="gaugeChart" class="epoch gauge-large category20c" style="width: 400px; height: 300px;"></div>
</div>
</div>
</div>
<div class="column md-col-6 lg-col-4">
<div class="chart-container ">
<ul class="chart-header clear-list inline-list text-center ">
<li class="pull-left">
<a href="#" class="text-blue"><i class="ion align ion-more"></i></a>
</li>
<li>
<h5>App Memory Usage</h5>
</li>
<li class="pull-right">
<a href="#" class="text-blue js-close"><i class="ion align ion-backspace-outline"></i></a>
</li>
</ul>
<div class="chart-body">
<div id="gaugeChart2" class="epoch gauge-large" style="width: 400px; height: 300px;"></div>
</div>
</div>
</div>
<div class="column md-col-6 lg-col-4">
<div class="chart-container ">
<ul class="chart-header clear-list inline-list text-center ">
<li class="pull-left">
<a href="#" class="text-blue"><i class="ion align ion-more"></i></a>
</li>
<li>
<h5>CPU / Memory</h5>
</li>
<li class="pull-right">
<a href="#" class="text-blue js-close"><i class="ion align ion-backspace-outline"></i></a>
</li>
</ul>
<div class="chart-body">
<div id="areaChart" class="epoch" style="width: 500px; height: 300px;"></div>
</div>
</div>
</div>
<div class="column md-col-6 lg-col-4">
<div class="chart-container ">
<ul class="chart-header clear-list inline-list text-center ">
<li class="pull-left">
<a href="#" class="text-blue"><i class="ion align ion-more"></i></a>
</li>
<li>
<h5>CPU / Memory</h5>
</li>
<li class="pull-right">
<a href="#" class="text-blue js-close"><i class="ion align ion-backspace-outline"></i></a>
</li>
</ul>
<div class="chart-body">
<div id="barChart" class="epoch" style="width: 500px; height: 300px;"></div>
</div>
</div>
</div>
<div class="column md-col-6 lg-col-4">
<div class="chart-container ">
<ul class="chart-header clear-list inline-list text-center ">
<li class="pull-left">
<a href="#" class="text-blue"><i class="ion align ion-more"></i></a>
</li>
<li>
<h5>CPU / Memory</h5>
</li>
<li class="pull-right">
<a href="#" class="text-blue js-close"><i class="ion align ion-backspace-outline"></i></a>
</li>
</ul>
<div class="chart-body">
<div id="lineChart" class="epoch" style="width: 500px; height: 300px;"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<footer></footer>
</div>
<script src="Scripts/signalrapp.js" type="text/javascript"></script>
</body>
</html>
Let's spin it all up and see what it looks like:
We can substitute another charting library and the way I am consuming the metrics on the JavaScript side isn't the cleanest but really this post is about F# and the Advent Calendar not JavaScript!
Much can be said about the power of F# as a functional first language. There really are so many great benefits to using F#, that is why Jet.com uses it as our primary backend language to our distributed system and all of our micro-services and libraries are built in it. However, in this article I hope I have been able to show you the power of F# as a general purpose language, potentially, and hopefully, replacing even your favorite OO language for your day to day work.
Merry Christmas!
P.S. Don't forget to subscribe to the e-mail list below.