Real-time chart using ASP.NET Core and WebSocket

Using WebSocket support in ASP.NET Core we can easily write real-time data visualization solutions. What if we mix together ASP.NET Core, WebSocket, Knockout and 3D charts? The answer is – nice real-time chart that visualizes sensor readings. This blog post introduces simple real-time chart and IoT device simulator that help to get started with real-time data visualization.

Solution in glance

The following diagram illustrates our solution where IoT device reports readings to web site and users can see readings in real time.

IoT, ASP.NET Core and WebSocket in action

There is IoT device that reports sensors readings to ASP.NET Core application. Users open the site in their browsers and they will see readings in real-time. Readings are shown as table and visualized as a line chart.

NB! Those who are interested in playing with Visual Studio 2017 solution and source code can find it from AspNetCoreRealTimeChart Github repository.

Adding WebSocket support

First we go and visit Radu Matei’s blog and take some code from there. Of course, we give him cookie and credits for his excellent writing Creating a WebSockets middleware for ASP .NET Core. We use a little modified version of his WebSocketManager class.

public class TemperatureSocketManager
{
   
private static ConcurrentDictionary<string, WebSocket> _sockets = new ConcurrentDictionary<string, WebSocket
>();

   
public WebSocket GetSocketById(string
id)
    {
       
return
_sockets.FirstOrDefault(p => p.Key == id).Value;
    }

   
public ConcurrentDictionary<string, WebSocket
> GetAll()
    {
       
return
_sockets;
    }

   
public string GetId(WebSocket
socket)
    {
       
return
_sockets.FirstOrDefault(p => p.Value == socket).Key;
    }
   
public string AddSocket(WebSocket
socket)
    {
       
var
id = CreateConnectionId();
        _sockets.TryAdd(id, socket);

       
return
id;
    }

   
public async Task RemoveSocket(string
id)
    {
       
WebSocket
socket;
        _sockets.TryRemove(id,
out
socket);

       
await socket.CloseAsync(closeStatus: WebSocketCloseStatus
.NormalClosure,
                                statusDescription:
"Closed by the WebSocketManager"
,
                                cancellationToken:
CancellationToken
.None);
    }

   
private string
CreateConnectionId()
    {
       
return Guid
.NewGuid().ToString();
    }

   
public async Task SendMessageToAllAsync(string
message)
    {
       
foreach (var pair in
_sockets)
        {
           
if (pair.Value.State == WebSocketState
.Open)
               
await
SendMessageAsync(pair.Value, message);
        }
    }

   
private async Task SendMessageAsync(WebSocket socket, string
message)
    {
       
if (socket.State != WebSocketState
.Open)
           
return
;

       
await socket.SendAsync(buffer: new ArraySegment<byte>(array: Encoding
.ASCII.GetBytes(message),
                                                                offset: 0,
                                                                count: message.Length),
                                messageType:
WebSocketMessageType
.Text,
                                endOfMessage:
true
,
                                cancellationToken:
CancellationToken.None);
    }
}

We also need WebSocket middleware to keep internal sockets dictionary fresh. Here we will use a little modified version of Radu Matei’s WebSocket middleware.

public class TemperatureSocketMiddleware
{
   
private readonly RequestDelegate
_next;
   
private readonly TemperatureSocketManager
_socketManager;

   
public TemperatureSocketMiddleware(RequestDelegate
next,
                                       
TemperatureSocketManager
socketManager)
    {
        _next = next;
        _socketManager = socketManager;
    }

   
public async Task Invoke(HttpContext
context)
    {
       
if
(!context.WebSockets.IsWebSocketRequest)
        {
           
await
_next.Invoke(context);
           
return
;
        }

       
var socket = await
context.WebSockets.AcceptWebSocketAsync();
       
var
id = _socketManager.AddSocket(socket);

       
await Receive(socket, async
(result, buffer) =>
        {
           
if (result.MessageType == WebSocketMessageType
.Close)
            {
               
await
_socketManager.RemoveSocket(id);
               
return
;
            }
        });
    }

   
private async Task Receive(WebSocket socket, Action<WebSocketReceiveResult, byte
[]> handleMessage)
    {
       
var buffer = new byte
[1024 * 4];

       
while (socket.State == WebSocketState
.Open)
        {
           
var result = await socket.ReceiveAsync(buffer: new ArraySegment<byte
>(buffer),
                                                    cancellationToken:
CancellationToken.None);

            handleMessage(result, buffer);
        }
    }
}

Now let’s add reference to Microsoft.AspNetCore.WebSockets NuGet package and wire WebSocket stuff to application. We use Configure() method of Startup class for this.

app.UseStaticFiles();
app.UseWebSockets();
app.UseMiddleware<
TemperatureSocketMiddleware
>();

app.UseMvc(routes =>
{
    routes.MapRoute(
        name:
"default"
,
        template:
"{controller=Home}/{action=Index}/{id?}");
});

We have to register also WebSocket manager as a service to be able to broadcast data to browsers. Here is the ConfigureServices() method of application Startup class.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddSingleton<
TemperatureSocketManager>();
}

Now we have everything we need to support WebSockets in out application.

Web API for IoT device

We need some web end-point where IoT device can send sensor readings.

public class ApiController : Controller
{
   
private readonly TemperatureSocketManager
_socketManager;

   
public ApiController(TemperatureSocketManager
socketManager)
    {
        _socketManager = socketManager;
    }

   
public async Task Report(double
liquidTemp)
    {
       
var reading = new
        {
            Date =
DateTime
.Now,
            LiquidTemp = liquidTemp
        };

       
await _socketManager.SendMessageToAllAsync(JsonConvert
.SerializeObject(reading));
    }

   
public async Task
Generate()
    {
       
var rnd = new Random
();

       
for(var
i = 0; i < 100; i++)
        {               
           
await
Report(rnd.Next(23, 35));
           
await Task.Delay(5000);
        }
    }
}

Report() method accepts one sensor reading per time and broadcasts it to all registered sockets. Generate() method is there to simulate sensor that reports data. We can use this method if we don’t have any IoT device in our network.

Building user interface

Let’s build user interface for our solution to display real-time data to users. We start with simple home controller that just servers some views with no additional work.

public class HomeController : Controller
{
   
public IActionResult
Index()
    {
       
return
View();
    }

   
public IActionResult
Error()
    {
       
return View();
    }
}

Home view of Index controller is also simple. There are references to some D3 chart and Knockout related scripts. We will come back to these later. The view has placeholder for D3 chart. There is also table where sensor readings are displayed.

@{
    ViewData[
"Title"] = "Home Page"
;
}

<div class="row">
    <div class="col-lg-8 bigChart" data-bind="lineChart: lineChartData"></div>
    <div class="col-lg-4">
        <table class="table">
            <thead>
                <tr>
                    <th>#</th>
                    <th>Time</th>
                    <th>Temperature</th>
                </tr>
            </thead>
            <tbody data-bind="foreach: lineChartData">
                <tr>
                    <td data-bind="text: $index() + 1"></td>
                    <td data-bind="text: Date.toLocaleTimeString()"></td>
                    <td data-bind="text: LiquidTemp"></td>
                </tr>
            </tbody>
        </table>
    </div
>
</
div>

@section Scripts {
   
<script src="~/js/data-view-model.js"></script>
    <script src="~/js/line-chart-binding.js"></script>

   
<script>
        var D3KD = this
.D3KD || {};

        (
function
() {
           
"use strict"
;
           
var dataViewModel = new D3KD
.dataViewModel();

           
var protocol = location.protocol === "https:" ? "wss:" : "ws:"
;
           
var wsUri = protocol + "//"
+ window.location.host;
           
var socket = new
WebSocket(wsUri);

            socket.onmessage =
function
(e) {
               
var
reading = JSON.parse(e.data);
                reading.Date =
new
Date(reading.Date);

                dataViewModel.addDataPoint(reading);
            };

            ko.applyBindings(dataViewModel);
        }());
   
</script>
}

When page is loaded then WebSocket connection is established and script starts listening to WebSocket. When data comes in the script sets Date property to JavaScript date and adds reading to Knockout array of data model.

Before wiring everything together let’s also modify layout view. I removed environment based mess from layout view and made popular scripts to be downloaded from CDN-s.

<!DOCTYPE html>
<
html
>
<
head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - AspNetCoreRealTimeChart</title>

   
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" 
/>
</
head
>
<
body>
    <nav class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">AspNetCoreRealTimeChart</a>
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
                </ul>
            </div>
        </div>
    </nav>
    <div class="container body-content">
        @RenderBody()
       
<hr />
        <footer>
            <p>&copy; 2017 - AspNetCoreRealTimeChart</p>
        </footer>
    </div>

   
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"></script>
    <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"></script>
    <script src="http://d3js.org/d3.v3.min.js"></script>
    <script src="http://knockoutjs.com/downloads/knockout-3.0.0.js"></script>
    @RenderSection("Scripts", required: false)
</body
>
</
html
>

With visible part of user interface we are done now and it’s time to stitch all parts together.

Displaying real-time data

As we are using D3 chart and Knockout to display real-time data we need some classes to bind these two together. I found d3-knockout-demo by Teodor Elstad where this problem is solved. It’s simple demo you can download to your machine and run it directly from directory. It doesn’t need any external data services to work. We start with data model class that is simplified to minimum.The code below goes to data-view-model.js file (see Index view of home controller).

/*global ko, setInterval*/

var D3KD = this
.D3KD || {};

(
function
(namespace) {
   
"use strict"
;
    namespace.dataViewModel =
function
() {
       
var self = this
;

        self.lineChartData = ko.observableArray();
        self.addDataPoint =
function
(point) {
           
if (self.lineChartData().length >= 10) {
                self.lineChartData.shift();
            }

            self.lineChartData.push(point);
        };
    };
}(D3KD));

The data model class holds Knockout observable array with readings. It also has addDataPoint() method that adds new reading to array. It aslo avoids array to grow over 10 elements. If array already has 10 readings then first reading is removed before new one is added.

To keep chart up to date we need Knockout bindingHandler. This comes also from Teodor’s demo project and it goes to line-chart-binding.js file (see Index view of home controller).

/*global ko, d3*/

ko.bindingHandlers.lineChart = {
    init:
function
(element) {
       
"use strict"
;

       
var
margin = { top: 20, right: 20, bottom: 30, left: 50 },
            elementWidth = parseInt(d3.select(element).style(
"width"
), 10),
            elementHeight = parseInt(d3.select(element).style(
"height"
), 10),
            width = elementWidth - margin.left - margin.right,
            height = elementHeight - margin.top - margin.bottom,

            svg = d3.select(element).append(
"svg"
)
                .attr(
"width"
, width + margin.left + margin.right)
                .attr(
"height"
, height + margin.top + margin.bottom)
                .append(
"g"
)
                .attr(
"transform", "translate(" + margin.left + "," + margin.top + ")"
);

        svg.append(
"g"
)
            .attr(
"class", "x axis"
)
            .attr(
"transform", "translate(0," + height + ")"
);

        svg.append(
"g"
)
            .attr(
"class", "y axis"
)
            .append(
"text"
)
            .attr(
"transform", "rotate(-90)"
)
            .attr(
"y"
, 6)
            .attr(
"dy", ".71em"
)
            .style(
"text-anchor", "end"
)
            .text(
"Temperature"
);

        svg.append(
"path"
)
            .attr(
"class", "line data"
);

    },
    update:
function
(element, valueAccessor) {
       
"use strict"
;

       
var
margin = { top: 20, right: 20, bottom: 30, left: 50 },
            elementWidth = parseInt(d3.select(element).style(
"width"
), 10),
            elementHeight = parseInt(d3.select(element).style(
"height"
), 10),
            width = elementWidth - margin.left - margin.right,
            height = elementHeight - margin.top - margin.bottom,

           
// set the time it takes for the animation to take.
            animationDuration = 750,

            x = d3.time.scale()
                .range([0, width]),

            y = d3.scale.linear()
                .range([height, 0]),

            xAxis = d3.svg.axis()
                .scale(x)
                .orient(
"bottom"
),

            yAxis = d3.svg.axis()
                .scale(y)
                .orient(
"left"
),

           
// define the graph line
            line = d3.svg.line()
                .x(
function (d) { return
x(d.Date); })
                .y(
function (d) { return
y(d.LiquidTemp); }),

            svg = d3.select(element).select(
"svg g"
),

           
// parse data from the data-view-model
            data = ko.unwrap(valueAccessor());

       
// define the domain of the graph. max and min of the dimensions
        x.domain(d3.extent(data, function (d) { return
d.Date; }));
        y.domain([0, d3.max(data,
function (d) { return
d.LiquidTemp; })]);

        svg.select(
"g.x.axis"
)
            .transition()
            .duration(animationDuration)
            .call(xAxis);

        svg.select(
"g.y.axis"
)
            .transition()
            .duration(animationDuration)
            .call(yAxis);

       
// add the line to the canvas
        svg.select("path.line.data"
)
            .datum(data)
            .transition()
            .duration(animationDuration)
            .attr(
"d", line);
    }
};

No we have all ends connected and it’s time to see the web applicaton in action.

Real-time sensor data in action

To illustrate the end result better I added here screenshot and video. Video demonstrates how call to /api/Generate broadcasts new reading to all registered sensors after every five seconds.

aspnet-core-websocket-chart
Screenshot of real-time sensor data.

Video of real-time sensor data. Notice how always last ten result are shown.

Wrapping up

After borrowing some code from internet and climbing to the shoulders of giants we are able to build simple real-time sensor data page with table and line chart. It was easy to get done with WebSocket support and sensor data broadcasting. On UI side things were more complex as we had to write a special handler class to update chart. The idea of handler was simple but the API of D3 charts is not very easy and intuitive in the beginning. Still we were able to build application that we can use in different data visualization scenarios.

References

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    22 thoughts on “Real-time chart using ASP.NET Core and WebSocket

    • March 29, 2017 at 10:35 pm
      Permalink

      Hi, there is bug in the AddSocket method. The returned id will never exisit in the dictionary.

      replace
      sockets.TryAdd(CreateConnectionId(), socket);
      with
      sockets.TryAdd(id, socket);

      public string AddSocket(WebSocket socket)
      {
      var id = CreateConnectionId();
      _sockets.TryAdd(CreateConnectionId(), socket);

      return id;
      }

    • March 31, 2017 at 4:12 pm
      Permalink

      Thanks for pointing out the bug. The code is fixed now.

    • April 2, 2017 at 10:37 pm
      Permalink

      We did this exactly the same exercise – but with SignalR. No need for code that manipulates websockets :)

    • February 5, 2018 at 7:44 pm
      Permalink

      Hi,
      Example doesn’t work. Visual Studio 2017 15.5.6 gives following output:
      AspNetCoreRealTimeChart> The application to execute does not exist: ‘C:\Users\A\Documents\Visual Studio 2017\Projects\AspNetCoreRealTimeChart\AspNetCoreRealTimeChart\bin\Debug\netcoreapp1.1\AspNetCoreRealTimeChart.dll’

      Error List shows:
      Severity Code Description Project File Line Suppression State
      Error Duplicate ‘Content’ items were included. The .NET SDK includes ‘Content’ items from your project directory by default. You can either remove these items from your project file, or set the ‘EnableDefaultContentItems’ property to ‘false’ if you want to explicitly include them in your project file. For more information, see https://aka.ms/sdkimplicititems. The duplicate items were: ‘wwwroot\js\data-view-model.js’; ‘wwwroot\js\line-chart-binding.js’ AspNetCoreRealTimeChart C:\Program Files\dotnet\sdk\2.1.4\Sdks\Microsoft.NET.Sdk\build\Microsoft.NET.Sdk.DefaultItems.targets 285

      Please, can you help me out! I have done over 15 chart samples, no one works.

      AllanT

    • February 6, 2018 at 3:01 pm
      Permalink

      Hi,

      I got errors:

      AspNetCoreRealTimeChart>
      It was not possible to find any compatible framework version
      The specified framework ‘Microsoft.NETCore.App’, version ‘1.1.2’ was not found.
      Check application dependencies and target a framework version installed at:
      Alternatively, install the framework version ‘1.1.2’.

      Can you upgrade the project to Core 2.0 compatible packages, because it seems that Core 1.1 is not relevant anymore.

      AllanT

    • February 8, 2018 at 9:32 am
      Permalink

      Let’s try to make it work with .NET Core 2.0. To make it work with .NET Core 1.1 you must have .NET Core 1.1 installed on your machine. But let’s try to upgrade solution to 2.0.

    • February 8, 2018 at 9:42 am
      Permalink

      And we are on .NET Core 2.0 now!

    • February 8, 2018 at 5:50 pm
      Permalink

      Hi Gunnar,
      Thank you very much!. Though, in my case the simulator doesn’t work, I see an empty chart with axes and no data points on it. Also, there is no data in the table on the right side.
      BTW, how to change the simulator with the real Iot device. I have one in my network, it is sending temperature readings to MySQL database.

    • February 8, 2018 at 7:27 pm
      Permalink

      Run web application. Then open another tab with it and call /api/generate URL to start generating data. To report reading there is /api/report call like shown in blog post above.

    • February 8, 2018 at 8:19 pm
      Permalink

      Sorry, I didn’t understand, how to define /api/generate URL.

    • February 8, 2018 at 8:24 pm
      Permalink

      1. Run web application in browser
      2. Take URL with copy and paste (by settings I suppose it’s http://localhost:21585/)
      3. Open new tab in browser
      4. Go to the following URL in it: http://localhost:21585/api/generate
      5. Now go back to tab where web application runs and see data coming

      It generates data over web socket with five seconds interval.

    • February 8, 2018 at 9:14 pm
      Permalink

      Hi,
      api/generate works well. A magical thing! Thank you.
      What about listening IoT devices in the local network?

    • February 8, 2018 at 9:26 pm
      Permalink

      Sorry, it’s getting harder again??

    • February 8, 2018 at 9:30 pm
      Permalink

      This is the URL where IoT device makes requesto to report the reading. Web application must be hosted on some machine that IoT device can see in network and the web application must be accessible over HTTP. So, instead of localhost you have some other host name.

    • Pingback:performance in using web socket in .net core2 – program faq

    • May 16, 2018 at 10:00 am
      Permalink

      I get the error “WebSocket connection to ‘ws://localhost:21585/’ failed: Error during WebSocket handshake: Unexpected response code: 200” for “var socket = new WebSocket(wsUri);”

      Any idea what causes this

    • May 16, 2018 at 10:31 am
      Permalink

      Forget my question – I figured out what the problem was :-)

      When running it from VS 2017 it was started with IIS Express

    • November 25, 2018 at 3:38 pm
      Permalink

      if (result.MessageType == WebSocketMessageType.Close)
      {
      >> await _socketManager.RemoveSocket(id, socket);
      return;
      }

      System.NullReferenceException: ‘Object reference not set to an instance of an object.’

      I found that
      public async Task RemoveSocket(string id )
      {
      WebSocket socket = socket;
      _sockets.TryRemove(id, out socket);

      socket = null

      so then await socket.CloseAsync(…) fall into exeption.

      Don’t know how to figure this out.

    • February 3, 2019 at 10:20 am
      Permalink

      Hi,
      I know Its not very relevant to your post but…
      we have a realtime mobile app
      we use .net Core 2.2
      what should we use…
      signalR or Websocket or socket.io?

    • February 3, 2019 at 8:53 pm
      Permalink

      As SignalR is supported on ASP.NET Core for now I suggest to go with it.

    Leave a Reply

    Your email address will not be published. Required fields are marked *