Course: Azure Developer Associate (AZ-204) – Building Connected and Distributed Systems
Module 1 – Azure API Management (APIM)
Why Use API Management?
- Centralized gateway: security, access control, monitoring.
- API lifecycle management: creation, deployment, versioning, retirement.
- Cloud-native scalability: high availability during traffic spikes.
- Collaboration: developer portal, documentation, onboarding for partners/clients.
Architecture and Pricing Tiers
| Tier | Usage | SLA | Features |
|---|---|---|---|
| Consumption | Serverless, lightweight, internal APIs. Pay-per-use. | Limited | Reduced features |
| Developer | Dev/Test only. Low fixed price. | ❌ No SLA | Almost all features |
| Premium | Production. HA, multi-region. | ✅ Guaranteed | All features + VNet, scale-out |
APIM Components
- Gateway: receives API requests, applies policies, forwards to backends.
- Management API: configuration management via portal, CLI, ARM.
- Developer Portal: auto-generated portal for developers/partners. Customizable (WYSIWYG).
Importing and Publishing an API
- Supports OpenAPI (Swagger), WSDL, WADL, and other formats.
- Each operation has a configurable suffix URL.
- Can test directly in the portal with detailed tracing.
Products and Subscriptions
- A Product groups one or more APIs.
- Each Subscription is associated with a Product and generates 2 subscription keys (for rotation).
- Clients include the key in requests (header
Ocp-Apim-Subscription-Key). - Reason for 2 keys: allows rotation without service interruption.
Securing APIs
Subscription Keys
- Simple, lightweight. Per-client control. Quotas and rate limiting per key.
- Limitations: no granular role/identity control.
OAuth 2.0 with Entra ID
- Register an app in Entra ID → note the
clientIdandclientSecret. - Configure a
redirectUri(e.g.https://localhostfor testing). - Expose the API in Entra ID: define the Application ID URI.
- Configure APIM: add JWT validation in policies.
- Client Credentials Flow: obtain a token via Postman with tenant ID, client ID, secret.
- Include the
Bearer tokenin API requests.
Combination: Subscription Key + OAuth
- Multi-layer security: the key identifies the consumer, the token authenticates the identity.
RBAC
- Controls who can create/modify/delete APIs in APIM.
- Integrated with Entra ID for centralized access management.
Policies
- Allow modifying API behavior without touching the backend code.
- Scopes (application levels):
- All APIs: applies to all APIs in the instance.
- Product: applies to all APIs in a Product.
- API: applies to a specific API.
- Operation: applies to a specific operation.
Policy Types
- Inbound: processing before the backend call (auth, transformation, validation).
- Backend: managing the connection to the backend.
- Outbound: response processing (transformation, logging).
Common Policies
| Policy | Description |
|---|---|
rate-limit | Limits the number of calls (e.g.: 5 calls/minute per subscription key) |
cors | Enables cross-origin requests for browser apps |
rewrite-uri | Rewrites the request URL |
set-header | Adds/modifies headers |
validate-jwt | Validates an OAuth JWT token |
cache-lookup / cache-store | Response caching |
XML Policy Format
<policies>
<inbound>
<rate-limit-by-key calls="5" renewal-period="60" counter-key="@(context.Subscription?.Key ?? "anonymous")" />
<cors>
<allowed-origins><origin>*</origin></allowed-origins>
<allowed-methods><method>GET</method></allowed-methods>
</cors>
<base />
</inbound>
<backend><base /></backend>
<outbound><base /></outbound>
<on-error><base /></on-error>
</policies>
Developer Portal
- Auto-generated web portal, fully customizable (CSS, fonts, menus).
- WYSIWYG editing with widgets/sections.
- Developers see published APIs, Products, methods, and can test them.
- Publish from admin mode → available to anonymous or registered users.
Module 2 – Azure Event Grid and Event Hubs (Event-driven Solutions)
Event-Driven Architecture (EDA)
- Event producers → Brokers → Event consumers.
- Asynchronous communication: the producer continues without waiting for a response.
- Benefits: natural scalability, loose coupling, real-time reactivity.
Azure Event Grid
- Discrete event routing (state change notifications).
- Architecture: Topics → Subscriptions → Event Handlers.
Topics: System vs Custom
| Type | Description |
|---|---|
| System Topics | Automatically created by Azure for its resources (Storage, Resource Groups, etc.) |
| Custom Topics | Manually created for your application events |
Supported Event Handlers
- Azure Functions, Logic Apps, Event Hubs, Service Bus, Webhooks, Storage Queues.
Creating and Sending an Event (CLI)
# Retrieve the topic endpoint and key
endpoint=$(az eventgrid topic show --name <topic> -g <rg> --query "endpoint" -o tsv)
key=$(az eventgrid topic key list --name <topic> -g <rg> --query "key1" -o tsv)
# Send an event
curl -X POST -H "aeg-sas-key: $key" -H "Content-Type: application/json" \
-d '[{"id":"1","subject":"test","eventType":"testEvent","dataVersion":"1.0","data":{"message":"Hello"}}]' \
$endpoint
Triggering an Azure Function from Event Grid (local with ngrok)
- Develop the function locally (debug mode, port 7172).
- Use ngrok to create a public tunnel:
ngrok http 7172. - Copy the ngrok URL + the function path as the Event Handler endpoint.
- Event Grid validates the endpoint via a validation handshake.
- Limitation: the ngrok URL is only valid while the ngrok window remains open.
Azure Event Hubs
- High-performance stream ingestion (streaming).
- Use cases: IoT telemetry, centralized logs, real-time analytics.
Event Hubs Architecture
Producers → Event Hubs Namespace → Event Hub → Partitions → Consumer Groups
- Namespace: secure container for one or more Event Hubs (create them separately).
- Partitions: divide the stream into parallel lanes → scalability.
- Consumer Groups: independent stream views for multiple consumers in parallel.
- Retention: events kept for a configurable duration → consumers can replay.
Event Grid vs Event Hubs
| Characteristic | Event Grid | Event Hubs |
|---|---|---|
| Use case | Discrete events/notifications | Continuous massive data stream |
| Throughput | Moderate | Very high (millions of events/s) |
| Persistence | Very short | Configurable (days/weeks) |
| Ordering | Not guaranteed | Ordered by partition |
| Analogy | Letter carrier (targeted delivery) | Data river (continuous stream) |
Sending Events to Event Hubs (C#)
var connectionString = "<connection-string>";
var eventHubName = "<hub-name>";
await using var producer = new EventHubProducerClient(connectionString, eventHubName);
using var eventBatch = await producer.CreateBatchAsync();
for (int i = 1; i <= 10; i++)
{
var eventData = new EventData(Encoding.UTF8.GetBytes($"Event #{i}"));
eventBatch.TryAdd(eventData);
}
await producer.SendAsync(eventBatch);
Consuming from Event Hubs (C#)
// EventProcessorClient requires a blob container for checkpoints
var storageClient = new BlobContainerClient(storageConnectionString, containerName);
var processor = new EventProcessorClient(storageClient, EventHubConsumerClient.DefaultConsumerGroupName,
connectionString, eventHubName);
processor.ProcessEventAsync += async (args) =>
{
Console.WriteLine(Encoding.UTF8.GetString(args.Data.Body.ToArray()));
await args.UpdateCheckpointAsync(); // Save the read position
};
processor.ProcessErrorAsync += (args) =>
{
Console.WriteLine(args.Exception.Message);
return Task.CompletedTask;
};
await processor.StartProcessingAsync();
Console.ReadKey();
await processor.StopProcessingAsync();
Checkpoints
- Save the read position in the stream for each partition.
- Stored in Blob Storage via
EventProcessorClient. - Allow resuming reading after a stop without reprocessing already-processed messages.
- Consumer Groups allow multiple consumers to read the same stream independently.
EDA Best Practices
- Clear event schemas: include source, type, timestamp. Plan for backward compatibility.
- Idempotency: repeated processing without creating duplicates.
- Dead-letter: queue for events not processed after multiple attempts.
- Partitioning: enables parallel processing.
- Backpressure: avoid overwhelming consumers.
- Azure Monitor: alerts on metrics (lag, error rate).
Module 3 – Azure Service Bus and Queue Storage (Messaging)
Messaging vs Eventing Difference
- Eventing: producer does not wait for a response, is not responsible for processing.
- Messaging: the message contains a command expecting an action. The producer may require a response.
Azure Service Bus vs Azure Storage Queues
| Characteristic | Service Bus | Storage Queues |
|---|---|---|
| Protocols | AMQP, HTTPS | HTTPS only |
| Max message size | 256 KB (Standard), 100 MB (Premium) | 64 KB |
| Retention duration | 14 days max | 7 days max |
| Ordering | FIFO with sessions | Not guaranteed |
| Duplicate detection | ✅ Built-in | ❌ No |
| Topics/Subscriptions | ✅ Publish-subscribe | ❌ No |
| Dead-letter queue | ✅ Automatic | ❌ No |
| Sessions | ✅ | ❌ |
| Transactions | ✅ | ❌ |
| Price | Higher | Low |
| Use case | Enterprise, complex workflows | Simple decoupling, high non-critical volume |
Service Bus – Queues vs Topics
- Queue: point-to-point (1 producer → 1 consumer). FIFO. Message consumed once.
- Topic + Subscriptions: publish-subscribe (1 producer → N consumers). Each subscription receives a copy. Configurable filters per subscription.
Messaging Patterns
| Pattern | Service Bus | Description |
|---|---|---|
| Command (point-to-point) | Queue | One message → one recipient |
| Publish-Subscribe | Topic + Subscriptions | One message → N subscribers |
| Request-Reply | Correlation IDs | Simulates synchronicity |
Sending/Receiving with Service Bus (C#)
// Connection (Do NOT use Master Key in production → prefer Entra ID)
var client = new ServiceBusClient(connectionString);
// Send a message
var sender = client.CreateSender("demoqueue");
await sender.SendMessageAsync(new ServiceBusMessage("Hello World"));
// Receive messages
var receiver = client.CreateReceiver("demoqueue");
var message = await receiver.ReceiveMessageAsync();
Console.WriteLine(message.Body);
await receiver.CompleteMessageAsync(message); // Confirm receipt
Sessions (FIFO Messaging)
- Group related messages with the same
SessionId. - Messages in the same group processed in order by a single consumer.
- Must be enabled when creating the queue/subscription.
Session Use Cases
- Sequential steps of a workflow (payment → stock allocation → shipping).
- Financial transactions per user.
- IoT: data from the same device in order.
- Multi-step approval processes.
// Send with SessionId
var message = new ServiceBusMessage("Step 1") { SessionId = "order-123" };
await sender.SendMessageAsync(message);
// Receive a specific session
var sessionReceiver = await client.AcceptSessionAsync("sessionqueue", "order-123");
var msg = await sessionReceiver.ReceiveMessageAsync();
Dead-Letter Queue (DLQ)
- Logical sub-queue automatically attached to each queue/subscription.
- Receives messages that cannot be processed (after N attempts, expired, or explicitly rejected).
- Access path:
<queue-name>/$deadletterqueue. - DLQ messages retain all their properties + error metadata.
- MaxDeliveryCount: max number of attempts before DLQ (default: 10, configurable).
- Note: producers CANNOT write directly to the DLQ.
// Explicitly move a message to the DLQ with reason
await receiver.DeadLetterMessageAsync(message,
deadLetterReason: "ValidationFailed",
deadLetterErrorDescription: "The 'email' field is invalid");
// Access the DLQ for inspection/processing
var dlqReceiver = client.CreateReceiver("demoqueue",
new ServiceBusReceiverOptions { SubQueue = SubQueue.DeadLetter });
Publish-Subscribe with Filters
// Create filtering rules on a subscription
await adminClient.CreateRuleAsync("demotopic", "sub1", new CreateRuleOptions
{
Name = "OddFilter",
Filter = new SqlRuleFilter("Category = 'Odd'")
});
// Send a message with custom property
var message = new ServiceBusMessage("Data") { ApplicationProperties = { ["Category"] = "Odd" } };
Azure Storage Queues
- Lightweight REST-based solution.
- Use cases: simple decoupling (e.g.: offloading background processing to Azure Functions/WebJobs).
- No topics, sessions, or duplicate detection.
var queueClient = new QueueClient(connectionString, "demoqueue");
await queueClient.CreateIfNotExistsAsync(); // Idempotent
// Send
await queueClient.SendMessageAsync("Hello World");
// Receive and process
var messages = await queueClient.ReceiveMessagesAsync(maxMessages: 10);
foreach (var msg in messages.Value)
{
Console.WriteLine(msg.MessageText);
await queueClient.DeleteMessageAsync(msg.MessageId, msg.PopReceipt);
}
Messaging Security
- SAS (Shared Access Signature): time-limited access (read/write/delete). Use for third-party delegation.
- RBAC with Entra ID: recommended for Azure applications.
Azure Service Bus Data Sender: send messages.Azure Service Bus Data Receiver: receive messages.
- Encryption: at-rest automatically by Microsoft. Option for customer-managed keys.
- TLS: all communications encrypted in transit.
Key Points for the Exam
APIM
- Policies = correct answer for auth headers and transformations (not Products, Named Values, or Backends).
- 3 policy levels: All APIs → Products → APIs (cumulative inheritance).
- Subscription Key + OAuth = multi-layer security.
Event Grid vs Event Hubs vs Service Bus
- Event Grid: discrete events, low volume, reactivity (resource changes, user actions).
- Event Hubs: IoT telemetry, logs, millions of events/s, high-performance streaming.
- Service Bus: enterprise workflows, FIFO, sessions, transactions, dead-letter queue.
Common Trick Questions
- Azure Notification Hubs = push notifications to mobile apps (not for IoT telemetry).
- For FIFO with subsets (by user, by transaction) → Service Bus sessions.
- To filter messages by properties → Subscription Filters (not ForwardTo, Sessions, or DLQ).
- Event Hubs checkpoints → stored in Blob Storage via
EventProcessorClient.
Module 5 – Azure Service Bus: Deep Dive
Namespaces and Service Tiers
A namespace is the root container for all Service Bus entities (queues, topics, relays). It has a unique DNS name (<namespace>.servicebus.windows.net).
| Tier | Max message size | Partitioning | Availability zones | Use case |
|---|---|---|---|---|
| Basic | 256 KB | ❌ | ❌ | Simple queues, dev/test |
| Standard | 256 KB | ✅ | ❌ | Standard production |
| Premium | 100 MB | ✅ | ✅ | High perf, network isolation, VNET |
# Create a Premium namespace with CLI
az servicebus namespace create \
--resource-group myRG \
--name myNamespace \
--sku Premium \
--location eastus
Queues vs Topics / Subscriptions
flowchart LR
P[Producer] -->|SendMessageAsync| Q["(Queue)"]
Q -->|ReceiveMessageAsync| C[Single consumer]
P2[Producer] -->|SendMessageAsync| T["(Topic)"]
T --> S1["(Sub 1\nSQL filter)"]
T --> S2["(Sub 2\nCorrelation filter)"]
T --> S3["(Sub 3\nNo filter)"]
S1 --> C1[Consumer A]
S2 --> C2[Consumer B]
S3 --> C3[Consumer C]
Subscription Filters
| Type | Syntax | Performance | Usage |
|---|---|---|---|
| SQL Filter | Category = 'Electronics' AND Price > 100 | Moderate | Complex conditions on properties |
| Correlation Filter | { CorrelationId = "abc", Label = "order" } | Very fast | Exact match (recommended) |
| True Filter | (default) | Maximum | Receive all messages |
// Create a SQL filter on a subscription
var adminClient = new ServiceBusAdministrationClient(connectionString);
// Remove the default filter ($Default = True)
await adminClient.DeleteRuleAsync("demotopic", "sub1", "$Default");
// Add a SQL filter
await adminClient.CreateRuleAsync("demotopic", "sub1", new CreateRuleOptions
{
Name = "HighPrice",
Filter = new SqlRuleFilter("Category = 'Electronics' AND Price > 100"),
Action = new SqlRuleAction("SET Label = 'premium'") // Optional action
});
// Add a correlation filter (more performant)
await adminClient.CreateRuleAsync("demotopic", "sub2", new CreateRuleOptions
{
Name = "CorrelFilter",
Filter = new CorrelationRuleFilter
{
CorrelationId = "urgent-order",
ApplicationProperties = { ["Priority"] = "High" }
}
});
Dead-Letter Queue (DLQ) — Deep Dive
flowchart TD
M[Incoming message] --> Q["(Active Queue)"]
Q -->|Attempt 1..N| P[Processor]
P -->|CompleteAsync| END[✅ Processed]
P -->|AbandonAsync| Q
P -->|DeadLetterAsync| DLQ["(Dead-Letter Queue\nqueue/$deadletterqueue)"]
Q -->|MaxDeliveryCount exceeded| DLQ
Q -->|TTL expired| DLQ
DLQ --> DIAG[🔍 Diagnostic / Replay]
Properties automatically added to DLQ:
| Property | Description |
|---|---|
DeadLetterReason | Cause of dead-lettering (MaxDeliveryCountExceeded, TTLExpiredException, custom) |
DeadLetterErrorDescription | Detailed description provided by the consumer |
EnqueuedTimeUtc | Original enqueue timestamp |
DeliveryCount | Number of delivery attempts made |
// Read and reprocess from DLQ
var dlqReceiver = client.CreateReceiver(
"demoqueue",
new ServiceBusReceiverOptions { SubQueue = SubQueue.DeadLetter });
ServiceBusReceivedMessage dlqMsg = await dlqReceiver.ReceiveMessageAsync();
Console.WriteLine($"DLQ Reason: {dlqMsg.DeadLetterReason}");
Console.WriteLine($"Description: {dlqMsg.DeadLetterErrorDescription}");
Console.WriteLine($"Body: {dlqMsg.Body}");
// Optional: re-send the corrected message
var sender = client.CreateSender("demoqueue");
await sender.SendMessageAsync(new ServiceBusMessage(dlqMsg.Body));
await dlqReceiver.CompleteMessageAsync(dlqMsg);
Sessions — Guaranteed FIFO Order
Sessions enable strict FIFO processing within a group of messages identified by a SessionId. Only one consumer holds the session lock at a time.
sequenceDiagram
participant P as Producer
participant Q as Queue (sessions ON)
participant C1 as Consumer 1
participant C2 as Consumer 2
P->>Q: msg{SessionId="user-A", seq=1}
P->>Q: msg{SessionId="user-B", seq=1}
P->>Q: msg{SessionId="user-A", seq=2}
P->>Q: msg{SessionId="user-A", seq=3}
Q-->>C1: AcceptSession("user-A") → locked
C1->>C1: Processes seq=1, seq=2, seq=3 in order
Q-->>C2: AcceptSession("user-B")
C2->>C2: Processes seq=1
// Send messages with SessionId
var sender = client.CreateSender("sessionqueue");
for (int i = 1; i <= 5; i++)
{
await sender.SendMessageAsync(new ServiceBusMessage($"Step {i}")
{
SessionId = "order-456",
Subject = $"step-{i}"
});
}
// Receive a session (blocks until available)
ServiceBusSessionReceiver sessionReceiver =
await client.AcceptSessionAsync("sessionqueue", "order-456");
while (true)
{
ServiceBusReceivedMessage msg = await sessionReceiver.ReceiveMessageAsync(TimeSpan.FromSeconds(5));
if (msg == null) break;
Console.WriteLine($"Session {msg.SessionId}: {msg.Body}");
await sessionReceiver.CompleteMessageAsync(msg);
}
// Session state (contextual storage)
await sessionReceiver.SetSessionStateAsync(BinaryData.FromString("progress=3/5"));
BinaryData state = await sessionReceiver.GetSessionStateAsync();
Duplicate Detection
- Enabled at queue/topic creation time (not modifiable afterward).
- Configurable window: 20 seconds to 7 days (default: 10 minutes).
- Service Bus stores the
MessageIdwithin the window → silently rejects duplicates.
// Create a queue with duplicate detection
await adminClient.CreateQueueAsync(new CreateQueueOptions("my-queue")
{
RequiresDuplicateDetection = true,
DuplicateDetectionHistoryTimeWindow = TimeSpan.FromMinutes(30)
});
// Send with a stable MessageId (idempotent)
await sender.SendMessageAsync(new ServiceBusMessage("payload")
{
MessageId = "order-789-retry-2" // Same ID → ignored if already received
});
Message Deferral
A consumer can defer a message it is not ready to process yet, without rejecting it. The message remains in the queue but is no longer delivered automatically — it must be recalled by its SequenceNumber.
// Defer a message
long seqNumber = message.SequenceNumber;
await receiver.DeferMessageAsync(message);
// Later: retrieve the deferred message by sequence number
ServiceBusReceivedMessage deferred = await receiver.ReceiveDeferredMessageAsync(seqNumber);
await receiver.CompleteMessageAsync(deferred);
Scheduled Messages
// Schedule delivery in 2 hours
DateTimeOffset scheduledTime = DateTimeOffset.UtcNow.AddHours(2);
long seqNumber = await sender.ScheduleMessageAsync(
new ServiceBusMessage("Appointment reminder"),
scheduledTime);
// Cancel a scheduled message
await sender.CancelScheduledMessageAsync(seqNumber);
Service Bus Transactions
Multiple operations can be grouped in an atomic transaction (all or nothing). This works within the same namespace.
// Example: move a message from queue A to queue B in a transaction
var receiverA = client.CreateReceiver("queueA");
var senderB = client.CreateSender("queueB");
ServiceBusReceivedMessage msgA = await receiverA.ReceiveMessageAsync();
using ServiceBusTransactionContext txContext = await client.CreateTransactionAsync();
await senderB.SendMessageAsync(new ServiceBusMessage(msgA.Body), txContext);
await receiverA.CompleteMessageAsync(msgA, txContext);
await txContext.CommitAsync(); // or RollbackAsync() on error
Module 6 – Azure Event Grid: Deep Dive
System vs Custom Topics
flowchart LR
subgraph Azure Platform
SA[Storage Account] -->|BlobCreated\nBlobDeleted| ST[System Topic]
RG[Resource Group] -->|ResourceWritten| ST2[System Topic]
end
subgraph Your application
APP[.NET App] -->|PublishEventsAsync| CT[Custom Topic]
end
ST --> SUB1[Subscription → Azure Function]
ST2 --> SUB2[Subscription → Logic App]
CT --> SUB3[Subscription → Event Hub]
CT --> SUB4[Subscription → Webhook]
| Characteristic | System Topic | Custom Topic |
|---|---|---|
| Creation | Automatic by Azure | Manual |
| Source | Azure resources (Storage, ACR, IoT Hub…) | Your application |
| Management | Microsoft manages lifecycle | You manage |
| Endpoint | Not visible | URL + SAS key exposed |
Event Schemas
Event Grid Schema (native Azure)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "Microsoft.Storage.BlobCreated",
"subject": "/blobServices/default/containers/images/blobs/photo.jpg",
"eventTime": "2024-03-15T10:30:00Z",
"dataVersion": "1.0",
"data": {
"api": "PutBlockList",
"url": "https://storage.blob.core.windows.net/images/photo.jpg",
"contentType": "image/jpeg"
}
}
CloudEvents Schema (recommended CNCF standard)
{
"specversion": "1.0",
"type": "com.example.order.created",
"source": "/myapp/orders",
"id": "A234-1234-1234",
"time": "2024-03-15T10:30:00Z",
"datacontenttype": "application/json",
"data": {
"orderId": "ORD-001",
"amount": 149.99
}
}
| Criterion | Event Grid Schema | CloudEvents |
|---|---|---|
| Standard | Azure proprietary | CNCF (industry) |
| Portability | Azure only | Multi-cloud |
| Recommendation | Legacy | New development |
Subscription Filters
flowchart TD
T[Custom Topic] --> F{Subscription filters}
F -->|subject beginsWith /orders| S1[Sub Orders]
F -->|eventType = OrderShipped| S2[Sub Shipments]
F -->|advanced: data.amount > 1000| S3[Sub Large orders]
S1 --> FN1[Azure Function]
S2 --> FN2[Logic App]
S3 --> FN3[Webhook Alerting]
// Create a subscription with advanced filters
var eventGridClient = new EventGridManagementClient(credential);
await eventGridClient.EventSubscriptions.CreateOrUpdateAsync(
scope: "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.EventGrid/topics/myTopic",
eventSubscriptionName: "order-subscription",
eventSubscriptionInfo: new EventSubscription
{
Destination = new WebHookEventSubscriptionDestination
{
EndpointUrl = "https://myapp.azurewebsites.net/api/handler"
},
Filter = new EventSubscriptionFilter
{
SubjectBeginsWith = "/orders/",
SubjectEndsWith = ".json",
IncludedEventTypes = new[] { "OrderCreated", "OrderUpdated" },
AdvancedFilters = new List<AdvancedFilter>
{
new NumberGreaterThanAdvancedFilter
{
Key = "data.amount",
Value = 1000
}
}
},
RetryPolicy = new RetryPolicy
{
MaxDeliveryAttempts = 30,
EventTimeToLiveInMinutes = 1440 // 24h
}
});
Publishing Events to a Custom Topic (C#)
// SDK Azure.Messaging.EventGrid
var topicEndpoint = new Uri("https://mytopic.eastus-1.eventgrid.azure.net/api/events");
var credential = new AzureKeyCredential("<topic-key>");
var client = new EventGridPublisherClient(topicEndpoint, credential);
// CloudEvents event
var cloudEvent = new CloudEvent(
source: "/myapp/orders",
type: "com.example.order.created",
jsonSerializableData: new { OrderId = "ORD-001", Amount = 149.99 })
{
Id = Guid.NewGuid().ToString(),
Time = DateTimeOffset.UtcNow
};
await client.SendEventAsync(cloudEvent);
// Or send multiple events in a batch
await client.SendEventsAsync(new[] { cloudEvent1, cloudEvent2, cloudEvent3 });
Delivery Guarantees and Retry Policy
| Parameter | Default value | Range |
|---|---|---|
| Max attempts | 30 | 1 – 30 |
| Event TTL | 1440 min (24h) | 1 – 1440 min |
| Delay between attempts | Exponential (10s → 10min) | Not configurable |
| Guarantee | At-least-once | — |
At-least-once: an event may be delivered more than once in case of timeout. Handlers must be idempotent.
Push vs Pull Delivery (Event Grid)
| Mode | Description | Use case |
|---|---|---|
| Push | Event Grid pushes the event to the endpoint (webhook, Function…) | Minimal latency, real-time |
| Pull | Consumer polls an Event Grid namespace (Event Grid namespace + topic) | Batch workloads, throughput control |
Module 7 – Azure Event Hubs: Advanced
Creating a Namespace and Event Hub
# Create the namespace
az eventhubs namespace create \
--resource-group myRG \
--name myEHNamespace \
--sku Standard \
--location eastus \
--enable-kafka true # Kafka compatibility
# Create the Event Hub with 4 partitions
az eventhubs eventhub create \
--resource-group myRG \
--namespace-name myEHNamespace \
--name myHub \
--partition-count 4 \
--message-retention 7
Kafka Compatibility
Event Hubs (Standard and Premium) exposes a Kafka endpoint compatible with the Kafka 1.0+ API. Existing Kafka applications can point to Event Hubs without modifying code, by only changing the broker configuration.
# Kafka → Event Hubs configuration
bootstrap.servers=myEHNamespace.servicebus.windows.net:9093
security.protocol=SASL_SSL
sasl.mechanism=PLAIN
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \
username="$ConnectionString" \
password="<connection-string>";
Partition Key Selection
flowchart LR
E1[Event\ndeviceId=D1] -->|"hash(D1) % 4 = 0"| P0[Partition 0]
E2[Event\ndeviceId=D2] -->|"hash(D2) % 4 = 1"| P1[Partition 1]
E3[Event\ndeviceId=D1] -->|"hash(D1) % 4 = 0"| P0
E4[Event\ndeviceId=D3] -->|"hash(D3) % 4 = 3"| P3[Partition 3]
P0 --> CG1[Consumer Group A]
P1 --> CG1
P0 --> CG2[Consumer Group B]
// Send with partition key (guaranteed order by deviceId)
var producer = new EventHubProducerClient(connectionString, eventHubName);
var batchOptions = new CreateBatchOptions { PartitionKey = "device-001" };
using EventDataBatch batch = await producer.CreateBatchAsync(batchOptions);
batch.TryAdd(new EventData(Encoding.UTF8.GetBytes("telemetry-1")));
batch.TryAdd(new EventData(Encoding.UTF8.GetBytes("telemetry-2")));
await producer.SendAsync(batch);
Partition key selection rules:
- Use the logical entity identifier (deviceId, userId, orderId) to guarantee order.
- Do NOT use a very high cardinality key (e.g. random GUID per event) → loses grouping.
- Do NOT use a fixed key → all events in a single partition → hotspot.
Consumer Groups — Consumer Isolation
flowchart TD
EH[Event Hub\n4 partitions] --> CGA[Consumer Group A\nAnalytics Service]
EH --> CGB[Consumer Group B\nAlerting Service]
EH --> CGC[Consumer Group C\nArchival Service]
CGA -->|independent checkpoint| BLOB_A[Blob Storage\ncheckpoints-analytics]
CGB -->|independent checkpoint| BLOB_B[Blob Storage\ncheckpoints-alerting]
CGC -->|independent checkpoint| BLOB_C[Blob Storage\ncheckpoints-archive]
// Dedicated consumer group for the analytics service
var storageClient = new BlobContainerClient(storageConnStr, "checkpoints-analytics");
await storageClient.CreateIfNotExistsAsync();
var processor = new EventProcessorClient(
storageClient,
consumerGroup: "analytics-group", // Specific consumer group
connectionString,
eventHubName);
processor.ProcessEventAsync += async args =>
{
var data = Encoding.UTF8.GetString(args.Data.Body.ToArray());
var partitionId = args.Partition.PartitionId;
var offset = args.Data.Offset;
Console.WriteLine($"[Partition {partitionId}, Offset {offset}] {data}");
// Checkpoint every 100 messages to reduce Blob calls
if (args.Data.SequenceNumber % 100 == 0)
await args.UpdateCheckpointAsync();
};
processor.ProcessErrorAsync += args =>
{
Console.Error.WriteLine($"Partition {args.PartitionId} error: {args.Exception}");
return Task.CompletedTask;
};
await processor.StartProcessingAsync();
Capture to Azure Data Lake Storage (ADLS)
The Capture feature automatically writes events to Azure Blob Storage or ADLS Gen2 in Avro format.
# Enable Capture on an Event Hub
az eventhubs eventhub update \
--resource-group myRG \
--namespace-name myEHNamespace \
--name myHub \
--enable-capture true \
--capture-interval 300 \
--capture-size-limit 314572800 \
--destination-name EventHubArchive.AzureBlockBlob \
--storage-account /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/myStorage \
--blob-container captures \
--archive-name-format "{Namespace}/{EventHub}/{PartitionId}/{Year}/{Month}/{Day}/{Hour}/{Minute}/{Second}"
Schema Registry with Avro
The Event Hubs Schema Registry allows versioning and validating Avro/JSON event schemas.
// Serialize with the Schema Registry
var schemaRegistryClient = new SchemaRegistryClient(
"myEHNamespace.servicebus.windows.net", new DefaultAzureCredential());
var serializer = new SchemaRegistryAvroSerializer(
schemaRegistryClient,
groupName: "mySchemaGroup",
new SchemaRegistryAvroSerializerOptions { AutoRegisterSchemas = true });
var telemetry = new TelemetryData { DeviceId = "D1", Temperature = 23.5f };
EventData eventData = await serializer.SerializeAsync<EventData, TelemetryData>(telemetry);
await producer.SendAsync(new[] { eventData });
Module 8 – Azure Queue Storage: Deep Dive
Service Bus vs Queue Storage Comparison (detailed)
| Feature | Azure Queue Storage | Azure Service Bus |
|---|---|---|
| Protocol | HTTPS REST | AMQP 1.0, HTTPS |
| Max message size | 64 KB | 256 KB (Standard), 100 MB (Premium) |
| Max retention | 7 days | 14 days |
| Delivery ordering | Approximate (best-effort) | Guaranteed FIFO with sessions |
| Duplicate detection | ❌ | ✅ |
| Dead-letter | ❌ (manual poison messages) | ✅ automatic |
| Topics / Pub-sub | ❌ | ✅ |
| Sessions | ❌ | ✅ |
| Transactions | ❌ | ✅ |
| Max messages | Unlimited (Azure storage) | Configurable (namespace quota) |
| Visibility timeout | ✅ (30s default) | ✅ (lock duration, 60s default) |
| Cost | Very low | Moderate to high |
TTL (Time-To-Live) and Visibility Timeout
- TTL: maximum duration a message stays in the queue (default: 7 days, max: 7 days).
- Visibility Timeout: when a consumer retrieves a message, it becomes invisible to others for this delay. If not deleted within that delay → becomes visible again (automatic redelivery).
sequenceDiagram
participant P as Producer
participant Q as Queue Storage
participant C as Consumer
P->>Q: SendMessage (TTL = 1h)
C->>Q: ReceiveMessages (visibilityTimeout=30s)
Q-->>C: message + PopReceipt
Note over Q: Message invisible for 30s
C->>C: Processing
C->>Q: DeleteMessage(messageId, popReceipt) ✅
Note over Q: Message permanently deleted
alt If timeout exceeded without DeleteMessage
Q->>Q: Becomes visible again → redelivery
end
Poison Message Handling
Azure Queue Storage does not have a native DLQ. Standard pattern:
- The message is redelivered until
dequeueCountis high. - Read
DequeueCount→ if > threshold (e.g. 5), move to a separate poison queue.
var queueClient = new QueueClient(connectionString, "myqueue");
var poisonQueue = new QueueClient(connectionString, "myqueue-poison");
await poisonQueue.CreateIfNotExistsAsync();
QueueMessage[] messages = await queueClient.ReceiveMessagesAsync(
maxMessages: 10,
visibilityTimeout: TimeSpan.FromSeconds(30));
foreach (var msg in messages)
{
if (msg.DequeueCount > 5)
{
// Move to poison queue
await poisonQueue.SendMessageAsync(msg.MessageText);
await queueClient.DeleteMessageAsync(msg.MessageId, msg.PopReceipt);
Console.WriteLine($"Poison message moved: {msg.MessageId}");
continue;
}
try
{
// Normal processing
ProcessMessage(msg.MessageText);
await queueClient.DeleteMessageAsync(msg.MessageId, msg.PopReceipt);
}
catch
{
// Do not delete → becomes visible again after visibilityTimeout
Console.WriteLine("Processing failed, message will be redelivered");
}
}
Peek vs Dequeue
| Operation | SDK Method | Visibility effect | Auto-deletion |
|---|---|---|---|
| Peek | PeekMessagesAsync | None | ❌ |
| Receive (Dequeue) | ReceiveMessagesAsync | Invisible during timeout | ❌ (manual) |
| Delete | DeleteMessageAsync | Deleted | ✅ |
// Peek — read without removing (non-destructive read)
PeekedMessage[] peeked = await queueClient.PeekMessagesAsync(maxMessages: 5);
foreach (var msg in peeked)
Console.WriteLine($"[Peek] {msg.MessageText}");
// Dequeue — read and make invisible
QueueMessage[] received = await queueClient.ReceiveMessagesAsync(
maxMessages: 10,
visibilityTimeout: TimeSpan.FromMinutes(2));
foreach (var msg in received)
{
Console.WriteLine($"[Dequeue] {msg.MessageText} (Attempt #{msg.DequeueCount})");
// Processing...
await queueClient.DeleteMessageAsync(msg.MessageId, msg.PopReceipt);
}
Module 9 – Azure Notification Hubs
Architecture and Push Notification Flow
flowchart LR
BACKEND[Backend App\n.NET / Node] -->|Send notification| NH[Azure\nNotification Hub]
NH -->|APNs protocol| IOS[📱 iOS App]
NH -->|FCM protocol| ANDROID[📱 Android App]
NH -->|WNS protocol| WIN[🖥️ Windows App]
IOS -->|Register token| NH
ANDROID -->|Register token| NH
WIN -->|Register token| NH
PNS (Platform Notification Services)
| Platform | PNS | Protocol |
|---|---|---|
| iOS | APNs (Apple Push Notification service) | HTTP/2 |
| Android | FCM (Firebase Cloud Messaging) | HTTP |
| Windows | WNS (Windows Push Notification Services) | HTTPS |
Tags — Precise Targeting
Tags allow segmenting recipients without maintaining a list on the server side.
// Client-side registration with tags (Notification Hubs SDK)
var hub = new NotificationHub("myHub", "myConnectionString");
// iOS registration with user and device tags
string registrationId = await hub.RegisterNativeAsync(
deviceToken: "<APNs-device-token>",
tags: new[] { "userId:user-42", "category:sports", "region:FR" });
// Targeted send from the backend — tag expression
var hub = new NotificationHubClient.CreateClientFromConnectionString(
connectionString, hubName);
// Send to all French users interested in sports
string tagExpression = "category:sports && region:FR";
var notification = new AppleNotification("{\"aps\":{\"alert\":\"New match tonight!\"}}");
await hub.SendNotificationAsync(notification, tagExpression);
Templates — Cross-platform Notifications
Templates allow sending a single backend call for all platforms, letting Notification Hubs adapt the format.
// iOS template registration
string iosTemplate = "{\"aps\":{\"alert\":\"$(message)\"}}";
await hub.RegisterTemplateAsync(deviceToken, iosTemplate, "genericTemplate",
tags: new[] { "userId:user-42" });
// Android template registration
string androidTemplate = "{\"data\":{\"message\":\"$(message)\"}}";
await hub.RegisterTemplateAsync(fcmToken, androidTemplate, "genericTemplate",
tags: new[] { "userId:user-42" });
// Send from the backend — a single call for all platforms
var templateParams = new Dictionary<string, string> { ["message"] = "Your order has shipped!" };
await hub.SendTemplateNotificationAsync(templateParams, "userId:user-42");
Backendless Registration
Instead of the backend managing registrations, the client registers directly via a temporary SAS token provided by the backend.
// Backend: generate a limited access token (listen only)
string sasToken = SharedAccessSignatureTokenProvider.GetSharedAccessSignature(
keyName: "DefaultListenSharedAccessSignature",
sharedAccessKey: "<listen-key>",
targetUri: "https://<namespace>.servicebus.windows.net/<hubName>",
tokenTimeToLive: TimeSpan.FromHours(1));
// Client: use the token to register directly
var client = new NotificationHubClient(sasToken, hubEndpoint);
await client.RegisterNativeAsync(deviceToken);
Debugging Push Notifications
| Problem | Check |
|---|---|
| Notification not received | Check PNS credentials (APNs cert expiration, FCM key) |
| Incorrectly targeted tag | Use Test Send in the Azure portal with tag expression |
| Expired registration | PNS tokens expire → re-register regularly |
| Quota exceeded | Check tier (Free = 1M push/month, Basic = 10M, Standard = unlimited) |
Module 10 – APIM: Advanced Policies
Cache — cache-lookup / cache-store
<policies>
<inbound>
<cache-lookup vary-by-developer="false" vary-by-developer-groups="false">
<vary-by-header>Accept</vary-by-header>
<vary-by-query-parameter>version</vary-by-query-parameter>
</cache-lookup>
<base />
</inbound>
<outbound>
<cache-store duration="3600" /> <!-- Cache 1 hour -->
<base />
</outbound>
</policies>
Send-Request — Outbound HTTP Call from a Policy
<inbound>
<send-request mode="new" response-variable-name="tokenResponse" timeout="20" ignore-error="false">
<set-url>https://login.microsoftonline.com/{tenantId}/oauth2/token</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/x-www-form-urlencoded</value>
</set-header>
<set-body>grant_type=client_credentials&client_id={{clientId}}&client_secret={{clientSecret}}&resource=https://graph.microsoft.com/</set-body>
</send-request>
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + ((IResponse)context.Variables["tokenResponse"]).Body.As<JObject>()["access_token"].ToString())</value>
</set-header>
<base />
</inbound>
Choose — Conditional Logic
<inbound>
<choose>
<when condition="@(context.Request.Headers.GetValueOrDefault("X-Client-Version", "0").CompareTo("2.0") >= 0)">
<!-- v2+ clients → new backend -->
<set-backend-service base-url="https://api-v2.contoso.com" />
</when>
<when condition="@(context.User.Groups.Contains("premium"))">
<set-header name="X-Priority" exists-action="override">
<value>high</value>
</set-header>
</when>
<otherwise>
<!-- Legacy clients → old backend -->
<set-backend-service base-url="https://api-v1.contoso.com" />
</otherwise>
</choose>
<base />
</inbound>
Retry — Automatic Resilience
<backend>
<retry condition="@(context.Response.StatusCode >= 500)" count="3" interval="2" max-interval="10" delta="1" first-fast-retry="true">
<forward-request />
</retry>
</backend>
Mock-Response — Simulating a Backend
<inbound>
<mock-response status-code="200" content-type="application/json">
<representations>
<representation content-type="application/json">
{ "status": "ok", "data": [{"id": 1, "name": "Mock Item"}] }
</representation>
</representations>
</mock-response>
</inbound>
Set-Variable and Trace
<inbound>
<!-- Store a value in a context variable -->
<set-variable name="clientRegion" value="@(context.Request.Headers.GetValueOrDefault("X-Region", "unknown"))" />
<!-- Trace a value in the journal (visible in Test → Trace) -->
<trace source="CustomPolicy" severity="information">
<message>@($"Request from region: {(string)context.Variables["clientRegion"]}")</message>
<metadata name="requestId" value="@(context.RequestId)" />
</trace>
<base />
</inbound>
Named Values and Backends
Named Values: key-value store in APIM. Referenced in policies with {{valueName}}. Can be secrets (stored in Key Vault).
<!-- Use a Named Value in a policy -->
<set-header name="X-API-Key" exists-action="override">
<value>{{BackendApiKey}}</value> <!-- References a Named Value -->
</set-header>
Backends: named entities representing a backend service, with circuit breaker and load balancing.
<backend>
<set-backend-service backend-id="my-backend-pool" /> <!-- References an APIM Backend -->
</backend>
Policy Expressions — C# Subset
Policy expressions use a subset of C# executed in the context context:
<!-- Common expression examples -->
<!-- Read a header -->
@(context.Request.Headers.GetValueOrDefault("X-Forwarded-For", "0.0.0.0"))
<!-- Read a JWT claim -->
@(context.Request.Headers["Authorization"].First().Split(' ')[1])
<!-- Encode to Base64 -->
@(Convert.ToBase64String(Encoding.UTF8.GetBytes("user:password")))
<!-- Calculate an MD5 hash -->
@{
var body = context.Request.Body.As<string>(preserveContent: true);
return BitConverter.ToString(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(body))).Replace("-","").ToLower();
}
<!-- Check an Entra ID role in the JWT token -->
@(context.Request.Headers.ContainsKey("Authorization") &&
new Jwt(context.Request.Headers["Authorization"].First().Replace("Bearer ", ""))
.Claims["roles"].Contains("API.Write"))
Module 11 – Azure Relay
WCF Relay vs Hybrid Connections
Azure Relay allows exposing on-premises services on the Internet without opening inbound ports or configuring a VPN.
| Feature | WCF Relay | Hybrid Connections |
|---|---|---|
| Protocol | WCF / SOAP | WebSocket (HTTPS) |
| Language | .NET (WCF only) | Any language |
| Status | Legacy | Recommended |
| Use case | Migration of existing WCF services | New development, Node.js, Java |
Hybrid Connections Architecture
sequenceDiagram
participant C as Client (Internet)
participant R as Azure Relay\nHybrid Connection
participant L as Listener\n(On-Premises)
L->>R: Outbound WebSocket connection (outbound only)
Note over L,R: Persistent tunnel established from on-prem
C->>R: Inbound HTTPS request
R-->>L: Forward via WebSocket tunnel
L->>L: Local processing
L-->>R: Response
R-->>C: HTTP response
The on-premises server initiates the outbound connection to Azure Relay (port 443 HTTPS outbound only — no inbound ports to open).
SDK Setup — Hybrid Connections
// Listener side on-premises (behind the firewall)
var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(
keyName: "RootManageSharedAccessKey",
sharedAccessKey: "<key>");
var relayNamespace = "myrelay.servicebus.windows.net";
var connectionName = "myconnection";
var listener = new HybridConnectionListener(
new Uri($"sb://{relayNamespace}/{connectionName}"),
tokenProvider);
listener.RequestHandler = async (context) =>
{
using var reader = new StreamReader(context.Request.InputStream);
string body = await reader.ReadToEndAsync();
Console.WriteLine($"Request received: {body}");
context.Response.StatusCode = HttpStatusCode.OK;
using var writer = new StreamWriter(context.Response.OutputStream);
await writer.WriteLineAsync("Response from the on-premises server!");
await context.Response.CloseAsync();
};
await listener.OpenAsync();
Console.WriteLine("Relay listener started. Press any key to stop.");
Console.ReadKey();
await listener.CloseAsync();
// Client side (Internet)
var sender = new HybridConnectionClient(
new Uri($"sb://{relayNamespace}/{connectionName}"),
tokenProvider);
var stream = await sender.CreateConnectionAsync();
using var writer = new StreamWriter(stream) { AutoFlush = true };
using var reader = new StreamReader(stream);
await writer.WriteLineAsync("Hello from the Internet!");
string response = await reader.ReadLineAsync();
Console.WriteLine($"Response: {response}");
Azure Relay Use Cases
- Exposing an internal API on the Internet for partners without VPN.
- Debugging a local API from a cloud environment (enterprise alternative to ngrok).
- Progressively migrating on-premises WCF services to Azure.
- Connecting SaaS applications to internal databases or services.
Module 12 – Messaging Patterns
Pattern 1: Competing Consumers
Multiple consumer instances read from the same queue → horizontal scalability.
flowchart LR
P[Producer] -->|messages| Q["(Queue)"]
Q --> C1[Consumer 1\nInstance A]
Q --> C2[Consumer 2\nInstance B]
Q --> C3[Consumer 3\nInstance C]
C1 --> DB["(Database)"]
C2 --> DB
C3 --> DB
Key rules:
- Each message is processed by only one consumer (automatic locking by Service Bus).
- Ideal for Azure Functions or Container Apps workers in scale-out.
- The
lock durationmust be sufficient for processing (otherwise the message is redelivered).
Pattern 2: Publisher / Subscriber
flowchart LR
PUB[Publisher] -->|message| T["(Topic)"]
T --> S1["(Sub: Notifications\ncustomer:* filter)"]
T --> S2["(Sub: Analytics\nall messages)"]
T --> S3["(Sub: Audit\nall messages)"]
S1 --> SVC1[Notifications Service\nEmail / SMS]
S2 --> SVC2[Analytics Service\nPower BI]
S3 --> SVC3[Audit Service\nLog Storage]
Pattern 3: Request / Reply
Simulates synchronous communication via asynchronous messaging with ReplyTo and CorrelationId.
sequenceDiagram
participant REQ as Requesting Service
participant Q as Request Queue
participant REP_Q as Reply Queue
participant SVC as Target Service
REQ->>Q: Send(msg, ReplyTo="reply-queue", CorrelationId="abc-123")
SVC->>Q: Receive
SVC->>SVC: Processing
SVC->>REP_Q: Send(response, CorrelationId="abc-123")
REQ->>REP_Q: Receive (filter by CorrelationId="abc-123")
// Requester: send and wait for the response
var requestMsg = new ServiceBusMessage("Calculate price for product 42")
{
ReplyTo = "reply-queue",
CorrelationId = Guid.NewGuid().ToString(),
MessageId = Guid.NewGuid().ToString()
};
await requestSender.SendMessageAsync(requestMsg);
// Wait for response with matching CorrelationId
var replyReceiver = client.CreateReceiver("reply-queue");
ServiceBusReceivedMessage reply;
do
{
reply = await replyReceiver.ReceiveMessageAsync(TimeSpan.FromSeconds(30));
} while (reply?.CorrelationId != requestMsg.CorrelationId);
Console.WriteLine($"Response received: {reply.Body}");
await replyReceiver.CompleteMessageAsync(reply);
Pattern 4: Dead-Letter Pattern
flowchart TD
Q["(Active Queue)"] -->|Delivery N| C[Consumer]
C -->|Business error| DLQ["(Dead-Letter Queue)"]
C -->|Success| DONE[✅ Message processed]
DLQ --> ALERT[🔔 Azure Monitor Alert]
DLQ --> REPLAY[🔁 Replay Service\nafter fix]
REPLAY -->|Corrected message| Q
Pattern 5: Message Routing
flowchart LR
SRC[Source Events] --> T["(Service Bus Topic)"]
T -->|SQL: OrderType='Standard'| STANDARD["(Sub Standard)"]
T -->|SQL: OrderType='Express'| EXPRESS["(Sub Express)"]
T -->|SQL: OrderType='International'| INTL["(Sub International)"]
STANDARD --> WF1[Standard Workflow\n2-3 days]
EXPRESS --> WF2[Express Workflow\n24h]
INTL --> WF3[International Workflow\nCustoms + delivery]
Module 13 – Resilience Patterns
Retry with Exponential Backoff and Jitter
flowchart TD
REQ[Request] --> SVC{Service\navailable?}
SVC -->|✅ Success| DONE[OK Response]
SVC -->|❌ 5xx Error| CALC[Calculate delay\nbaseDelay × 2^attempt + jitter]
CALC --> WAIT[Wait]
WAIT --> COUNT{Attempts\n< max?}
COUNT -->|Yes| SVC
COUNT -->|No| FAIL[💥 Final failure\nDead-letter / Exception]
// Manual retry with exponential backoff + jitter
public async Task<T> RetryWithBackoff<T>(Func<Task<T>> operation,
int maxAttempts = 5, int baseDelayMs = 100)
{
var rng = new Random();
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
return await operation();
}
catch (Exception ex) when (attempt < maxAttempts)
{
// Exponential backoff: 100ms, 200ms, 400ms, 800ms...
int delay = (int)(baseDelayMs * Math.Pow(2, attempt - 1));
// Jitter: add randomness to avoid thundering herds
int jitter = rng.Next(0, delay / 2);
await Task.Delay(delay + jitter);
Console.WriteLine($"Attempt {attempt} failed ({ex.Message}). Retry in {delay + jitter}ms");
}
}
return await operation(); // Last attempt — let the exception propagate
}
Circuit Breaker with Polly
stateDiagram-v2
[*] --> Closed : Start
Closed --> Open : N failures in the time window
Open --> HalfOpen : After pause duration
HalfOpen --> Closed : Test request succeeds
HalfOpen --> Open : Test request fails
state Closed { [*] --> ok }
state Open { [*] --> fail }
state HalfOpen { [*] --> test }
// Polly v8 — Circuit Breaker + Retry combined
using Polly;
using Polly.CircuitBreaker;
using Polly.Retry;
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>()
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5, // Opens if 50% of calls fail
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10, // Minimum 10 calls before opening
BreakDuration = TimeSpan.FromSeconds(60), // Stays open for 60s
ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>()
})
.Build();
// Usage
await pipeline.ExecuteAsync(async cancellationToken =>
{
var response = await httpClient.GetAsync("/api/orders", cancellationToken);
response.EnsureSuccessStatusCode();
});
Bulkhead Isolation
Limits the number of concurrent requests to a service to prevent overload from contaminating the entire system.
// Polly v8 — Rate Limiter (equivalent to bulkhead)
var bulkhead = new ResiliencePipelineBuilder()
.AddConcurrencyLimiter(
permitLimit: 10, // Max 10 simultaneous calls
queueLimit: 5) // Max 5 waiting
.Build();
Timeout Handling
var pipeline = new ResiliencePipelineBuilder()
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(10),
OnTimeout = args =>
{
Console.WriteLine($"Timeout after {args.Timeout.TotalSeconds}s");
return default;
}
})
.Build();
Saga Pattern — Distributed Transactions
The Saga Pattern manages distributed transactions via a series of compensatable local transactions. On failure, compensatory actions roll back the previous steps.
sequenceDiagram
participant O as Orchestrator
participant CMD as Orders Service
participant INV as Inventory Service
participant PAY as Payment Service
participant SHIP as Shipping Service
O->>CMD: Create order
CMD-->>O: Order created ✅
O->>INV: Reserve stock
INV-->>O: Stock reserved ✅
O->>PAY: Charge payment
PAY-->>O: ❌ Payment declined
Note over O: Triggering compensations
O->>INV: Release stock (compensation)
O->>CMD: Cancel order (compensation)
O-->>O: Saga ended in failure ✅ clean
// Orchestrated Saga via Azure Service Bus
public class OrderSagaOrchestrator
{
private readonly ServiceBusClient _client;
public async Task ExecuteAsync(OrderCreatedEvent orderEvent)
{
var sagaId = Guid.NewGuid().ToString();
var compensations = new Stack<Func<Task>>();
try
{
// Step 1: Reserve stock
await SendCommand("inventory-commands", new ReserveStockCommand
{
SagaId = sagaId, OrderId = orderEvent.OrderId, Items = orderEvent.Items
});
compensations.Push(() => SendCommand("inventory-commands",
new ReleaseStockCommand { SagaId = sagaId, OrderId = orderEvent.OrderId }));
// Step 2: Charge payment
await SendCommand("payment-commands", new ChargePaymentCommand
{
SagaId = sagaId, OrderId = orderEvent.OrderId, Amount = orderEvent.Amount
});
compensations.Push(() => SendCommand("payment-commands",
new RefundPaymentCommand { SagaId = sagaId, OrderId = orderEvent.OrderId }));
// Step 3: Create shipment
await SendCommand("shipping-commands", new CreateShipmentCommand
{
SagaId = sagaId, OrderId = orderEvent.OrderId
});
}
catch (Exception ex)
{
Console.WriteLine($"Saga {sagaId} failed: {ex.Message}. Compensating...");
// Unwind compensations in reverse order
while (compensations.Count > 0)
await compensations.Pop()();
}
}
private async Task SendCommand(string queue, object command)
{
var sender = _client.CreateSender(queue);
var msg = new ServiceBusMessage(JsonSerializer.Serialize(command))
{
ContentType = "application/json"
};
await sender.SendMessageAsync(msg);
}
}
Module 14 – AZ-204 Review Questions
Q1 — Service Bus: Sessions
Question: An order processing application must guarantee that all steps of an order (payment, stock allocation, shipping) are processed in order for each individual order. Which Azure Service Bus feature should you use?
- A. Duplicate Detection
- B. Dead-Letter Queue
- C. Sessions ✅
- D. Message Deferral
Explanation: Sessions allow grouping messages by
SessionId(e.g. the order ID) and guarantee that messages in the same session are processed in FIFO order by a single consumer at a time.
Q2 — Event Grid: Schema
Question: You are developing a new multi-cloud event-driven solution. Which Azure Event Grid event schema do you recommend to maximize portability and compatibility with industry standards?
- A. Event Grid Schema (native)
- B. CloudEvents Schema (CNCF) ✅
- C. JSON Schema
- D. Avro Schema
Explanation: CloudEvents is a CNCF (Cloud Native Computing Foundation) standard supported by multiple cloud providers. For new development, CloudEvents is Microsoft’s recommendation to maximize portability.
Q3 — Event Hubs: Consumer Groups
Question: You have an Event Hub ingesting IoT data. Three different services (analytics, alerting, archiving) must all process all events independently. How do you implement this?
- A. Create 3 separate Event Hubs and send messages to each
- B. Create 3 separate Consumer Groups on the same Event Hub ✅
- C. Partition the Event Hub into 3 partitions and assign one to each service
- D. Use subscription filters for each service
Explanation: Consumer Groups provide independent views of the stream. Each group maintains its own checkpoint, allowing the 3 services to read all events at their own pace without blocking each other.
Q4 — Queue Storage: Poison Messages
Question: An Azure Function reads messages from an Azure Storage Queue. Some messages consistently cause exceptions. After how many deliveries is a message considered poison in Azure Queue Storage, and where should it be moved?
- A. After 5 deliveries, to an automatic dead-letter queue
- B. After 10 deliveries, to an automatic dead-letter queue
- C. After N deliveries (configurable), manually to a separate queue ✅
- D. Never automatically, it must be deleted manually
Explanation: Azure Queue Storage does not have a native DLQ. The
DequeueCountproperty increments with each read. The developer must implement poison message detection logic (e.g.DequeueCount > 5) and manually move them to a separate poison queue.
Q5 — APIM: Policy Scope
Question: You want to apply a rate limit to all APIs for a specific group of consumers, without applying it to all APIs in the APIM instance or to each API individually. Which policy scope do you use?
- A. All APIs
- B. Product ✅
- C. API
- D. Operation
Explanation: The Product scope applies to all APIs grouped in that product. Clients subscribe to a Product, and the Product’s policies apply to all their requests to the Product’s APIs. This is the intermediate level between “All APIs” and a specific API.
Q6 — Notification Hubs: Tags
Question: You are using Azure Notification Hubs to send push notifications. You want to send a notification only to French users interested in the “sports” category. Which feature do you use?
- A. Templates
- B. Scheduled notifications
- C. Tags and tag expressions ✅
- D. Consumer Groups
Explanation: Tags allow segmenting registered devices. A tag expression like
region:FR && category:sportstargets only devices with both tags. This avoids maintaining a list of devices on the backend.
Q7 — Azure Relay
Question: A company has a REST API hosted on an on-premises server behind a strict firewall. It needs to expose this API to external partners via the Internet without opening inbound ports or configuring a VPN. Which Azure solution do you choose?
- A. Azure VPN Gateway
- B. Azure ExpressRoute
- C. Azure API Management with direct on-premises backend
- D. Azure Relay — Hybrid Connections ✅
Explanation: Azure Relay Hybrid Connections allows exposing on-premises services on the Internet via an outbound WebSocket connection established from the internal server. No inbound ports are required. This is the ideal solution for this scenario without VPN.
Q8 — Service Bus: Duplicate Detection
Question: A payment service sends Service Bus messages. In case of network timeout, the service may resend the same message. How do you prevent double processing of a payment?
- A. Use Service Bus sessions
- B. Enable Duplicate Detection and use a stable MessageId ✅
- C. Use a Dead-Letter Queue to filter duplicates
- D. Implement a distributed lock with Azure Redis Cache
Explanation: Duplicate Detection stores received
MessageIdvalues within a configurable time window. If a message with the sameMessageIdis resent within this window, Service Bus silently rejects it. The service must use a deterministicMessageId(e.g. transaction ID).
Q9 — Event Grid: Delivery Guarantee
Question: An Azure Function handler receives Event Grid events. Due to an update, the Function is unavailable for 2 minutes. What happens to events sent during this period?
- A. Events are permanently lost
- B. Events are redirected to another region
- C. Event Grid retries delivery with exponential backoff until the event TTL ✅
- D. Events are paused until manual restart
Explanation: Event Grid guarantees at-least-once delivery with a configurable retry policy (up to 30 attempts, 24h TTL by default). During the 2-minute unavailability, Event Grid retries delivery with increasing delays until the Function is available again.
Q10 — Resilience: Circuit Breaker
Question: Your .NET application calls an external microservice that has intermittent failures. You want to prevent cumulative timeouts from degrading the overall application performance. Which pattern and .NET library do you recommend?
- A. Retry with exponential backoff only (Polly)
- B. Bulkhead isolation (SemaphoreSlim)
- C. Circuit Breaker (Polly) ✅
- D. Message Queue for all requests
Explanation: The Circuit Breaker (implemented with Polly) quickly cuts calls to a failing service (Open state) after an error threshold, avoiding costly timeouts. After a pause period, it moves to Half-Open state to test if the service has recovered. This is the appropriate pattern to protect the application from cascading failures.
Q11 — Event Hubs: Checkpoints
Question: You are using EventProcessorClient to consume an Event Hub. The application restarts after a failure. How do you ensure that events are not reprocessed from the beginning?
- A. Event Hubs automatically deletes read events
- B. The consumer group keeps the read position in memory
- C. Checkpoints stored in Azure Blob Storage preserve the read position per partition ✅
- D. You must manually configure a start offset in the code
Explanation:
EventProcessorClientcallsUpdateCheckpointAsync()to save the read position (offset) of each partition in Azure Blob Storage. On restart, the processor resumes from the last saved checkpoint, avoiding reprocessing.
Q12 — Saga Pattern
Question: In a microservices architecture, an order transaction involves 3 services (inventory, payment, shipping). If payment fails after stock has been reserved, how do you guarantee data consistency?
- A. Use a distributed 2PC (Two-Phase Commit) transaction
- B. Manually cancel each service on error in the orchestrator code
- C. Implement the Saga Pattern with compensatory actions ✅
- D. Use Azure SQL Distributed Transactions
Explanation: The Saga Pattern decomposes a distributed transaction into a sequence of local transactions. If a step fails, compensatory transactions are executed in reverse order to undo the effects of previous steps. This is the recommended solution for microservices architectures as it avoids the performance and availability issues of classic distributed transactions (2PC).
Summary — Service Selection Table
| Scenario | Recommended service | Reason |
|---|---|---|
| IoT telemetry, millions of events/s | Event Hubs | High performance, partitions, retention |
| Azure state change notification | Event Grid (System Topic) | Integrated with Azure resources |
| Workflow with entity-ordered steps | Service Bus + Sessions | FIFO guaranteed by SessionId |
| Pub-sub with property-based filtering | Service Bus Topics + Filters | SQL/Correlation filters |
| Simple queue, low cost | Azure Queue Storage | Simple REST, minimal price |
| Mobile push iOS/Android/Windows | Notification Hubs | Multi-platform PNS abstraction |
| Expose on-prem API without VPN | Azure Relay Hybrid Connections | Outbound connection only |
| Secure and observable API gateway | APIM | Policies, rate limit, cache, portal |
| Distributed microservice transactions | Saga Pattern + Service Bus | Compensation without 2PC |
| HTTP call resilience | Polly + Circuit Breaker | Fail-fast, cascade protection |
Search Terms
az-204 · connected · distributed · systems · azure · developer · microsoft · event · hubs · service · bus · grid · pattern · storage · messaging · policy · queue · apim · architecture · deep · dive · notification · queues · relay