Best Practices with Azure Components

Mastering Async Programming in .NET

Merwan Chinta
CodeNx
Published in
5 min readMay 15, 2024

--

Async programming is a cornerstone of modern .NET development, enabling efficient and responsive applications. When combined with Azure’s robust cloud services, we can handle complex real-world problems with elegance and efficiency.

In this article, we’ll dive deep into async programming best practices, focusing on a use case of a toll road system where vehicles are charged for passing through toll booths.

We’ll use Azure components to build this system and walk through the entire architecture with detailed code examples and explanations.

The Challenge: Real-Time Toll Collection System

Imagine a toll road system where thousands of vehicles pass through toll booths every minute. Our objectives are to:

  1. Detect vehicles passing through toll booths.
  2. Charge vehicles based on their toll plans.
  3. Store transaction data for audit and analysis.
  4. Provide an API for clients to retrieve their transaction history.

This use case involves high throughput and requires low-latency processing to ensure timely charging and data storage.

We’ll use Azure services and async programming in .NET to tackle these requirements.

Toll Collection System with Azure Components — Image source: Created by Author

Azure Components Used

  1. Azure IoT Hub: Manages communication between toll booth sensors and the cloud.
  2. Azure Event Hubs: Handles large streams of toll events.
  3. Azure Cosmos DB: Stores transaction data.
  4. Azure API Management: Provides a secure API for client access.

Step-by-Step Approach

1. Detecting Vehicles with Azure IoT Hub

Azure IoT Hub will be used to receive data from toll booth sensors. Each sensor sends a message when a vehicle passes through.

Types of Sensors

  • RFID Sensors: These sensors read RFID tags attached to vehicles. Each tag has a unique identifier that can be used as the vehicle ID.
  • License Plate Recognition Cameras: These cameras use OCR (Optical Character Recognition) to read license plates and derive vehicle IDs.
  • Bluetooth Beacons: These beacons interact with Bluetooth devices in vehicles to identify them.

Here’s an example using an RFID sensor:

// IoT Hub connection string
string connectionString = "<Your IoT Hub Connection String>";

// Create a device client
var deviceClient = DeviceClient.CreateFromConnectionString(connectionString, TransportType.Mqtt);

// Async method to send vehicle detection message
public async Task SendVehicleDetectionAsync(string vehicleId)
{
var message = new Message(Encoding.ASCII.GetBytes(vehicleId));
await deviceClient.SendEventAsync(message);
}

In this code, the RFID sensor reads a vehicle’s RFID tag and sends the vehicle ID to Azure IoT Hub.

2. Handling High Throughput with Azure Event Hubs

For high-volume scenarios, Azure Event Hubs ensures reliable data ingestion. Azure Event Hubs is preferred over Azure Functions when handling large streams of data because it can handle millions of events per second and provides reliable data retention and partitioning.

How Event Hubs Manages High Throughput

Azure Event Hubs stores incoming events temporarily and processes them in batches. This buffering mechanism allows for high throughput and efficient processing. Here’s how it works:

  • Partitioning: Event Hubs splits the data stream into partitions. Each partition acts as a sequential log of events, enabling parallel processing.
  • Checkpointing: This mechanism keeps track of the last successfully processed event, ensuring that processing can resume from where it left off in case of failure.
  • Batch Processing: Events are sent to consumers in batches, which improves processing efficiency and reduces latency.

Processing Events with Azure Event Hubs

public static class EventHubProcessing
{
private static Container container = CosmosDBHelper.GetCosmosContainer();

[FunctionName("EventHubProcessing")]
public static async Task Run([EventHubTrigger("tollhub", Connection = "EventHubConnection")] EventData[] events, ILogger log)
{
var exceptions = new List<Exception>();

foreach (EventData eventData in events)
{
try
{
string vehicleId = Encoding.UTF8.GetString(eventData.Body.Array);
var charge = await CalculateTollChargeAsync(vehicleId);

var transaction = new TollTransaction
{
Id = Guid.NewGuid().ToString(),
VehicleId = vehicleId,
Charge = charge,
Timestamp = DateTime.UtcNow
};

await SaveToCosmosDBAsync(transaction);
}
catch (Exception e)
{
exceptions.Add(e);
}
}

if (exceptions.Count > 0)
{
throw new AggregateException(exceptions);
}
}

private static async Task<decimal> CalculateTollChargeAsync(string vehicleId)
{
// Implement toll calculation logic
// For example, return a fixed charge for simplicity
return await Task.FromResult(5.00m);
}

private static async Task SaveToCosmosDBAsync(TollTransaction transaction)
{
await container.CreateItemAsync(transaction, new PartitionKey(transaction.VehicleId));
}
}

Explanation:

  1. EventHubTrigger Attribute: Binds the function to Azure Event Hub.
  2. foreach Loop: Iterates through each event (vehicle detection) in the batch.
  3. Encoding.UTF8.GetString: Decodes the vehicle ID from the event data.
  4. CalculateTollChargeAsync: Calculates the toll charge for the vehicle. This can be based on various factors like vehicle type, time of day, etc.
  5. SaveToCosmosDBAsync: Saves the transaction to Azure Cosmos DB.

3. Storing Data with Azure Cosmos DB

Azure Cosmos DB provides scalable, low-latency data storage, perfect for storing transaction records.

public static class CosmosDBHelper
{
private static readonly string EndpointUri = "<Your Cosmos DB URI>";
private static readonly string PrimaryKey = "<Your Cosmos DB Primary Key>";
private static CosmosClient cosmosClient = new CosmosClient(EndpointUri, PrimaryKey);
private static Database database = cosmosClient.GetDatabase("TollData");
private static Container container = database.GetContainer("Transactions");

public static async Task SaveToCosmosDBAsync(TollTransaction transaction)
{
await container.CreateItemAsync(transaction, new PartitionKey(transaction.VehicleId));
}

public class TollTransaction
{
public string Id { get; set; }
public string VehicleId { get; set; }
public decimal Charge { get; set; }
public DateTime Timestamp { get; set; }
}
}

Explanation:

  1. CosmosClient Initialization: Connects to the Cosmos DB account using the endpoint URI and primary key.
  2. SaveToCosmosDBAsync: Saves the transaction to the Cosmos DB container.
  3. TollTransaction Class: Represents a toll transaction with properties for the transaction ID, vehicle ID, charge amount, and timestamp.

4. Exposing Data via Azure API Management

To allow clients to query their transaction history, we’ll expose an API using Azure API Management.

public static class TollTransactionApi
{
private static Container container = CosmosDBHelper.GetCosmosContainer();

[FunctionName("GetTransaction")]
public static async Task<IActionResult> GetTransaction([HttpTrigger(AuthorizationLevel.Function, "get", Route = "transaction/{id}")] HttpRequest req, ILogger log, string id)
{
try
{
var response = await container.ReadItemAsync<TollTransaction>(id, new PartitionKey(id));
return new OkObjectResult(response.Resource);
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return new NotFoundResult();
}
}

public class TollTransaction
{
public string Id { get; set; }
public string VehicleId { get; set; }
public decimal Charge { get; set; }
public DateTime Timestamp { get; set; }
}
}

Explanation:

  1. HttpTrigger Attribute: Defines an HTTP-triggered Azure Function for API endpoints.
  2. container.ReadItemAsync: Fetches the transaction from Cosmos DB using the transaction ID.
  3. OkObjectResult: Returns the transaction details if found.
  4. NotFoundResult: Returns a 404 status code if the transaction is not found.

Best Practices for Async Programming in .NET

Avoid Blocking Calls: Use async and await for all I/O-bound operations. Avoid using .Result or .Wait() as they block the calling thread.

Cancellation Tokens: Use CancellationToken to cancel async operations when necessary.

Exception Handling: Always handle exceptions in async methods. Use try-catch blocks and log errors for diagnosis.

Task Combinators: Use Task.WhenAll and Task.WhenAny for running multiple async operations concurrently.

ConfigureAwait(false): Use ConfigureAwait(false) to avoid deadlocks in library code where context capturing is not needed.

Conclusion

By leveraging async programming in .NET and Azure services, we can create a robust toll collection system capable of handling high throughput and low latency requirements.

This example demonstrates the power of combining async programming with cloud services to build scalable and efficient applications. Mastering these techniques is essential for developing modern, high-performance applications.

Stay tuned for more in-depth articles on advanced .NET programming and cloud architecture best practices!

I trust this information has been valuable to you. 🌟 Wishing you an enjoyable and enriching learning journey!

📚 For more insights like these, feel free to 👏 follow 👉 Merwan Chinta

--

--

Merwan Chinta
CodeNx

🚧 Roadblock Eliminator & Learning Advocate 🖥️ Software Architect 🚀 Efficiency & Performance Guide 🌐 Cloud Tech Specialist