Sending IoT Hub telemetry to a Blazor Web App

For those who are interested in software development for the web using the C# programming language, Blazor is a viable alternative for building progressive websites as compared to Asp.Net Core / Angular / JavaScript.

Blazor lets you build interactive web UIs using C# instead of JavaScript. Blazor apps are composed of reusable web UI components implemented using C#, HTML, and CSS. Both client and server code is written in C#, allowing you to share code and libraries.

In the past, I already implemented Blazor on the Edge, including message routing.

Now, let’s see how we can integrate a Blazor website with telemetry coming from an Azure IoT Hub in the cloud.

For this to happen, we need this architecture:

So, the moving parts are:

  • An IoT Hub with message routing enabled
  • Azure Function with IoT Hub / EventHub trigger
  • Server-side Blazor website with API Controller integration

Let’s see how this is set up.

I created these Azure resources in advance:

I made these overall configuration changes already:

  • The WebApp is based on ASP.Net Core 3.1 on Windows
  • The WebApp is put on an Application Service Plan running a B1 SKU so we can enjoy ‘Always on’ for better performance (no start-up time)
  • It’s a good practice to set ‘HTTPS Only’ to true in the TLS/SSL settings of the Webapp
  • The FunctionApp is based on ASP.Net Core on Windows. It’s hosted on the same Application Service Plan and runs alongside the WebApp on the B1 server

The Application Service Plan is therefore hosting both the website and the Function App.

Let’s connect the resources.

Setting up a device

We need an Azure IoT device to generate some telemetry to process.

For this, I use this Microsoft IoT device example from GitHub. Just follow this tutorial to set up the test device. You basically start ‘dotnet run’ in the folder where the project is pushed.

After filling in the connection string of a new test device (registered in the IoT Hub) in the source code and running the application, we see data being sent to the IoT Hub:

And after a few minutes, we see the IoT Hub charts show the incoming messages:

We have some telemetry to show later on.

Keep the device running.

Setting up the routing to the Azure Function

Each IoT Hub exposes it’s incoming telemetry to other Azure resources.

The most popular way is making use of IoT Hub routing. You can send data to eg. event hubs and blob storage. This routing also makes it possible to both filter messages for specific endpoints and enrich the incoming telemetry with some basic properties.

To receive this telemetry in an Azure Function, there are a couple of solutions available.

You can eg. trigger a function based on Azure resources which are provided telemetry using the IoT Hub routing. This could be that event hub or a message bus or blob storage which is configured as an endpoint.

Azure Event Grid is also a viable solution.

Here we use the ‘old fashioned’ solution. We make use of the original IoT Hub trigger which listens to the Event Hub compatible endpoint which is exposed by the IoT Hub already.

Basically, we make use of both the original IoT Hub trigger and the ‘original routing’.

Setting up the consumer group

It’s a good practice to create multiple consumer groups for all resources consuming the Event Hub compatible endpoint. The coming Azure Function trigger will use it so we create this ‘fa’ consumer group:

Normally, we should be ready configuring the IoT Hub.

Though, there is a catch…

If somewhere in the future IoT Hub routing is enabled (eg. a route to a blob storage endpoint is created) this Event Hub compatible endpoint will stop emitting telemetry!

To fix this, we create an explicit route to the ‘build-in endpoint’.

Setting up the routing

We just create a route named ‘Build-inEndpointRoute’ and point it to the already selectable Build-in endpoint and route the device telemetry messages:

Note: We leave the query to ‘true’ so all incoming messages are routed.

Save the route and wait until it’s created. This will result in this line:

Our IoT Hub is now exposing incoming telemetry. Let’s consume that telemetry.

Creating the Azure Function IoT Hub trigger

We now move over to the Azure Function App.

Add an Azure Function in the portal for adding an IoT Hub triggered function:

Fill in a proper function name and set it to the correct consumer group ‘fa’:

As you can see, the Event Hub connection has yet to be set up.

Hit the ‘New’ link for an Event Hub connection dialog. Select the IoT Hub dialog and select your IoT Hub (and the built-in events endpoint):

Once the connection is set, create the function. Check the log and see the messages flowing into the Azure Function:

Ok, the messages are arriving at the Azure Function, but we want to send them to a Blazor app. So let’s create that app first.

Creating a Blazor app

Open Visual Studio 2019. Here we create a new project:

A Blazor template is available so select it:

Give the project the name and location of your choice:

We create a Blazor Server App. This will run in a Web App and serves an HTML experience.

Accept all the standard settings and create the app:

Once the project is created, test it by building and running it. The website will be shown:

You see the default ‘Index.razor’ page. We want to show the telemetry here. So we have to add some logic.

After playing around, stop the app.

Adding an API Controller to the Blazor app

The easiest way of ingesting data is making use of an API controller which accepts REST calls. We want to accept POST methods with the telemetry in the body.

Note: Technically SignalR could be a solution too. The heart of Blazor is SignalR communication between backend and webpages. This SignalR hub could be hosted in Azure SignalR Services and therefore be reachable by an Azure Function too. This is out of scope in this blog.

In the project, create a ‘Controllers’ folder. Within the controller folder, add a controller class using the (right-click) dialog menu:

Add an API Controler with read/write actions:

Note: We will alter this controller in a few minutes. For now, it’s a convenient way for testing controller access.

Keep the ‘ValuesController.cs’ name and create the class. The controller is now created:

The controller is not yet hooked up with the ASP.NET core services. The website routing has to be set up. So open up ‘StartUp.cs’ to do this.

At the top of the ‘StartUp.cs’, add this using:

using Microsoft.AspNetCore.Mvc; // Added

In the available class, in the ‘ConfigureServices’ method, add at the top:

services.AddMvc(options => options.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_3_0); // Added

In the ‘Configure’ method add this line just before ‘app.UseEndpoints(..)’:

app.UseMvcWithDefaultRoute(); // Added

This is the result of the three additions:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using IoTOnBlazorApp.Data;
using Microsoft.AspNetCore.Mvc; // Added

namespace IoTOnBlazorApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(options => options.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_3_0); // Added

            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddSingleton<WeatherForecastService>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseMvcWithDefaultRoute(); // Added

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapBlazorHub();
                endpoints.MapFallbackToPage("/_Host");
            });
        }
    }
}

Run the project again and navigate to ‘https://localhost:%5Byour portnumber]/api/values’ where you have to replace ‘[your portnumber]’:

As you can see, we get the correct answer from the GET method (an array of two values).

We will use the POST method later on because we have to pass a JSON message to the page through the controller. Before altering the Controller to handle telemetry, we first need something to pass telemetry from this controller to the Index page.

Setting up a telemetry service

Blazor supports this concept of injectable services.

Note: Perhaps you already noticed the ‘WeatherForecastService’ in the startup.cs. This service is a C# class which serves random weather information. We will use a similar solution to inject a telemetry service.

We have to create a service and register it before we can inject it:

We will use a simple service that can only ingest a message and distribute it to listeners. Create a new class in the already existing Data folder:

using System;
using System.Threading.Tasks;

namespace IoTOnBlazorApp.Data
{
    public class TelemetryService
    {
        public event EventHandler<TelemetryDataPoint> InputMessageReceived;

        private async Task OnInputMessageReceived(TelemetryDataPoint telemetryDataPoint)
        {
            await Task.Run(() => { InputMessageReceived?.Invoke(this, telemetryDataPoint); });
        }

        public async Task SendTelemetry(TelemetryDataPoint telemetryDataPoint)
        {
            if (telemetryDataPoint != null)
            {
                await OnInputMessageReceived(telemetryDataPoint);
            }
        }
    }

    public class TelemetryDataPoint
    {
        private double temperature { get; set; }
        private double humidity { get; set; }
    };
}

This service is capable to receive a message using the method which will send it to all listeners attached to the event.

Note: The actual message format is based on the message format passed by the device simulation application (in ‘class SimulatedDevice’) which you pulled from GitHub:

var telemetryDataPoint = new
{
    temperature = currentTemperature,
    humidity = currentHumidity
};

We now only have to register for the service. Go to ‘startup.cs’ and add this line at the end of ‘ConfigureService’:

 services.AddSingleton<TelemetryService>();

Note: The AddSingleton method makes the Telemetry Service available to all users logged in to the website. You can limit access to the scope of your session by choosing AddScoped.

Updating the Blazor page to show telemetry

Now move over to the ‘Index.razor’. Inject and use the TelemetryService. The page will look like this:

@page "/"

@using IoTOnBlazorApp.Data

@inject TelemetryService TelemetryService

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<p>
    Message:@message
</p>

@code
{
    private string message;

    protected override void OnInitialized()
    {
        base.OnInitialized();

        TelemetryService.InputMessageReceived += OnInputTelemetryReceived;
    }

    private void OnInputTelemetryReceived(object sender, TelemetryDataPoint telemetryDataPoint)
    {
        message = $"Temperature: {telemetryDataPoint.temperature} - Humidity: {telemetryDataPoint.humidity}";

        InvokeAsync(() => StateHasChanged());
    }
}

When the page is loaded, the instance of the Telemetry Service singleton is injected. So we can connect to the InputMessageReceived event on initialization.

Once a message is received, the message string member of the page is updated which is bound to the @message placeholder in the HTML. This is the power of SignalR.

Note: Once a message is received, we have to refresh the message placeholder. Normally, Razor pages are updated after some action of the user in the browser. Now, something is altered on the backend. Therefore, we have to notify the page the state has changed by calling “StateHasChanged()”.

Note: For simplicity, I do not use a code-behind class for code integration. For production code, please check out the “@inherits” keyword of the page to find out how code-behind classes support a separation of concerns.

At this point, the Blazore page is now connected to the Telemetry service. The last thing to do in the Blazor app is updating the API Controller so it can receive and pass on incoming messages.

Updating the API Controller

Go back to the ValuesController which is our API controller and capable of ingesting telemetry.

We know how to write methods that ingest Rest POST calls. The tricky part is the injection of the Telemetry Service.

We cannot reuse this ‘@inject’ (or the [Inject] attribute seen in Blazor code-behind solutions). This is the Blazor way of injection services.

We are in luck!

Basic constructor injection is still working fine for us. and we have to call the Telemetry service method when a POST message is received.

So all we have to do is to change the API controller code into this:

using System.Threading.Tasks;
using IoTOnBlazorApp.Data;
using Microsoft.AspNetCore.Mvc;

namespace IoTOnBlazorApp.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private TelemetryService _telemetryService;

        public ValuesController(TelemetryService telemetryService)
        {
            _telemetryService = telemetryService;
        }

        // POST api/<ValuesController>
        [HttpPost]
        public async Task Post([FromBody] TelemetryDataPoint telemetryDataPoint)
        {
            await _telemetryService.SendTelemetry(telemetryDataPoint);
        }
    }
}

We are now injecting the Telemetry Service singleton using the constructor. Incoming messages (in the body of the POST message) are automatically turned into a TelemetryDataPoint (with temperature and humidity values).

Deploying the Blazor project to a Web App

Our Blazor project is ready, just deploy it to the cloud in our Web App.

For this, start the Publish wizard and select publishing to Azure:

We are going to publish to an existing App Service (on Windows):

Select the existing Web App:

The publish template is created. Now publish:

After a successful publish, the website is shown in the browser:

Notice the website is secured using HTTPS. But still, no messages are shown…

Our device is still generating telemetry which is sent to the Azure Function.

Our website is ‘waiting’ on telemetry using the API controller.

So let’s connect these two worlds.

Update the Azure Function to pass on telemetry to the Blazor app

Go back to the Azure Function and change the code:

In this function, the triggered message must be sent to the API controller POST endpoint.

Now, update the code of the function:

using System;
using System.Net.Http;
using System.Text;

public static void Run(string myIoTHubMessage, ILogger log)
{
    log.LogInformation($"C# IoT Hub trigger function processed a message: {myIoTHubMessage}");

    var url = "https://[your WebAp name].azurewebsites.net/api/values";
  
    var stringContent = new StringContent(myIoTHubMessage, Encoding.UTF8, "application/json");

    using var httpClient = new HttpClient();

    var response = httpClient.PostAsync(url, stringContent).Result;

    Console.WriteLine($"Response {response.StatusCode}");
}

Notice you have to fill in the right URL string for your own service.

Note: For simplicity, no extra authentication is needed. The endpoint is wide open for everyone in the world which knows the specific routing URL.

Start running the updated function. Check the log for compile errors.

Once the function is started, you can see the Telemetry messages arriving in the Blazor Web App:

We have successfully connected an IoT Hub device to the Blazor App!

Conclusion

The same mechanism can be used for more generic use-cases where the user of a Blazor website has to be notified by some external solution.

For simplicity, no filtering on Device ID or other ‘enriched’ data is conducted. So you will see all messages from all devices flowing through the IoT Hub. You could start filtered using the IoT Hub routing or do something special in the Azure Function or even in the controller or service.

Enhancement could be interesting to add at least the device ID.

Perhaps you noticed in the simulated device code a message property could be added:

message.Properties.Add("temperatureAlert", (currentTemperature > 30) ? "true" : "false");

This property is lost in translation in the Azure Function. There is a simple solution to fix this. Extra message properties have to be added to the original message. This will likely result in an altered message format.