Introduction
Modern industrial IoT demands real-time digital twin synchronization without traditional database overhead. This guide demonstrates building an event-driven Azure Digital Twins NDJSON solution using Azure Functions, Service Bus, and in-memory caching for lightning-fast performance and scalability.
Architecture Overview
Our solution leverages Azure Digital Twins for core twin management, Service Bus for enterprise messaging, Event Grid for real-time notifications, Azure Functions for serverless processing, and a Web API with MemoryCache for high-performance NDJSON data serving.
Step 1. Solution Setup.
dotnet new sln -n SmartFactoryTwins
dotnet new classlib -n TwinCore.Models
dotnet new functionsapp -n TwinProcessor.Functions --framework net8.0
dotnet new webapi -n TwinData.WebAPI --framework net8.0
dotnet new console -n TwinFactory.Generator --framework net8.0
Add references and packages.
dotnet sln add TwinCore.Models/TwinCore.Models.csproj
dotnet add TwinProcessor.Functions package Azure.DigitalTwins.Core
dotnet add TwinProcessor.Functions package Azure.Messaging.ServiceBus
dotnet add TwinData.WebAPI package Microsoft.Extensions.Caching.Memory
This creates a multi-project solution with enhanced naming for better organization. Each project handles specific responsibilities: models, event processing, API services, and twin generation.
Sample Output
![Sample Output]()
Step 2. Core Models Implementation.
This creates and uploads DTDL models defining the structure and properties of digital twins. Models establish the schema for industrial assets like sensors and motors.
// TwinUpdateRequest.cs in TwinCore.Models
namespace TwinCore.Models
{
public class TwinUpdateRequest
{
public string TwinId { get; set; } = string.Empty;
public Dictionary<string, object> Properties { get; set; } = new();
public DateTime LastUpdateTime { get; set; }
public string EventType { get; set; } = string.Empty;
}
}
// TwinDataModel.cs
namespace TwinCore.Models
{
public class TwinDataModel
{
public string Id { get; set; } = string.Empty;
public string ModelId { get; set; } = string.Empty;
public Dictionary<string, object> Properties { get; set; } = new();
public DateTime LastUpdateTime { get; set; }
}
}
These models define essential data contracts for twin updates and data representation. They ensure type safety and consistent data flow across all solution components.
Step 3. DTDL Model Creation.
// ModelUploader.cs in TwinFactory.Generator
public class ModelUploader
{
public async Task UploadIndustrialModels()
{
var models = new List<string>
{
GetTemperatureSensorModel(),
GetMotorModel()
};
var response = await _client.CreateModelsAsync(models);
Console.WriteLine($"Uploaded {response.Value.Count} models successfully");
}
private string GetTemperatureSensorModel()
{
return @"{
""@context"": ""dtmi:dtdl:context;3"",
""@id"": ""dtmi:factory:TemperatureSensor;1"",
""@type"": ""Interface"",
""displayName"": ""Temperature Sensor"",
""contents"": [
{ ""@type"": ""Property"", ""name"": ""name"", ""schema"": ""string"" },
{ ""@type"": ""Property"", ""name"": ""location"", ""schema"": ""string"" },
{ ""@type"": ""Property"", ""name"": ""temperature"", ""schema"": ""double"" },
{ ""@type"": ""Property"", ""name"": ""status"", ""schema"": ""string"" }
]
}";
}
// You may want to implement GetMotorModel here
}
Sample Output
![Sample Output]()
Step 4. Event Processing with Azure Functions.
This implements dual event processing using Event Grid for real-time updates and Service Bus for reliable message processing. The function ensures no twin updates are lost.
public class EventProcessor
{
private readonly DigitalTwinsClient _dtClient;
private readonly ServiceBusProcessor _serviceBus;
[Function("ProcessTwinEvents")]
public async Task ProcessEventGrid([EventGridTrigger] EventGridEvent gridEvent)
{
var twinId = ExtractTwinId(gridEvent.Subject);
var twin = await _dtClient.GetDigitalTwinAsync<BasicDigitalTwin>(twinId);
// Send to Service Bus for reliability
await _serviceBus.SendMessageAsync(new TwinUpdateRequest
{
TwinId = twinId,
Properties = twin.Value.Contents,
LastUpdateTime = DateTime.UtcNow,
EventType = gridEvent.EventType
});
// Direct API notification for real-time updates
await NotifyWebAPI(updateRequest);
}
[Function("ProcessServiceBusMessages")]
public async Task ProcessServiceBus(
[ServiceBusTrigger("twin-updates")] ServiceBusReceivedMessage message)
{
var updateRequest = JsonSerializer.Deserialize<TwinUpdateRequest>(message.Body);
await ProcessTwinUpdate(updateRequest);
}
}
Sample output
![Sample output]()
Step 5. High-Performance Web API.
The Web API provides NDJSON endpoints with in-memory caching for ultra-fast data retrieval. Cache updates ensure real-time synchronization with Azure Digital Twins.
[ApiController]
[Route("api/[controller]")]
public class TwinsController : ControllerBase
{
private readonly IMemoryCache _cache;
private const string CACHE_KEY = "factory_twins";
[HttpGet("ndjson")]
public async Task<IActionResult> GetNDJSON()
{
var twins = await GetCachedTwins();
var ndjsonBuilder = new StringBuilder();
foreach (var twin in twins.Values)
{
ndjsonBuilder.AppendLine(JsonSerializer.Serialize(twin));
}
return Content(ndjsonBuilder.ToString(), "application/x-ndjson");
}
[HttpPost("cache/update")]
public async Task<IActionResult> UpdateCache([FromBody] TwinUpdateRequest request)
{
var twins = await GetCachedTwins();
twins[request.TwinId] = new TwinDataModel
{
Id = request.TwinId,
Properties = request.Properties,
LastUpdateTime = request.LastUpdateTime
};
_cache.Set(CACHE_KEY, twins, TimeSpan.FromHours(1));
return Ok(new { Success = true, TwinId = request.TwinId });
}
}
Sample output
![Sample output]()
Step 6. Twin Factory Generator.
This automated generator creates 600 industrial twins for testing and demonstration. It supports multiple twin types with realistic property values.
public class TwinFactory
{
public async Task CreateIndustrialTwins()
{
var twinTypes = new[] { "Motor", "TemperatureSensor", "PressureSensor" };
for (int i = 1; i <= 600; i++)
{
var twinType = twinTypes[i % twinTypes.Length];
var twinId = $"{twinType}-{i:D3}";
var twin = new BasicDigitalTwin
{
Metadata = { ModelId = $"dtmi:factory:{twinType};1" },
Contents = GenerateProperties(twinType)
};
await _client.CreateOrReplaceDigitalTwinAsync(twinId, twin);
if (i % 100 == 0)
Console.WriteLine($"Created {i} twins...");
}
}
}
Sample output
![Sample output]()
Step 7. Azure Infrastructure Setup.
This PowerShell script deploys the complete Azure infrastructure, including Digital Twins, Service Bus, Function Apps, and Web API, with proper naming conventions.
# deploy-smart-factory.ps1
param (
[string] $ResourceGroup,
[string] $Location,
[string] $FactoryName
)
Create core resources
az group create --name $ResourceGroup --location $Location
az dt create --dt-name $FactoryName --resource-group $ResourceGroup
Set up Service Bus
$serviceBusNamespace = "$FactoryName-messaging"
az servicebus namespace create `
--name $serviceBusNamespace `
--resource-group $ResourceGroup `
--sku Standard
az servicebus queue create `
--name "twin-updates" `
--namespace-name $serviceBusNamespace `
--resource-group $ResourceGroup
Deploy Function App and Web API
az functionapp create \
--name "$FactoryName-processor" \
--resource-group $ResourceGroup \
--runtime dotnet
az webapp create \
--name "$FactoryName-api" \
--resource-group $ResourceGroup \
--runtime "DOTNETCORE|8.0"
Sample Output
![Sample Output]()
Performance Benefits
This architecture delivers sub-second twin synchronization, horizontal scalability through Azure Functions, and cost-effective serverless operations. The dual processing approach ensures both real-time performance and enterprise-grade reliability with Service Bus messaging.
Conclusion
This event-driven Azure Digital Twins solution demonstrates modern cloud-native patterns for industrial IoT at scale. The combination of in-memory caching, Service Bus messaging, and serverless computing provides a robust foundation for real-time digital twin management in manufacturing environments.