A .NET CLI tool that sends simulated industrial sensor telemetry through three Azure IoT messaging paths simultaneously, demonstrating the architecture options available in Microsoft Azure IoT.
All three paths deliver the same JSON payload to a single Fabric Real-Time Intelligence (RTI) table:
Path 1: CLI ──MQTT──▶ IoT Hub ──────────────────▶ Event Hubs ──▶ Fabric RTI
Path 2: CLI ──MQTT──▶ Event Grid MQTT Broker ───▶ Event Hubs ──▶ Fabric RTI
Path 3: CLI ──MQTT──▶ IoT Operations MQTT Broker▶ Dataflow ───▶ Fabric RTI
All three senders use MQTTnet — the only difference is the broker endpoint and authentication method.
{
"deviceId": "factory-sensor-01",
"timestamp": "2026-03-01T13:05:02.123Z",
"source": "iot-hub",
"temperature": 72.4,
"pressure": 14.7,
"vibration": 0.032
}Values use a random walk algorithm with realistic drift. Vibration has a ~5% chance of anomaly spikes (0.15–0.25g) to simulate bearing wear.
Each sender uses a unique device identity:
| Sender | DeviceId Source |
|---|---|
| IoT Hub | IoTHub.DeviceId from appsettings.json |
| Event Grid | CN (Common Name) extracted from X.509 client certificate |
| IoT Operations | IoTOperations.Username from appsettings.json |
- .NET 8 SDK
- An Azure subscription with the following resources provisioned:
- Azure IoT Hub
- Azure Event Grid namespace with MQTT broker enabled
- Azure IoT Operations on an Arc-enabled Kubernetes cluster
- Microsoft Fabric workspace with Real-Time Intelligence eventhouse
cd IoTDemo
dotnet build
# Edit appsettings.json with your connection details (see below)
dotnet runThe Spectre.Console dashboard launches. Press 1, 2, or 3 to toggle each sender on/off. Press Q to quit.
──────────────── IoT Demo — Industrial Sensor Telemetry ────────────────
Press 1/2/3 to toggle senders | Q to quit
╭──────┬───────────┬──────────┬────────────┬───────────┬────────────────╮
│ # │ Sender │ Status │ Messages │ Last Sent │ Last Error │
├──────┼───────────┼──────────┼────────────┼───────────┼────────────────┤
│ [1] │ IoT Hub │ ● Running│ 42 │ 13:05:02 │ -- │
│ [2] │ Event Grid│ ○ Stopped│ 0 │ -- │ -- │
│ [3] │ IoT Ops │ ● Running│ 38 │ 13:05:01 │ -- │
╰──────┴───────────┴──────────┴────────────┴───────────┴────────────────╯
Press 1/2/3 to toggle │ Q to quit │ Interval: 2s
All connection details are stored in appsettings.json:
{
"IntervalSeconds": 2,
"IoTHub": {
"Hostname": "<iothub-name>.azure-devices.net",
"DeviceId": "factory-sensor-01",
"SharedAccessKey": "<device-sas-key>"
},
"EventGrid": {
"Hostname": "<namespace>.westus2-1.ts.eventgrid.azure.net",
"Port": 8883,
"ClientCertPath": "certs/client.pem",
"ClientKeyPath": "certs/client-key.pem",
"CaCertPath": "certs/ca.pem",
"Topic": "factory/sensors/telemetry"
},
"IoTOperations": {
"Hostname": "aio-mq-dmqtt-frontend",
"Port": 1883,
"Username": "factory-sensor-03",
"Password": "<password>",
"Topic": "azure-iot-operations/data/factory-sensor-03"
}
}
⚠️ Do not commit real credentials. Thecerts/directory is in.gitignore.
IoT Hub uses MQTT v3.1.1 with SAS token authentication. The CLI generates the SAS token automatically from the shared access key.
-
Create an IoT Hub (S1 tier or free tier for demo):
az iot hub create --name <iothub-name> --resource-group <rg> --sku S1
-
Register a device:
az iot hub device-identity create --hub-name <iothub-name> --device-id factory-sensor-01
-
Get the device SAS key:
az iot hub device-identity show --hub-name <iothub-name> --device-id factory-sensor-01 --query "authentication.symmetricKey.primaryKey" -o tsv
-
Update
appsettings.json:Hostname:<iothub-name>.azure-devices.netDeviceId:factory-sensor-01SharedAccessKey: the key from step 3
-
Route to Fabric RTI: Use IoT Hub's built-in Event Hubs-compatible endpoint as a source in your Fabric eventstream.
Event Grid uses MQTT v5 with X.509 certificate authentication.
-
Create an Event Grid namespace with MQTT broker:
az eventgrid namespace create \ --name <namespace> \ --resource-group <rg> \ --topic-spaces-configuration "{state:'Enabled'}"
-
Generate certificates using the CertificateGenerator tool:
# Clone and run the certificate generator git clone https://github.com/howardginsburg/CertificateGenerator.git cd CertificateGenerator # Follow the README to generate a CA and client certificate # Use "factory-sensor-02" as the CN for the client cert (this becomes the deviceId)
-
Register the CA certificate in Event Grid:
az eventgrid namespace ca-certificate create \ --resource-group <rg> \ --namespace-name <namespace> \ --ca-certificate-name demo-ca \ --certificate "$(cat ca.pem | base64)"
-
Create a client (using the cert subject CN as the authentication name):
az eventgrid namespace client create \ --resource-group <rg> \ --namespace-name <namespace> \ --client-name factory-sensor-02 \ --authentication-name factory-sensor-02 \ --client-certificate-authentication "{validationScheme:'SubjectMatchesAuthenticationName'}"
-
Create a topic space that allows publishing:
az eventgrid namespace topic-space create \ --resource-group <rg> \ --namespace-name <namespace> \ --topic-space-name factory-telemetry \ --topic-templates "factory/sensors/#"
-
Create a permission binding for the client to publish:
az eventgrid namespace permission-binding create \ --resource-group <rg> \ --namespace-name <namespace> \ --permission-binding-name factory-publish \ --client-group-name '$all' \ --topic-space-name factory-telemetry \ --permission publisher
-
Route to Event Hubs: Configure Event Grid routing to forward MQTT messages to an Event Hub, then connect that Event Hub as a source in your Fabric eventstream.
-
Update
appsettings.json:Hostname: from the Event Grid namespace MQTT hostname- Copy
client.pem,client-key.pem, andca.peminto thecerts/directory
IoT Operations uses MQTT v5 with username/password authentication on the local broker. This setup creates an asset visible in the Operations Experience portal.
The default AIO broker only allows Kubernetes SAT authentication. To enable username/password for the demo, deploy a custom authentication server.
-
Deploy the username/password auth server from the Azure IoT Operations samples:
# Clone the samples repo git clone https://github.com/Azure-Samples/explore-iot-operations.git cd explore-iot-operations/samples/auth-server-user-pass-mqtt # Build and deploy to your cluster kubectl apply -f deploy/
-
Create a BrokerListener with a non-TLS port for the demo (or use TLS on port 8883):
apiVersion: mqttbroker.iotoperations.azure.com/v1 kind: BrokerListener metadata: name: demo-listener namespace: azure-iot-operations spec: brokerRef: default serviceName: aio-broker-demo serviceType: ClusterIP ports: - port: 1883 authenticationRef: demo-authn
-
Create a BrokerAuthentication resource pointing to the custom auth server:
apiVersion: mqttbroker.iotoperations.azure.com/v1 kind: BrokerAuthentication metadata: name: demo-authn namespace: azure-iot-operations spec: authenticationMethods: - method: Custom customSettings: endpoint: https://authn-server.azure-iot-operations.svc.cluster.local:443 caCertConfigMap: custom-auth-ca headers: Content-Type: application/json
-
Create a user in the auth server for the demo:
# This depends on your auth server implementation # The username becomes the deviceId in the telemetry payload
This is the key step that makes the asset appear in the AIO portal UI.
-
Open the Operations Experience portal at https://iotoperations.azure.com
-
Select your IoT Operations instance
-
Navigate to Assets → Create asset:
Field Value Asset name factory-sensor-03Description Industrial sensor — temperature, pressure, vibrationInbound endpoint Select your custom MQTT endpoint -
Create a dataset:
Field Value Dataset name telemetryDestination MQTTTopic azure-iot-operations/data/factory-sensor-03 -
Add data points to the dataset:
Data point name Data source temperature temperaturepressure pressurevibration vibration -
Save the asset. It now appears in the Operations Experience portal with status indicators.
-
In Operations Experience, go to Dataflows → Create dataflow
-
Source: Select Asset → choose
factory-sensor-03 -
Destination: Select Microsoft Fabric OneLake or Event Hubs endpoint
- If using Event Hubs: create a dataflow endpoint pointing to your Event Hub namespace
- The Event Hub feeds into your Fabric eventstream
-
Save the dataflow. Data published to
azure-iot-operations/data/factory-sensor-03will flow through to Fabric RTI.
If running the CLI on the cluster (e.g., via kubectl port-forward):
kubectl port-forward svc/aio-broker-demo 1883:1883 -n azure-iot-operationsUpdate appsettings.json:
Hostname:localhost(or the service DNS name if running in-cluster)Port:1883Username: the username you created in the auth serverPassword: the corresponding passwordTopic:azure-iot-operations/data/factory-sensor-03(must match the asset dataset topic)
When the sender publishes, the asset in the Operations Experience portal shows data flowing, and the dataflow routes it to Fabric RTI.
In your Fabric RTI eventhouse, run:
.create table IndustrialTelemetry (
deviceId: string,
timestamp: datetime,
source: string,
temperature: real,
pressure: real,
vibration: real
)Create an eventstream for each ingestion path (or a shared one if using a single Event Hub namespace):
- IoT Hub → Add IoT Hub as a source → route to
IndustrialTelemetrytable - Event Grid → Add the Event Hub (where Event Grid routes MQTT messages) as a source
- IoT Operations → Add the Event Hub (where the AIO dataflow sends data) as a source
// All telemetry from all sources
IndustrialTelemetry
| where timestamp > ago(5m)
| order by timestamp desc
// Compare sources side by side
IndustrialTelemetry
| where timestamp > ago(10m)
| summarize avg(temperature), avg(pressure), avg(vibration) by source, bin(timestamp, 30s)
| render timechart
// Detect vibration anomalies
IndustrialTelemetry
| where timestamp > ago(1h)
| where vibration > 0.10
| project timestamp, deviceId, source, vibration
| order by vibration descIoTDemo/
├── Program.cs # Spectre.Console dashboard + keypress handler
├── appsettings.json # Connection configuration
├── Models/
│ ├── AppSettings.cs # Strongly-typed config classes
│ └── TelemetryPayload.cs # JSON payload model
├── Services/
│ ├── ISender.cs # Sender interface
│ ├── TelemetryGenerator.cs # Random walk payload generator
│ ├── IoTHubSender.cs # MQTT v3.1.1 + SAS token
│ ├── EventGridSender.cs # MQTT v5 + X.509 cert
│ └── IoTOperationsSender.cs # MQTT v5 + username/password
└── certs/ # Client certs (gitignored)
MIT