Level: Intermediate to Advanced
Technologies: .NET 8 / C# / Isolated Worker Model / Azure Functions v4
Table of Contents
- Introduction and Fundamental Concepts
- HTTP Triggers – Functions triggered by HTTP
- Exploring Azure Functions Triggers
- Bindings – Connections to Azure Services
- Deployment to Azure
- Durable Functions – Resilient Workflows
- Azure Functions in Production
- Advanced Security
- Monitoring and Observability
- KEDA and Kubernetes
- Best Practices and Common Pitfalls
- Glossary
Module 1 – Introduction and Fundamental Concepts {#module-1}
What is an Azure Function?
Azure Functions is a serverless compute service offered by Microsoft Azure that allows developers to run event-triggered code without having to manage the underlying infrastructure. At the heart of Azure Functions is the idea of events and code: you simply provide code (typically a single function) in the language of your choice, and you tell Azure Functions which event should trigger your code.
Fundamental characteristics:
- Serverless: Azure fully manages the infrastructure, scalability, and availability
- Event-driven: Each function responds to a specific trigger (HTTP, Timer, Queue, Blob, etc.)
- Pay-per-use: Billing only for actual executions (Consumption plan)
- Polyglot: Supports C#, Java, JavaScript/TypeScript, Python, PowerShell, Go, Rust
- Automatic scalability: Instant scale-out based on load
flowchart TD
A[Event / Trigger] --> B{Azure Functions Runtime}
B --> C[Your Code / Function]
C --> D[Output Bindings]
D --> E[Azure Storage Queue]
D --> F[Cosmos DB]
D --> G[Service Bus]
D --> H[Blob Storage]
style A fill:#0078D4,color:#fff
style B fill:#50e6ff,color:#000
style C fill:#68217A,color:#fff
Fundamental concepts of serverless architecture
The term serverless may seem paradoxical since servers obviously exist. The serverless philosophy means that you fully delegate the management and maintenance of servers to a third party (here Microsoft Azure), so that you can focus exclusively on business code.
In a typical serverless architecture:
- You use Platform as a Service (PaaS) services for databases (Azure Cosmos DB, Azure SQL)
- You outsource authentication (Microsoft Entra ID, Auth0)
- You use third-party services for emails (SendGrid), payments (Stripe), etc.
- Azure Functions handles custom backend code
flowchart LR
subgraph "Traditional Architecture"
T1[App Server] --> T2[Database Server]
T1 --> T3[Auth Server]
T1 --> T4[Email Server]
end
subgraph "Serverless Architecture"
S1[Azure Functions] --> S2[Azure Cosmos DB PaaS]
S1 --> S3[Microsoft Entra ID PaaS]
S1 --> S4[SendGrid SaaS]
S1 --> S5[Azure Storage PaaS]
end
style S1 fill:#68217A,color:#fff
Hosting plans in detail
The choice of hosting plan is one of the most important architectural decisions for your Azure Functions application. Each plan offers different trade-offs between cost, performance, and features.
| Plan | Billing | Cold Starts | Max Timeout | VNet Integration | Max Instances | Typical use cases |
|---|---|---|---|---|---|---|
| Consumption | Per execution (1M free + 400K GB-sec/month) | Yes (a few seconds) | 10 min (configurable) | ❌ | 200 | Unpredictable workloads, prototyping, low volumes |
| Elastic Premium EP1 | Pre-warmed instances + scaling | No (pre-warmed) | Unlimited | ✅ | 100 | Latency-sensitive APIs, event processing |
| Elastic Premium EP2 | Same as EP1, more CPU/RAM | No | Unlimited | ✅ | 100 | CPU-intensive workloads |
| Elastic Premium EP3 | Same as EP2, even more powerful | No | Unlimited | ✅ | 100 | Memory-intensive workloads |
| Dedicated (App Service) | Monthly (existing ASP plan) | No | Unlimited | ✅ | Per plan | Long-running functions, sharing with web apps |
| Flex Consumption | Hybrid Consumption/Premium | Low | Variable | ✅ | Variable | Optimal cost/performance trade-off |
| Container Apps | Kubernetes-based | Variable | Variable | ✅ | Variable | Containerized workloads, multi-cloud |
Cost tip: For a startup or prototyping project, always start with the Consumption plan. You get 1 million free executions and 400,000 GB-seconds per month, which covers the vast majority of development scenarios at no cost.
Cost calculation – Consumption plan
Consumption plan billing is based on two metrics:
$$\text{Total Cost} = \text{Execution Cost} + \text{Resource Cost}$$
$$\text{Resource Cost} = \text{Duration (sec)} \times \text{Allocated RAM (GB)} \times \text{Price per GB-sec}$$
Concrete example:
- 5 million executions/month
- Average duration: 200ms
- RAM: 256MB (0.25 GB)
Calculation:
- Billable executions: 5M - 1M (free) = 4M → 4M × $0.0000002 = $0.80
- GB-sec consumed: 5M × 0.2s × 0.25GB = 250,000 GB-sec
- Billable GB-sec: 250,000 - 400,000 (free) = 0 (below free threshold!)
- Total: $0.80/month
Supported languages and C# programming models
Azure Functions officially supports several languages. For C#, there are two distinct models whose differences are crucial to understand:
| Aspect | In-Process (Legacy) | Isolated Worker (Recommended) |
|---|---|---|
| Process | Same process as Functions host | Process separate from host |
| .NET Runtime | Must match Functions runtime | Any compatible .NET version |
| Dependencies | Shared with host | Completely isolated |
| Startup | Faster | Slightly slower |
| Future | Deprecated | This is the future of the platform |
| Dependency injection | IFunctionsHostBuilder | Standard HostBuilder |
| Middleware | Limited | Full ASP.NET Core middleware support |
Important: Use exclusively the Isolated Worker model for all new development. Microsoft has announced that the in-process model will be deprecated. This course uses only the isolated worker model.
Structure of an Azure Functions project (Isolated Worker)
MyFunctionApp/
├── MyFunctionApp.csproj
├── host.json
├── local.settings.json
├── Program.cs ← Entry point and configuration
├── Functions/
│ ├── HttpTriggerFunction.cs
│ ├── TimerFunction.cs
│ └── QueueFunction.cs
└── Models/
└── OrderModel.cs
MyFunctionApp.csproj file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.22.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.2.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Timer" Version="4.3.1" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues" Version="5.3.1" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs" Version="6.3.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.17.4" />
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.2.0" />
</ItemGroup>
</Project>
Program.cs file:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
// Standard dependency injection
services.AddHttpClient();
services.AddSingleton<IMyService, MyService>();
})
.Build();
host.Run();
host.json file:
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"functionTimeout": "00:10:00",
"extensions": {
"queues": {
"batchSize": 16,
"maxDequeueCount": 5,
"visibilityTimeout": "00:00:30"
}
}
}
Module 2 – HTTP Triggers: Functions triggered by HTTP {#module-2}
HTTP Trigger architecture
The HTTP trigger is one of the most useful types of Azure Functions triggers. It allows your function to run in response to an HTTP request. The two main uses are:
- Implementing web APIs (RESTful APIs, GraphQL endpoints)
- Receiving webhooks from third-party services (Stripe, GitHub, Slack, etc.)
sequenceDiagram
participant Client as HTTP Client
participant APIM as Azure API Management
participant AF as Azure Functions
participant DB as Cosmos DB
participant Queue as Storage Queue
Client->>APIM: POST /api/orders
APIM->>APIM: OAuth2 Authentication
APIM->>AF: Forwards request
AF->>DB: Saves order
AF->>Queue: Sends async message
AF-->>APIM: HTTP 201 Created
APIM-->>Client: HTTP 201 Created
Queue-->>AF: Triggers async processing
Complete HTTP Function structure (Isolated Worker)
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace GloboTicket.Functions;
public class OrderWebhookFunction
{
private readonly ILogger<OrderWebhookFunction> _logger;
private readonly IOrderService _orderService;
public OrderWebhookFunction(
ILogger<OrderWebhookFunction> logger,
IOrderService orderService)
{
_logger = logger;
_orderService = orderService;
}
[Function("NewPurchaseWebhook")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "orders")]
HttpRequestData req)
{
_logger.LogInformation("Payment webhook received.");
// JSON body deserialization
var order = await req.ReadFromJsonAsync<NewOrderRequest>();
if (order is null)
{
var badRequest = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequest.WriteStringAsync("Invalid request body.");
return badRequest;
}
try
{
await _orderService.ProcessOrderAsync(order);
var response = req.CreateResponse(HttpStatusCode.Created);
await response.WriteAsJsonAsync(new {
message = "Order processed successfully",
orderId = order.OrderId
});
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing order {OrderId}", order.OrderId);
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteStringAsync("Internal server error.");
return errorResponse;
}
}
}
Reading HTTP request data
// ① Query string: GET /api/orders?status=pending&page=2
string status = req.Query["status"] ?? "all";
int page = int.TryParse(req.Query["page"], out var p) ? p : 1;
// ② HTTP headers
string? userAgent = req.Headers.TryGetValues("User-Agent", out var values)
? values.FirstOrDefault()
: "unknown";
string? authorization = req.Headers.TryGetValues("Authorization", out var authValues)
? authValues.FirstOrDefault()
: null;
// ③ Route parameters: /api/orders/{orderId:guid}
// [HttpTrigger(AuthorizationLevel.Function, "get", Route = "orders/{orderId:guid}")]
// The method receives 'Guid orderId' as an additional parameter
// ④ JSON body with automatic deserialization
var order = await req.ReadFromJsonAsync<OrderModel>();
// ⑤ Raw body as string
string rawBody = await req.ReadAsStringAsync() ?? string.Empty;
// ⑥ Body as stream (for large files)
using var reader = new StreamReader(req.Body);
string content = await reader.ReadToEndAsync();
Multiple Output Bindings (advanced pattern)
When an HTTP function needs to return both an HTTP response and send data to other services, a multiple result class is used:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
namespace GloboTicket.Functions;
/// <summary>
/// Multiple output class: HTTP response + Queue message + Cosmos DB document
/// </summary>
public class NewOrderOutput
{
[HttpResult]
public required HttpResponseData HttpResponse { get; set; }
[QueueOutput("new-orders", Connection = "AzureWebJobsStorage")]
public OrderQueueMessage? QueueMessage { get; set; }
[CosmosDBOutput(
databaseName: "GloboTicketDB",
containerName: "Orders",
Connection = "CosmosDBConnection",
CreateIfNotExists = true)]
public OrderDocument? CosmosDocument { get; set; }
}
public class NewPurchaseWebhookFunction
{
private readonly ILogger<NewPurchaseWebhookFunction> _logger;
public NewPurchaseWebhookFunction(ILogger<NewPurchaseWebhookFunction> logger)
{
_logger = logger;
}
[Function("NewPurchaseWebhook")]
public async Task<NewOrderOutput> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "purchases")]
HttpRequestData req)
{
var payment = await req.ReadFromJsonAsync<PaymentNotification>();
if (payment is null)
{
var errorResp = req.CreateResponse(System.Net.HttpStatusCode.BadRequest);
return new NewOrderOutput { HttpResponse = errorResp };
}
_logger.LogInformation("Payment received for order {OrderId}", payment.OrderId);
var successResp = req.CreateResponse(System.Net.HttpStatusCode.OK);
await successResp.WriteAsJsonAsync(new { received = true });
return new NewOrderOutput
{
HttpResponse = successResp,
QueueMessage = new OrderQueueMessage
{
OrderId = payment.OrderId,
CustomerEmail = payment.CustomerEmail,
TotalAmount = payment.Amount
},
CosmosDocument = new OrderDocument
{
Id = payment.OrderId.ToString(),
OrderId = payment.OrderId,
Status = "Received",
CreatedAt = DateTime.UtcNow,
Amount = payment.Amount
}
};
}
}
Authorization levels and HTTP security
| Level | Description | Key delivery method | Use case |
|---|---|---|---|
| Anonymous | No key required | N/A | Public webhooks, health checks, public endpoints |
| Function | Key specific to this function | ?code=<key> or x-functions-key header | B2B integrations, secure webhooks |
| Admin | Application master key | ?code=<master-key> or x-functions-key header | Administration only – avoid in production! |
Managing keys via Azure CLI:
# List function keys
az functionapp keys list \
--name MyFunctionApp \
--resource-group MyResourceGroup
# Create a new function key
az functionapp function keys set \
--name MyFunctionApp \
--resource-group MyResourceGroup \
--function-name NewPurchaseWebhook \
--key-name my-integration-key
# Delete a compromised key
az functionapp function keys delete \
--name MyFunctionApp \
--resource-group MyResourceGroup \
--function-name NewPurchaseWebhook \
--key-name compromised-key
CORS Configuration
// host.json - Local CORS configuration
{
"version": "2.0",
"extensions": {
"http": {
"routePrefix": "api"
}
}
}
# CORS configuration via Azure CLI for production
az functionapp cors add \
--name MyFunctionApp \
--resource-group MyResourceGroup \
--allowed-origins "https://www.mysite.com" "https://app.mysite.com"
# Check CORS configuration
az functionapp cors show \
--name MyFunctionApp \
--resource-group MyResourceGroup
ASP.NET Core Integration (advanced alternative)
For scenarios where you prefer to use the full ASP.NET Core ecosystem (middleware, model binding, filters), the isolated worker model supports ASP.NET Core integration:
<!-- Add to .csproj -->
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.3.2" />
// Program.cs with ASP.NET Core integration
var host = new HostBuilder()
.ConfigureFunctionsWebApplication(worker =>
{
// Standard ASP.NET Core middleware
worker.UseMiddleware<RequestLoggingMiddleware>();
worker.UseMiddleware<AuthenticationMiddleware>();
})
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
})
.Build();
// With ASP.NET Core Integration - using ASP.NET Core types
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
public class OrderController
{
[Function("GetOrder")]
public async Task<IActionResult> GetOrder(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "orders/{id:guid}")]
HttpRequest req,
Guid id,
ILogger logger)
{
// Full access to ASP.NET Core features
var order = await _orderRepository.GetByIdAsync(id);
if (order is null)
return new NotFoundResult();
return new OkObjectResult(order);
}
}
Module 3 – Exploring Azure Functions Triggers {#module-3}
Overview of all available triggers
Azure Functions offers a wide range of triggers that integrate with many Azure services. Here is a complete table of supported triggers:
| Trigger | NuGet Package | Description | Use case |
|---|---|---|---|
| HTTP | Extensions.Http | HTTP/HTTPS requests | REST APIs, webhooks |
| Timer | Extensions.Timer | Scheduled execution (cron) | Periodic tasks, maintenance |
| Azure Storage Queue | Extensions.Storage.Queues | Messages on Azure Queue Storage | Lightweight async processing |
| Azure Service Bus | Extensions.ServiceBus | Service Bus messages | Enterprise messaging, pub/sub |
| Azure Event Grid | Extensions.EventGrid | Event Grid events | Reacting to Azure events |
| Azure Event Hubs | Extensions.EventHubs | High-performance event streams | IoT, telemetry, streaming |
| Azure Blob Storage | Extensions.Storage.Blobs | Blob creation/modification | File processing |
| Azure Cosmos DB | Extensions.CosmosDB | Changes in Cosmos DB | Synchronization, ETL |
| Azure SQL | Extensions.Sql | Changes in Azure SQL | CDC, synchronization |
| RabbitMQ | Extensions.RabbitMQ | RabbitMQ messages | On-premise integration |
| IoT Hub | Extensions.EventHubs | IoT events | IoT processing |
| SignalR | Extensions.SignalR | SignalR messages | Real-time notifications |
Timer Trigger – Scheduled functions
flowchart LR
A[Azure Timer Service] -->|"Cron: '0 0 2 * * *'"| B[Timer Triggered Function]
B --> C[Business processing]
C --> D[Azure SQL Database]
C --> E[Send report by email]
C --> F[Azure Blob Storage cleanup]
style A fill:#0078D4,color:#fff
style B fill:#68217A,color:#fff
CRON expression syntax for Azure Functions:
Azure Functions uses a 6-field CRON expression (different from the standard 5-field syntax):
{seconds} {minutes} {hours} {day-of-month} {month} {day-of-week}
| Expression | Description |
|---|---|
0 */5 * * * * | Every 5 minutes |
0 0 * * * * | Every hour (at minute 0) |
0 0 2 * * * | Every day at 2:00 AM |
0 30 9 * * 1-5 | Monday through Friday at 9:30 |
0 0 12 1 * * | First of every month at noon |
0 0 0 * * 0 | Every Sunday at midnight |
0 0 9,18 * * * | Every day at 9am and 6pm |
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace GloboTicket.Functions;
public class NightlyReportFunction
{
private readonly ILogger<NightlyReportFunction> _logger;
private readonly IReportingService _reportingService;
private readonly IEmailService _emailService;
public NightlyReportFunction(
ILogger<NightlyReportFunction> logger,
IReportingService reportingService,
IEmailService emailService)
{
_logger = logger;
_reportingService = reportingService;
_emailService = emailService;
}
/// <summary>
/// Runs every day at 2:00 AM UTC to generate the sales report
/// </summary>
[Function("NightlyReport")]
public async Task Run(
[TimerTrigger("0 0 2 * * *")] TimerInfo timerInfo)
{
_logger.LogInformation("Nightly report started at {Time}", DateTime.UtcNow);
// Check if the previous execution was missed
if (timerInfo.ScheduleStatus?.Last != null)
{
var lastRun = timerInfo.ScheduleStatus.Last;
_logger.LogInformation("Last execution: {LastRun}", lastRun);
if (timerInfo.IsPastDue)
{
_logger.LogWarning("Previous execution was missed!");
}
}
var yesterday = DateTime.UtcNow.Date.AddDays(-1);
try
{
var report = await _reportingService.GenerateDailySalesReportAsync(yesterday);
await _emailService.SendReportAsync(report, "management@globoticket.com");
_logger.LogInformation(
"Report sent. Sales: {TotalSales:C}, Tickets: {TicketCount}",
report.TotalSales,
report.TicketCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate report for {Date}", yesterday);
throw; // Allows Azure to capture the exception in Application Insights
}
}
}
Queue Storage Trigger – Asynchronous message processing
The Queue Storage trigger allows your function to process messages posted to an Azure Storage queue. This is a fundamental pattern for creating resilient asynchronous architectures.
sequenceDiagram
participant HTTP as HTTP Webhook Function
participant Queue as Azure Storage Queue
participant QF as Queue Triggered Function
participant DB as Database
participant Email as Email Service
HTTP->>Queue: Posts OrderQueueMessage
HTTP-->>Client: HTTP 200 (fast response)
Note over Queue: Message waiting to be processed
Queue->>QF: Triggers with message
QF->>DB: Saves order details
QF->>Email: Sends confirmation email
QF-->>Queue: Deletes message (success)
Note over Queue,QF: On error: automatic retry up to maxDequeueCount
Message model:
public record OrderQueueMessage
{
public Guid OrderId { get; init; }
public string CustomerEmail { get; init; } = string.Empty;
public decimal TotalAmount { get; init; }
public List<int> SeatNumbers { get; init; } = new();
public DateTime OrderDate { get; init; } = DateTime.UtcNow;
}
Processing function:
using Azure.Storage.Queues.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace GloboTicket.Functions;
public class ProcessNewOrderFunction
{
private readonly ILogger<ProcessNewOrderFunction> _logger;
private readonly IOrderProcessor _orderProcessor;
private readonly ITicketGenerator _ticketGenerator;
public ProcessNewOrderFunction(
ILogger<ProcessNewOrderFunction> logger,
IOrderProcessor orderProcessor,
ITicketGenerator ticketGenerator)
{
_logger = logger;
_orderProcessor = orderProcessor;
_ticketGenerator = ticketGenerator;
}
[Function("ProcessNewOrder")]
public async Task Run(
[QueueTrigger("new-orders", Connection = "AzureWebJobsStorage")]
OrderQueueMessage order)
{
_logger.LogInformation(
"Processing order {OrderId} for {Email}",
order.OrderId,
order.CustomerEmail);
// Business processing
await _orderProcessor.SaveOrderToDatabase(order);
// Generate tickets for each seat
foreach (var seatNumber in order.SeatNumbers)
{
await _ticketGenerator.GenerateTicketAsync(order.OrderId, seatNumber);
_logger.LogDebug("Ticket generated for seat {Seat}", seatNumber);
}
_logger.LogInformation(
"Order {OrderId} processed successfully. {TicketCount} tickets generated.",
order.OrderId,
order.SeatNumbers.Count);
}
}
Retry policy configuration in host.json:
{
"version": "2.0",
"extensions": {
"queues": {
"batchSize": 16,
"maxDequeueCount": 5,
"newBatchThreshold": 8,
"visibilityTimeout": "00:00:30",
"messageEncoding": "base64"
}
}
}
Important: When a message exceeds
maxDequeueCountprocessing attempts (default: 5), it is automatically moved to the dead-letter queue (poison queue), named{queue-name}-poison. Make sure to monitor this queue in production!
Blob Storage Trigger
using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace GloboTicket.Functions;
public class ProcessUploadedImageFunction
{
private readonly ILogger<ProcessUploadedImageFunction> _logger;
private readonly IImageProcessor _imageProcessor;
public ProcessUploadedImageFunction(
ILogger<ProcessUploadedImageFunction> logger,
IImageProcessor imageProcessor)
{
_logger = logger;
_imageProcessor = imageProcessor;
}
// Option 1: Read as string (text files)
[Function("ProcessTextBlob")]
public async Task RunAsString(
[BlobTrigger("text-uploads/{name}", Connection = "AzureWebJobsStorage")]
string content,
string name)
{
_logger.LogInformation("Text file received: {Name}, Size: {Size} chars", name, content.Length);
// Process text content...
}
// Option 2: Read as Stream (binary files, images, PDFs)
[Function("ProcessBinaryBlob")]
public async Task RunAsStream(
[BlobTrigger("image-uploads/{name}", Connection = "AzureWebJobsStorage")]
Stream blobStream,
string name,
FunctionContext context)
{
_logger.LogInformation("Image received: {Name}", name);
// For large files, always use Stream (not string or byte[])
await _imageProcessor.ResizeAndOptimizeAsync(blobStream, name);
}
// Option 3: Full access via BlobClient (read + write + delete)
[Function("ProcessBlobWithSdk")]
public async Task RunWithBlobClient(
[BlobTrigger("uploads/{blobName}", Connection = "AzureWebJobsStorage")]
BlobClient blobClient,
string blobName)
{
_logger.LogInformation("Blob detected: {BlobName}", blobName);
// Read metadata
var properties = await blobClient.GetPropertiesAsync();
_logger.LogInformation("Content-Type: {ContentType}", properties.Value.ContentType);
// Read content
using var stream = await blobClient.OpenReadAsync();
// Perform processing...
// Update metadata
await blobClient.SetMetadataAsync(new Dictionary<string, string>
{
["processed"] = "true",
["processedAt"] = DateTime.UtcNow.ToString("O")
});
}
}
Module 4 – Bindings: Connections to Azure Services {#module-4}
Understanding Input and Output Bindings
Azure Functions bindings considerably simplify the code needed to connect to other Azure services. They eliminate the need to write SDK client initialization code, manage connections, and serialize/deserialize data.
flowchart LR
subgraph "Without Bindings (manual code)"
A1[Function] -->|new BlobServiceClient| B1[Storage SDK]
B1 -->|UploadAsync| C1[Blob Storage]
A1 -->|new QueueClient| B2[Queue SDK]
B2 -->|SendMessageAsync| C2[Queue Storage]
end
subgraph "With Bindings (declarative)"
A2[Function] -->|BlobOutput attribute| C3[Blob Storage]
A2 -->|QueueOutput attribute| C4[Queue Storage]
end
style A2 fill:#68217A,color:#fff
style A1 fill:#ccc,color:#000
Types of available bindings:
| Type | Direction | Description |
|---|---|---|
| Input Binding | ← | Read data from a service before execution |
| Output Binding | → | Write data to a service after execution |
| Trigger Binding | ← | Trigger the function AND read initial data |
Output Binding – Queue Storage
// Sending a single message
public class SingleQueueOutputFunction
{
[Function("SendSingleMessage")]
[QueueOutput("my-queue", Connection = "AzureWebJobsStorage")]
public async Task<OrderQueueMessage> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
var order = await req.ReadFromJsonAsync<OrderQueueMessage>();
return order!; // Return the object that will be serialized to JSON and sent to the queue
}
}
// Sending multiple messages
public class MultipleQueueOutputFunction
{
[Function("SendMultipleMessages")]
[QueueOutput("my-queue", Connection = "AzureWebJobsStorage")]
public async Task<IEnumerable<string>> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
// Return an array → each element becomes a separate message
return new[]
{
"Message 1",
"Message 2",
"Message 3"
};
}
}
Input Binding – Reading from Blob Storage
using Azure.Storage.Blobs;
namespace GloboTicket.Functions;
public class GetTicketFunction
{
private readonly ILogger<GetTicketFunction> _logger;
public GetTicketFunction(ILogger<GetTicketFunction> logger)
{
_logger = logger;
}
/// <summary>
/// Retrieves the ticket for a specific order
/// Route: GET /api/tickets/{orderId:guid}
/// </summary>
[Function("GetTicket")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "tickets/{orderId:guid}")]
HttpRequestData req,
[BlobInput("tickets/{orderId}.txt", Connection = "AzureWebJobsStorage")]
string? ticketContent,
Guid orderId)
{
_logger.LogInformation("Retrieving ticket for order {OrderId}", orderId);
if (ticketContent is null)
{
var notFound = req.CreateResponse(System.Net.HttpStatusCode.NotFound);
await notFound.WriteStringAsync($"Ticket not found for order {orderId}");
return notFound;
}
var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
await response.WriteStringAsync(ticketContent);
return response;
}
}
Output Binding – Writing to Blob Storage
namespace GloboTicket.Functions;
/// <summary>
/// Result with two outputs: HTTP response + Blob creation
/// </summary>
public class CreateTicketOutput
{
[HttpResult]
public required HttpResponseData HttpResponse { get; set; }
[BlobOutput("tickets/{orderId}.txt", Connection = "AzureWebJobsStorage")]
public string? TicketContent { get; set; }
}
public class CreateTicketFunction
{
private readonly ILogger<CreateTicketFunction> _logger;
public CreateTicketFunction(ILogger<CreateTicketFunction> logger)
{
_logger = logger;
}
[Function("CreateTicket")]
public async Task<CreateTicketOutput> Run(
[QueueTrigger("ticket-requests", Connection = "AzureWebJobsStorage")]
TicketRequest request,
Guid orderId)
{
_logger.LogInformation("Creating ticket for order {OrderId}", request.OrderId);
var ticketContent = $"""
GLOBOTICKET TICKET
==================
Order: {request.OrderId}
Event: {request.EventName}
Date: {request.EventDate:D}
Seat: {request.SeatNumber}
==================
Generated: {DateTime.UtcNow:O}
""";
var fakeHttpResponse = null as HttpResponseData; // Queue trigger has no HTTP response
return new CreateTicketOutput
{
HttpResponse = fakeHttpResponse!,
TicketContent = ticketContent
};
}
}
Cosmos DB Bindings
using Microsoft.Azure.Cosmos;
namespace GloboTicket.Functions;
public class CosmosDbBindingsExample
{
private readonly ILogger<CosmosDbBindingsExample> _logger;
public CosmosDbBindingsExample(ILogger<CosmosDbBindingsExample> logger)
{
_logger = logger;
}
/// <summary>
/// Cosmos DB input binding: reading a document by ID
/// </summary>
[Function("GetOrderFromCosmos")]
public async Task<HttpResponseData> GetOrder(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "cosmos/orders/{id}")]
HttpRequestData req,
[CosmosDBInput(
databaseName: "GloboTicketDB",
containerName: "Orders",
Connection = "CosmosDBConnection",
Id = "{id}",
PartitionKey = "{id}")]
OrderDocument? order,
string id)
{
if (order is null)
{
var notFound = req.CreateResponse(System.Net.HttpStatusCode.NotFound);
return notFound;
}
var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
await response.WriteAsJsonAsync(order);
return response;
}
/// <summary>
/// Cosmos DB output binding: creating a document
/// </summary>
public class SaveOrderOutput
{
[HttpResult]
public required HttpResponseData HttpResponse { get; set; }
[CosmosDBOutput(
databaseName: "GloboTicketDB",
containerName: "Orders",
Connection = "CosmosDBConnection",
CreateIfNotExists = true,
PartitionKey = "/orderId")]
public OrderDocument? Order { get; set; }
}
[Function("SaveOrder")]
public async Task<SaveOrderOutput> SaveOrder(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "cosmos/orders")]
HttpRequestData req)
{
var order = await req.ReadFromJsonAsync<OrderDocument>();
if (order is null)
{
var badReq = req.CreateResponse(System.Net.HttpStatusCode.BadRequest);
return new SaveOrderOutput { HttpResponse = badReq };
}
order.Id = Guid.NewGuid().ToString();
order.CreatedAt = DateTime.UtcNow;
var response = req.CreateResponse(System.Net.HttpStatusCode.Created);
await response.WriteAsJsonAsync(new { id = order.Id });
return new SaveOrderOutput
{
HttpResponse = response,
Order = order
};
}
}
Dependency Injection with Entity Framework Core
For scenarios requiring Entity Framework Core or other ORMs, standard .NET dependency injection is fully supported:
// Program.cs - EF Core configuration
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices((context, services) =>
{
// EF Core configuration
var connectionString = context.Configuration.GetConnectionString("SqlDatabase")
?? Environment.GetEnvironmentVariable("SqlConnection");
services.AddDbContext<GloboTicketDbContext>(options =>
options.UseSqlServer(connectionString),
ServiceLifetime.Transient); // Transient is recommended for Functions
services.AddTransient<IOrderRepository, OrderRepository>();
services.AddTransient<ITicketRepository, TicketRepository>();
services.AddApplicationInsightsTelemetryWorkerService();
})
.Build();
// Usage in a function
public class OrderDataFunction
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<OrderDataFunction> _logger;
public OrderDataFunction(
IOrderRepository orderRepository,
ILogger<OrderDataFunction> logger)
{
_orderRepository = orderRepository;
_logger = logger;
}
[Function("GetAllOrders")]
public async Task<HttpResponseData> GetAllOrders(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "data/orders")]
HttpRequestData req)
{
var orders = await _orderRepository.GetAllAsync();
var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
await response.WriteAsJsonAsync(orders);
return response;
}
}
Module 5 – Deployment to Azure {#module-5}
Infrastructure required for Azure Functions
Before deploying code, you need to provision the necessary infrastructure:
flowchart TD
RG[Resource Group] --> SA[Storage Account\nrequired]
RG --> AI[Application Insights\nrecommended]
RG --> HP[Hosting Plan\nConsumption/Premium/Dedicated]
RG --> FA[Function App]
FA --> SA
FA --> AI
FA --> HP
FA -.->|optional| CDB[Cosmos DB]
FA -.->|optional| SB[Service Bus]
FA -.->|optional| KV[Key Vault]
style FA fill:#68217A,color:#fff
style RG fill:#0078D4,color:#fff
Deployment via Azure CLI (complete script)
#!/bin/bash
# Complete deployment script for Azure Functions
# Variables
RESOURCE_GROUP="rg-globoticket-prod"
LOCATION="eastus"
STORAGE_ACCOUNT="stgloboticket$(openssl rand -hex 4)" # Unique name
FUNCTION_APP="func-globoticket-prod"
APP_INSIGHTS="ai-globoticket-prod"
COSMOS_ACCOUNT="cosmos-globoticket-prod"
COSMOS_DB="GloboTicketDB"
echo "=== Azure login ==="
az login
az account set --subscription "My Production Subscription"
echo "=== Creating resource group ==="
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION
echo "=== Creating storage account ==="
az storage account create \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--sku Standard_LRS \
--kind StorageV2
echo "=== Creating Application Insights ==="
az monitor app-insights component create \
--app $APP_INSIGHTS \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--kind web \
--application-type web
# Retrieve Application Insights connection string
AI_CONNECTION_STRING=$(az monitor app-insights component show \
--app $APP_INSIGHTS \
--resource-group $RESOURCE_GROUP \
--query connectionString -o tsv)
echo "=== Creating Function App (Consumption plan) ==="
az functionapp create \
--name $FUNCTION_APP \
--resource-group $RESOURCE_GROUP \
--storage-account $STORAGE_ACCOUNT \
--consumption-plan-location $LOCATION \
--runtime dotnet-isolated \
--runtime-version 8 \
--functions-version 4 \
--app-insights-key $AI_CONNECTION_STRING
echo "=== Creating Cosmos DB ==="
az cosmosdb create \
--name $COSMOS_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--default-consistency-level Eventual \
--kind GlobalDocumentDB
az cosmosdb sql database create \
--account-name $COSMOS_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--name $COSMOS_DB
az cosmosdb sql container create \
--account-name $COSMOS_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--database-name $COSMOS_DB \
--name Orders \
--partition-key-path "/orderId"
# Retrieve Cosmos DB connection string
COSMOS_CONNECTION=$(az cosmosdb keys list \
--name $COSMOS_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--type connection-strings \
--query "connectionStrings[0].connectionString" -o tsv)
echo "=== Configuring App Settings ==="
az functionapp config appsettings set \
--name $FUNCTION_APP \
--resource-group $RESOURCE_GROUP \
--settings \
"CosmosDBConnection=$COSMOS_CONNECTION" \
"APPLICATIONINSIGHTS_CONNECTION_STRING=$AI_CONNECTION_STRING" \
"ASPNETCORE_ENVIRONMENT=Production"
echo "=== Deploying code ==="
func azure functionapp publish $FUNCTION_APP --dotnet-isolated
echo "=== Deployment complete! ==="
echo "URL: https://$FUNCTION_APP.azurewebsites.net"
Deployment with Bicep (Infrastructure as Code)
// main.bicep - Complete infrastructure definition
@description('Base name for resources')
param baseName string = 'globoticket'
@description('Environment (dev, staging, prod)')
@allowed(['dev', 'staging', 'prod'])
param environment string = 'dev'
@description('Azure region')
param location string = resourceGroup().location
// Variables
var storageAccountName = 'st${baseName}${environment}'
var functionAppName = 'func-${baseName}-${environment}'
var appServicePlanName = 'asp-${baseName}-${environment}'
var appInsightsName = 'ai-${baseName}-${environment}'
// Storage Account
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
}
}
// Application Insights
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: appInsightsName
location: location
kind: 'web'
properties: {
Application_Type: 'web'
RetentionInDays: 30
}
}
// Hosting Plan (Consumption)
resource hostingPlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: appServicePlanName
location: location
sku: {
name: 'Y1'
tier: 'Dynamic'
}
properties: {
reserved: false
}
}
// Function App
resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
name: functionAppName
location: location
kind: 'functionapp'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: hostingPlan.id
siteConfig: {
appSettings: [
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${az.environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
}
{
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${az.environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsights.properties.ConnectionString
}
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'dotnet-isolated'
}
{
name: 'DOTNET_ISOLATED_JITCACHING'
value: '1'
}
]
ftpsState: 'Disabled'
minTlsVersion: '1.2'
netFrameworkVersion: 'v8.0'
}
httpsOnly: true
}
}
// Outputs
output functionAppUrl string = 'https://${functionApp.properties.defaultHostName}'
output functionAppName string = functionApp.name
output storageAccountName string = storageAccount.name
CI/CD Pipelines
GitHub Actions
# .github/workflows/deploy-functions.yml
name: Deploy Azure Functions
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AZURE_FUNCTIONAPP_NAME: 'func-globoticket-prod'
AZURE_FUNCTIONAPP_PACKAGE_PATH: '.'
DOTNET_VERSION: '8.0.x'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET ${{ env.DOTNET_VERSION }}
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Unit tests
run: dotnet test --no-build --verbosity normal --configuration Release
- name: Publish
run: dotnet publish --configuration Release --output ./output
- name: Azure login
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Functions
uses: Azure/functions-action@v1
with:
app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }}
package: './output'
- name: Verify deployment
run: |
sleep 30
STATUS=$(az functionapp show \
--name ${{ env.AZURE_FUNCTIONAPP_NAME }} \
--resource-group rg-globoticket-prod \
--query 'state' -o tsv)
echo "Function App state: $STATUS"
if [ "$STATUS" != "Running" ]; then
echo "ERROR: Function App is not running"
exit 1
fi
Azure DevOps Pipeline
# azure-pipelines.yml
trigger:
branches:
include:
- main
- release/*
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.0.x'
functionAppName: 'func-globoticket-prod'
resourceGroup: 'rg-globoticket-prod'
stages:
- stage: Build
displayName: 'Build and Tests'
jobs:
- job: BuildJob
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
version: $(dotnetVersion)
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: 'restore'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: 'Tests'
inputs:
command: 'test'
arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage"'
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'functions-app'
- stage: Deploy
displayName: 'Production Deployment'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: DeployFunctions
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureFunctionApp@2
inputs:
azureSubscription: 'Azure Production'
appType: 'functionApp'
appName: $(functionAppName)
package: '$(Pipeline.Workspace)/functions-app/**/*.zip'
deploymentMethod: 'zipDeploy'
Module 6 – Durable Functions: Resilient Workflows {#module-6}
What are Durable Functions?
Durable Functions is an Azure Functions extension that enables creating stateful workflows in a serverless environment. It solves a fundamental problem: how to reliably orchestrate long, complex, and potentially distributed operations?
flowchart TD
subgraph "Without Durable Functions"
A1[Function 1] -->|Queue Message| B1[Function 2]
B1 -->|Queue Message| C1[Function 3]
B1 -->|Which error?| E1[Hard to debug]
E1 -->|No overview| F1[State lost on failure]
end
subgraph "With Durable Functions"
A2[Starter Function] -->|Creates orchestration| B2[Orchestrator Function]
B2 -->|CallActivityAsync| C2[Activity 1]
B2 -->|CallActivityAsync| D2[Activity 2]
B2 -->|WaitForExternalEvent| E2[Manual approval]
B2 -->|State saved| F2[Task Hub Storage]
F2 -->|Automatic resume| B2
end
style B2 fill:#68217A,color:#fff
style F2 fill:#0078D4,color:#fff
Durable Functions workflow patterns
Pattern 1: Function Chaining (sequential)
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
namespace GloboTicket.Functions.Durable;
public class OrderOrchestration
{
private readonly ILogger<OrderOrchestration> _logger;
public OrderOrchestration(ILogger<OrderOrchestration> logger)
{
_logger = logger;
}
/// <summary>
/// Main orchestrator for the order processing workflow
/// IMPORTANT RULE: Orchestrators must be deterministic!
/// - No direct I/O (no HTTP calls, no DateTime.Now, no Guid.NewGuid())
/// - Use context.CurrentUtcDateTime instead of DateTime.Now
/// - Use context.NewGuid() instead of Guid.NewGuid()
/// </summary>
[Function(nameof(OrderOrchestration))]
public async Task<OrderWorkflowResult> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var input = context.GetInput<OrderOrchestrationInput>()!;
var logger = context.CreateReplaySafeLogger(nameof(OrderOrchestration));
logger.LogInformation("Starting order workflow {OrderId}", input.OrderId);
try
{
// Step 1: Validate seat availability
var availability = await context.CallActivityAsync<AvailabilityResult>(
nameof(CheckSeatAvailability),
input);
if (!availability.IsAvailable)
{
return new OrderWorkflowResult
{
Success = false,
Message = "Seats not available"
};
}
// Step 2: Reserve seats
await context.CallActivityAsync(nameof(ReserveSeats), input);
// Step 3: Generate tickets
var tickets = await context.CallActivityAsync<List<string>>(
nameof(GenerateTickets),
input);
// Step 4: Send tickets by email
await context.CallActivityAsync(nameof(SendTicketsByEmail), new EmailRequest
{
OrderId = input.OrderId,
CustomerEmail = input.CustomerEmail,
TicketUrls = tickets
});
return new OrderWorkflowResult
{
Success = true,
TicketCount = tickets.Count,
Message = "Order processed successfully"
};
}
catch (Exception ex)
{
logger.LogError(ex, "Error in workflow {OrderId}", input.OrderId);
// Compensation attempt
await context.CallActivityAsync(nameof(CancelReservation), input.OrderId);
return new OrderWorkflowResult
{
Success = false,
Message = $"Error: {ex.Message}"
};
}
}
// ===================== ACTIVITY FUNCTIONS =====================
[Function(nameof(CheckSeatAvailability))]
public async Task<AvailabilityResult> CheckSeatAvailability(
[ActivityTrigger] OrderOrchestrationInput input,
FunctionContext executionContext)
{
_logger.LogInformation("Checking availability for {OrderId}", input.OrderId);
// Activities can do I/O, HTTP calls, etc.
// ... call to reservation service
return new AvailabilityResult { IsAvailable = true };
}
[Function(nameof(ReserveSeats))]
public async Task ReserveSeats(
[ActivityTrigger] OrderOrchestrationInput input,
FunctionContext executionContext)
{
_logger.LogInformation("Reserving seats for order {OrderId}", input.OrderId);
// ... reservation logic
}
[Function(nameof(GenerateTickets))]
public async Task<List<string>> GenerateTickets(
[ActivityTrigger] OrderOrchestrationInput input,
FunctionContext executionContext)
{
_logger.LogInformation("Generating {Count} tickets", input.SeatNumbers.Count);
var ticketUrls = new List<string>();
foreach (var seat in input.SeatNumbers)
{
var url = $"https://tickets.globoticket.com/{input.OrderId}/seat/{seat}";
ticketUrls.Add(url);
}
return ticketUrls;
}
[Function(nameof(SendTicketsByEmail))]
public async Task SendTicketsByEmail(
[ActivityTrigger] EmailRequest request,
FunctionContext executionContext)
{
_logger.LogInformation("Sending tickets to {Email}", request.CustomerEmail);
// ... email sending via SendGrid
}
[Function(nameof(CancelReservation))]
public async Task CancelReservation(
[ActivityTrigger] Guid orderId,
FunctionContext executionContext)
{
_logger.LogWarning("Cancelling reservation {OrderId}", orderId);
// ... compensation logic
}
// ===================== STARTER FUNCTION =====================
[Function("StartOrderWorkflow")]
public async Task<HttpResponseData> HttpStart(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "workflows/orders")]
HttpRequestData req,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext)
{
var input = await req.ReadFromJsonAsync<OrderOrchestrationInput>();
// Start the orchestration with a custom ID based on the OrderId
var instanceId = $"order-{input!.OrderId}";
await client.ScheduleNewOrchestrationInstanceAsync(
nameof(OrderOrchestration),
input,
new StartOrchestrationOptions { InstanceId = instanceId });
_logger.LogInformation("Orchestration started: {InstanceId}", instanceId);
// Return URLs to check the status
return await client.CreateCheckStatusResponseAsync(req, instanceId);
}
}
Pattern 2: Fan-Out / Fan-In (Parallelism)
[Function(nameof(ParallelTicketGeneration))]
public async Task<List<string>> RunParallelOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var input = context.GetInput<OrderOrchestrationInput>()!;
// Fan-Out: Launch all generations in parallel
var ticketTasks = input.SeatNumbers
.Select(seat => context.CallActivityAsync<string>(
nameof(GenerateSingleTicket),
new TicketRequest { OrderId = input.OrderId, SeatNumber = seat }))
.ToList();
// Fan-In: Wait for ALL activities to complete
var ticketUrls = await Task.WhenAll(ticketTasks);
return ticketUrls.ToList();
}
Pattern 3: Waiting for an external event with timeout
[Function(nameof(ApprovalWorkflow))]
public async Task<ApprovalResult> RunApprovalOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var input = context.GetInput<OrderOrchestrationInput>()!;
var logger = context.CreateReplaySafeLogger(nameof(ApprovalWorkflow));
// Check if manual approval is required (orders > $500)
if (input.TotalPrice >= 500)
{
logger.LogInformation("Order {OrderId} requires approval (amount: {Amount:C})",
input.OrderId, input.TotalPrice);
// Notify the approver
await context.CallActivityAsync(nameof(RequestApproval), input);
// Define a timeout (72 hours)
var deadline = context.CurrentUtcDateTime.AddHours(72);
using var timeoutCts = new CancellationTokenSource();
// Wait for approval OR timeout (whichever comes first)
var approvalTask = context.WaitForExternalEvent<ApprovalDecision>("OrderApproval");
var timeoutTask = context.CreateTimer(deadline, timeoutCts.Token);
var winner = await Task.WhenAny(approvalTask, timeoutTask);
if (winner == approvalTask)
{
timeoutCts.Cancel(); // Cancel the timer
var decision = await approvalTask;
if (!decision.Approved)
{
return new ApprovalResult { Approved = false, Reason = decision.Reason };
}
logger.LogInformation("Order approved by {Approver}", decision.ApprovedBy);
}
else
{
logger.LogWarning("Timeout: approval not received for order {OrderId}", input.OrderId);
return new ApprovalResult { Approved = false, Reason = "Approval timeout" };
}
}
// Continue processing...
return new ApprovalResult { Approved = true };
}
/// <summary>
/// Endpoint for submitting an approval decision
/// </summary>
[Function("ApproveOrder")]
public async Task<HttpResponseData> ApproveOrder(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "orders/{orderId}/approve")]
HttpRequestData req,
[DurableClient] DurableTaskClient client,
string orderId)
{
var decision = await req.ReadFromJsonAsync<ApprovalDecision>();
var instanceId = $"order-{orderId}";
// Send the event to the running orchestration
await client.RaiseEventAsync(instanceId, "OrderApproval", decision);
var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
await response.WriteStringAsync($"Decision submitted for order {orderId}");
return response;
}
Pattern 4: Eternal Orchestrations (continuous monitoring)
/// <summary>
/// Eternal orchestration: monitors pending orders every 30 minutes
/// </summary>
[Function(nameof(OrderMonitorOrchestration))]
public async Task RunMonitor(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var orderId = context.GetInput<Guid>();
var logger = context.CreateReplaySafeLogger(nameof(OrderMonitorOrchestration));
while (true)
{
var status = await context.CallActivityAsync<string>(nameof(CheckOrderStatus), orderId);
logger.LogInformation("Order {OrderId} status: {Status}", orderId, status);
if (status == "Completed" || status == "Cancelled")
{
logger.LogInformation("Monitoring ended for order {OrderId}", orderId);
return; // End of eternal orchestration
}
// Wait 30 minutes before next check
var nextCheck = context.CurrentUtcDateTime.AddMinutes(30);
await context.CreateTimer(nextCheck, CancellationToken.None);
// The orchestrator will wake up and resume here
context.ContinueAsNew(orderId); // Restart with clean history
return; // Required after ContinueAsNew
}
}
Monitoring Orchestrations with the HTTP API
// Check the state of an orchestration
[Function("GetOrchestrationStatus")]
public async Task<HttpResponseData> GetStatus(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "orchestrations/{instanceId}")]
HttpRequestData req,
[DurableClient] DurableTaskClient client,
string instanceId)
{
var metadata = await client.GetInstanceAsync(instanceId, getInputsAndOutputs: true);
if (metadata is null)
{
var notFound = req.CreateResponse(System.Net.HttpStatusCode.NotFound);
await notFound.WriteStringAsync($"Orchestration {instanceId} not found");
return notFound;
}
var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
{
instanceId = metadata.InstanceId,
status = metadata.RuntimeStatus.ToString(),
createdAt = metadata.CreatedAt,
lastUpdated = metadata.LastUpdatedAt,
output = metadata.SerializedOutput
});
return response;
}
// Terminate an orchestration
[Function("TerminateOrchestration")]
public async Task<HttpResponseData> Terminate(
[HttpTrigger(AuthorizationLevel.Admin, "post", Route = "orchestrations/{instanceId}/terminate")]
HttpRequestData req,
[DurableClient] DurableTaskClient client,
string instanceId)
{
await client.TerminateInstanceAsync(instanceId, "Manually terminated by administrator");
var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
await response.WriteStringAsync($"Orchestration {instanceId} terminated");
return response;
}
Task Hubs and Storage Providers
| Storage Provider | Use case | Advantages | Disadvantages |
|---|---|---|---|
| Azure Storage (default) | Most cases | Simple, no extra config, low cost | Limited performance for very high load |
| Netherite | High performance | High throughput, low latency | More complex configuration, Event Hubs cost |
| Microsoft SQL Server | Existing SQL environments | Full control, standard backup, multi-tenancy | Requires SQL Server, infrastructure management |
Module 7 – Azure Functions in Production {#module-7}
Observability and Application Insights
Azure Application Insights is the primary monitoring tool for Azure Functions. It automatically collects:
- Requests: each function invocation
- Dependencies: outgoing HTTP calls, SQL queries, etc.
- Exceptions: all unhandled exceptions
- Traces: ILogger logs
- Custom metrics: via TelemetryClient
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace GloboTicket.Functions;
public class ObservableFunction
{
private readonly ILogger<ObservableFunction> _logger;
private readonly TelemetryClient _telemetryClient;
public ObservableFunction(
ILogger<ObservableFunction> logger,
TelemetryClient telemetryClient)
{
_logger = logger;
_telemetryClient = telemetryClient;
}
[Function("ProcessOrderWithTelemetry")]
public async Task Run(
[QueueTrigger("orders", Connection = "AzureWebJobsStorage")]
OrderQueueMessage order)
{
// Structured log with properties
using (_logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = order.OrderId,
["CustomerEmail"] = order.CustomerEmail
}))
{
_logger.LogInformation("Starting order processing {OrderId}", order.OrderId);
// Custom metric
_telemetryClient.TrackMetric("OrderAmount", (double)order.TotalAmount);
// Custom event
_telemetryClient.TrackEvent("OrderProcessingStarted", new Dictionary<string, string>
{
["OrderId"] = order.OrderId.ToString(),
["TicketCount"] = order.SeatNumbers.Count.ToString()
});
try
{
// Measure the duration of a critical operation
using var operation = _telemetryClient.StartOperation<DependencyTelemetry>("GenerateTickets");
operation.Telemetry.Type = "CustomOperation";
// ... business logic
operation.Telemetry.Success = true;
_logger.LogInformation("Order {OrderId} processed in {ElapsedMs}ms",
order.OrderId, operation.Telemetry.Duration.TotalMilliseconds);
}
catch (Exception ex)
{
_telemetryClient.TrackException(ex, new Dictionary<string, string>
{
["OrderId"] = order.OrderId.ToString()
});
_logger.LogError(ex, "Failed to process order {OrderId}", order.OrderId);
throw;
}
}
}
}
KQL queries for Azure Monitor:
// Show functions that failed in the last 24 hours
requests
| where timestamp > ago(24h)
| where success == false
| summarize FailureCount = count() by name
| order by FailureCount desc
// Analyze response times by function
requests
| where timestamp > ago(1h)
| summarize
AvgDuration = avg(duration),
P95Duration = percentile(duration, 95),
P99Duration = percentile(duration, 99),
Count = count()
by name
| order by P95Duration desc
// Detect most frequent exceptions
exceptions
| where timestamp > ago(24h)
| summarize ExceptionCount = count() by type, outerMessage
| order by ExceptionCount desc
| take 20
// Trace Durable Functions workflow
customEvents
| where name == "OrderProcessingStarted"
| join kind=leftouter (
customEvents | where name == "OrderProcessingCompleted"
) on $left.customDimensions["OrderId"] == $right.customDimensions["OrderId"]
| project OrderId = customDimensions["OrderId"],
StartTime = timestamp,
EndTime = timestamp1,
Duration = (timestamp1 - timestamp) / 1ms
Configuring alerts
# Create an alert on function failures
az monitor metrics alert create \
--name "FunctionFailureAlert" \
--resource-group rg-globoticket-prod \
--scopes "/subscriptions/{sub}/resourceGroups/rg-globoticket-prod/providers/Microsoft.Web/sites/func-globoticket-prod" \
--condition "count requests where success false > 10" \
--window-size 5m \
--evaluation-frequency 1m \
--action-groups "/subscriptions/{sub}/resourceGroups/rg-globoticket-prod/providers/microsoft.insights/actionGroups/OpsTeam" \
--description "Alert: More than 10 failures in 5 minutes"
Module 8 – Advanced Security {#module-8}
Identity-based Connections
Instead of using connection strings containing secrets, identity-based connections use Azure Managed Identities and role-based access control (RBAC).
flowchart LR
FA[Function App\nManaged Identity] -->|MSI Authentication| AAD[Microsoft Entra ID]
AAD -->|JWT Token| FA
FA -->|Token + Request| Storage[Azure Storage]
FA -->|Token + Request| SB[Service Bus]
FA -->|Token + Request| KV[Key Vault]
subgraph "RBAC Assignments"
Storage --- R1[Storage Blob Data Contributor]
SB --- R2[Service Bus Data Receiver]
KV --- R3[Key Vault Secrets User]
end
style FA fill:#68217A,color:#fff
style AAD fill:#0078D4,color:#fff
Configuration for Azure Blob Storage without connection string:
# 1. Enable System-Assigned Managed Identity
az functionapp identity assign \
--name func-globoticket-prod \
--resource-group rg-globoticket-prod
# Retrieve the Principal ID of the Managed Identity
PRINCIPAL_ID=$(az functionapp identity show \
--name func-globoticket-prod \
--resource-group rg-globoticket-prod \
--query principalId -o tsv)
# 2. Assign RBAC role on the Storage Account
STORAGE_RESOURCE_ID=$(az storage account show \
--name stgloboticket \
--resource-group rg-globoticket-prod \
--query id -o tsv)
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Storage Blob Data Contributor" \
--scope $STORAGE_RESOURCE_ID
# 3. Configure App Setting (no secret!)
az functionapp config appsettings set \
--name func-globoticket-prod \
--resource-group rg-globoticket-prod \
--settings "MyStorageConnection__blobServiceUri=https://stgloboticket.blob.core.windows.net"
In code (no changes needed):
// The binding uses the connection named "MyStorageConnection"
// Azure Functions automatically detects that it's an identity-based connection
[BlobTrigger("uploads/{name}", Connection = "MyStorageConnection")]
public async Task ProcessBlob(Stream blobStream, string name) { ... }
Azure Key Vault References
Key Vault References allow storing secrets in Azure Key Vault and referencing them in App Settings without ever exposing the secret value.
# 1. Create the Key Vault
az keyvault create \
--name kv-globoticket-prod \
--resource-group rg-globoticket-prod \
--location eastus
# 2. Grant access to the Function App
az keyvault set-policy \
--name kv-globoticket-prod \
--object-id $PRINCIPAL_ID \
--secret-permissions get list
# 3. Add a secret
az keyvault secret set \
--vault-name kv-globoticket-prod \
--name "SqlConnectionString" \
--value "Server=tcp:sql-globoticket.database.windows.net;Database=GloboTicket;User Id=admin;Password=S3cur3P@ssw0rd!"
# 4. Retrieve the secret URI
SECRET_URI=$(az keyvault secret show \
--vault-name kv-globoticket-prod \
--name SqlConnectionString \
--query id -o tsv)
# 5. Reference the secret in App Settings
az functionapp config appsettings set \
--name func-globoticket-prod \
--resource-group rg-globoticket-prod \
--settings "SqlConnection=@Microsoft.KeyVault(SecretUri=$SECRET_URI)"
Azure API Management (APIM) as security layer
flowchart LR
Internet[Internet\nClients] --> APIM[Azure API Management]
APIM -->|Rate limiting| RLimit[Policy: 1000 req/min]
APIM -->|OAuth2 validation| Auth[JWT Validation]
APIM -->|IP filtering| IP[IP Filtering]
APIM -->|Transformation| AF[Azure Functions]
AF -->|Private network| DB["(Database)"]
style APIM fill:#0078D4,color:#fff
style AF fill:#68217A,color:#fff
APIM policy in XML:
<policies>
<inbound>
<!-- JWT token validation -->
<validate-jwt header-name="Authorization"
failed-validation-httpcode="401"
failed-validation-error-message="Invalid token">
<openid-config url="https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration"/>
<audiences>
<audience>api://globoticket-api</audience>
</audiences>
</validate-jwt>
<!-- Rate limiting -->
<rate-limit-by-key calls="1000"
renewal-period="60"
counter-key="@(context.Subscription.Id)"/>
<!-- Remove the x-functions-key header (don't expose it to clients) -->
<set-header name="x-functions-key" exists-action="override">
<value>{{function-app-key}}</value>
</set-header>
</inbound>
<backend>
<forward-request/>
</backend>
<outbound>
<!-- Remove sensitive headers -->
<set-header name="x-powered-by" exists-action="delete"/>
<set-header name="x-aspnet-version" exists-action="delete"/>
</outbound>
</policies>
Virtual Network Integration
# Enable VNet integration on the Function App (Premium plan required)
az functionapp vnet-integration add \
--name func-globoticket-prod \
--resource-group rg-globoticket-prod \
--vnet vnet-globoticket \
--subnet subnet-functions
# Restrict inbound access to VNet only
az functionapp config access-restriction add \
--name func-globoticket-prod \
--resource-group rg-globoticket-prod \
--rule-name "AllowVNetOnly" \
--action Allow \
--vnet-name vnet-globoticket \
--subnet subnet-apim \
--priority 100
Module 9 – Monitoring and Observability in Production {#module-9}
Azure Static Web Apps + Azure Functions
Azure Static Web Apps offers native integration with Azure Functions for hosting front-end applications (React, Angular, Vue, Blazor WASM) with a serverless backend.
# staticwebapp.config.json
{
"navigationFallback": {
"rewrite": "/index.html"
},
"routes": [
{
"route": "/api/*",
"allowedRoles": ["authenticated"]
},
{
"route": "/admin/*",
"allowedRoles": ["admin"]
}
],
"auth": {
"identityProviders": {
"azureActiveDirectory": {
"userDetailsClaim": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
"registration": {
"openIdIssuer": "https://login.microsoftonline.com/{tenant-id}/v2.0",
"clientIdSettingName": "AAD_CLIENT_ID",
"clientSecretSettingName": "AAD_CLIENT_SECRET"
}
}
}
}
}
Custom monitoring dashboard
// Azure Monitor Workbook - Combined KQL queries
// To create a complete Function App dashboard
// 1. Health overview
let timeRange = 24h;
requests
| where timestamp > ago(timeRange)
| summarize
TotalRequests = count(),
SuccessRate = round(countif(success == true) * 100.0 / count(), 2),
AvgDuration = round(avg(duration), 2),
P99Duration = round(percentile(duration, 99), 2)
by bin(timestamp, 1h), name
| project timestamp, FunctionName = name, TotalRequests, SuccessRate, AvgDuration, P99Duration
| order by timestamp desc
Module 10 – KEDA and Kubernetes {#module-10}
KEDA: Kubernetes-based Event Driven Autoscaling
KEDA allows hosting your Azure Functions on Kubernetes while retaining event-based automatic scaling capabilities.
flowchart TD
A[Azure Storage Queue\n500 pending messages] --> B[KEDA Scaler]
B -->|Detect load| C{Scaling calculation}
C -->|Scale out| D[Pod 1]
C -->|Scale out| E[Pod 2]
C -->|Scale out| F[Pod N]
D --> G[Drains the queue]
E --> G
F --> G
G -->|Queue empty| H[KEDA Scale to Zero]
style B fill:#326CE5,color:#fff
style A fill:#0078D4,color:#fff
Installing KEDA on AKS:
# Installation via Azure Functions Core Tools
func kubernetes install --namespace keda
# Or via Helm
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace
Deploying a Function App on Kubernetes:
# Generate the Kubernetes manifest
func kubernetes deploy \
--name globoticket-functions \
--registry myregistry.azurecr.io \
--max-replicas 20 \
--min-replicas 0
# Verify the deployment
kubectl get pods -n default
kubectl get scaledobjects -n default
Generated Kubernetes manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: globoticket-functions
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: globoticket-functions
template:
metadata:
labels:
app: globoticket-functions
spec:
containers:
- name: functions
image: myregistry.azurecr.io/globoticket-functions:latest
env:
- name: AzureWebJobsStorage
valueFrom:
secretKeyRef:
name: function-secrets
key: storage-connection
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: globoticket-queue-scaler
namespace: default
spec:
scaleTargetRef:
name: globoticket-functions
minReplicaCount: 0
maxReplicaCount: 20
pollingInterval: 15
cooldownPeriod: 60
triggers:
- type: azure-queue
metadata:
queueName: new-orders
queueLength: "5" # Scale when queue has > 5 messages per replica
accountName: stgloboticket
authenticationRef:
name: keda-azure-auth
Module 11 – Best Practices and Common Pitfalls {#module-11}
Best practices table
| Category | Best practice | Why |
|---|---|---|
| Performance | Keep functions short and focused | Reduce cold starts, optimize costs |
| Performance | Reuse HTTP connections (HttpClientFactory) | Avoid socket exhaustion |
| Performance | Use async/await everywhere | Don’t block threads |
| Performance | Minimize allocated memory | Reduce GB-sec costs |
| Security | Use Managed Identities | Eliminate secrets from configs |
| Security | Key Vault References for remaining secrets | Secret rotation without redeployment |
| Security | Never use AuthorizationLevel.Admin in production | Exposing the master key = full access |
| Resilience | Implement idempotence in queue functions | Messages can be delivered multiple times |
| Resilience | Monitor poison queues | Detect problematic messages |
| Observability | Structured logs with context (OrderId, UserId, etc.) | Ease debugging in production |
| Observability | Configure alerts on key metrics | React before customers complain |
| Deployment | Infrastructure as Code (Bicep/Terraform) | Reproducibility, living documentation |
| Deployment | Blue/Green or Slot deployment | Zero downtime during updates |
| Cost | Set a daily quota on the Consumption plan | Avoid billing surprises |
| Cost | Use Premium plan only when necessary | Consumption is sufficient for most cases |
Common pitfalls and solutions
Pitfall 1: Cold Starts on the Consumption plan
Problem: Functions idle for a long time may take several seconds to start.
Solution:
// ① Use the Premium plan with pre-warmed instances
// ② Or minimize initialization time in Program.cs
// ❌ Bad: slow initialization
services.AddDbContext<GloboTicketDbContext>(options =>
{
// Validating all models at startup = slow
options.UseSqlServer(connectionString, b => b.EnableRetryOnFailure());
options.UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll);
options.EnableDetailedErrors(); // Disable in production!
});
// ✅ Good: optimized initialization
services.AddDbContextPool<GloboTicketDbContext>(options =>
{
options.UseSqlServer(connectionString, b => b.EnableRetryOnFailure(3));
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); // Faster
}, poolSize: 10);
Pitfall 2: Incorrectly used HttpClient
// ❌ NEVER do this: creates a new connection on each execution
[Function("BadHttpExample")]
public async Task<HttpResponseData> BadExample(
[HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req)
{
using var client = new HttpClient(); // BAD! Socket exhaustion!
var result = await client.GetStringAsync("https://api.external.com/data");
// ...
}
// ✅ Correct: use IHttpClientFactory via dependency injection
public class GoodHttpExample
{
private readonly HttpClient _httpClient;
public GoodHttpExample(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("ExternalApi");
}
[Function("GoodHttpExample")]
public async Task<HttpResponseData> GoodExample(
[HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req)
{
var result = await _httpClient.GetStringAsync("data");
// ...
}
}
// Program.cs
services.AddHttpClient("ExternalApi", client =>
{
client.BaseAddress = new Uri("https://api.external.com/");
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
Pitfall 3: Golden rules of Durable Functions Orchestrators
// ❌ ORCHESTRATOR RULE VIOLATIONS
[Function(nameof(BadOrchestrator))]
public async Task RunBadOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
// ❌ FORBIDDEN: DateTime.Now is not deterministic!
var now = DateTime.Now;
// ❌ FORBIDDEN: Guid.NewGuid() is not deterministic!
var id = Guid.NewGuid();
// ❌ FORBIDDEN: Direct HTTP call from the orchestrator!
using var client = new HttpClient();
var result = await client.GetStringAsync("https://api.example.com");
// ❌ FORBIDDEN: Direct database access!
var order = await _dbContext.Orders.FindAsync(id);
// ❌ FORBIDDEN: Random is not deterministic!
var random = new Random().Next(100);
// ❌ FORBIDDEN: Thread.Sleep blocks the thread!
Thread.Sleep(1000);
}
// ✅ CORRECT: Deterministic orchestrator
[Function(nameof(GoodOrchestrator))]
public async Task RunGoodOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
// ✅ Use context.CurrentUtcDateTime (replay-safe)
var now = context.CurrentUtcDateTime;
// ✅ Use context.NewGuid() (deterministic on replay)
var id = context.NewGuid();
// ✅ Delegate I/O to Activity Functions
var result = await context.CallActivityAsync<string>(nameof(CallExternalApi), null);
// ✅ Use CreateTimer for delays
await context.CreateTimer(now.AddSeconds(5), CancellationToken.None);
}
Pitfall 4: Queue Trigger idempotency
// ❌ Non-idempotent: sends the email multiple times if function crashes after sending
[Function("ProcessOrderBad")]
public async Task ProcessBad(
[QueueTrigger("orders")] OrderQueueMessage order)
{
await _emailService.SendConfirmationEmail(order.CustomerEmail); // Sends email
await _dbContext.SaveChangesAsync(); // If crash here, message is replayed → duplicate email!
}
// ✅ Idempotent: check before acting
[Function("ProcessOrderGood")]
public async Task ProcessGood(
[QueueTrigger("orders")] OrderQueueMessage order)
{
// Check if already processed
var existing = await _dbContext.ProcessedOrders
.FirstOrDefaultAsync(o => o.OrderId == order.OrderId);
if (existing is not null)
{
_logger.LogWarning("Order {OrderId} already processed, skipping", order.OrderId);
return; // Idempotent: silent exit
}
// Atomic transaction
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
_dbContext.ProcessedOrders.Add(new ProcessedOrder { OrderId = order.OrderId });
await _dbContext.SaveChangesAsync();
await _emailService.SendConfirmationEmail(order.CustomerEmail);
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw; // Message will be retried
}
}
Performance optimization
// Pre-compile JSON expressions for frequently serialized types
[JsonSerializable(typeof(OrderQueueMessage))]
[JsonSerializable(typeof(OrderDocument))]
[JsonSerializable(typeof(NewOrderOutput))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }
// Program.cs - Optimized JSON configuration
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults(builder =>
{
builder.UseNewtonsoftJson(); // Or System.Text.Json
})
.ConfigureServices(services =>
{
services.Configure<JsonSerializerOptions>(options =>
{
options.TypeInfoResolver = AppJsonSerializerContext.Default;
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
})
.Build();
Complete Reference Architecture
flowchart TB
subgraph "Clients"
WEB[Web Application\nReact/Blazor]
MOB[Mobile Application]
EXT[External Services\nStripe, etc.]
end
subgraph "Azure API Management"
APIM[APIM\nRate limiting, Auth, Routing]
end
subgraph "Azure Functions Apps"
FA1[func-api\nHTTP Triggers\nRESTful API]
FA2[func-processing\nQueue/Timer Triggers\nBatch processing]
FA3[func-orchestration\nDurable Functions\nWorkflows]
end
subgraph "Messaging"
SB[Azure Service Bus\nTopics & Queues]
EG[Azure Event Grid\nEvent routing]
end
subgraph "Data"
SQL["(Azure SQL\nTransactional)"]
COSMOS["(Cosmos DB\nDocuments)"]
BLOB[Azure Blob Storage\nFiles]
end
subgraph "Monitoring & Security"
AI[Application Insights]
KV[Key Vault]
AAD[Microsoft Entra ID]
end
WEB --> APIM
MOB --> APIM
EXT --> APIM
APIM --> FA1
FA1 --> SB
SB --> FA2
SB --> FA3
FA1 --> SQL
FA2 --> COSMOS
FA3 --> BLOB
FA1 & FA2 & FA3 --> AI
KV -.->|secrets| FA1 & FA2 & FA3
AAD -.->|identity| FA1 & FA2 & FA3
style APIM fill:#0078D4,color:#fff
style FA1 fill:#68217A,color:#fff
style FA2 fill:#68217A,color:#fff
style FA3 fill:#68217A,color:#fff
Glossary {#glossary}
| Term | Definition |
|---|---|
| Activity Function | Function executing a concrete step in a Durable Functions workflow; can do I/O |
| App Service Plan | Hosting plan shared with Azure App Service; monthly billing |
| Application Insights | Azure monitoring service for application observability |
| Azurite | Local emulator for Azure Storage (Blob, Queue, Table) |
| Bicep | Declarative DSL language for Azure Resource Manager (Infrastructure as Code) |
| Binding | Declarative connection to an Azure service (input = read, output = write) |
| Cold Start | Initialization delay when starting a Function instance after a period of inactivity |
| Consumption Plan | Azure Functions serverless plan; per-execution billing with generous free quota |
| CORS | Cross-Origin Resource Sharing; browser security mechanism managing cross-domain requests |
| Durable Functions | Azure Functions extension enabling stateful and resilient workflows |
| Elastic Premium Plan | Hosting plan with pre-warmed instances, VNet, unlimited execution |
| Event Grid | Azure publish-subscribe event routing service |
| Event Hub | High-performance Azure event streaming service (IoT, telemetry) |
| Fan-Out/Fan-In | Durable Functions pattern for running activities in parallel then waiting for completion |
| Function App | Azure Functions deployment unit containing one or more functions |
| Host.json | Global configuration file for the Azure Functions host |
| HttpTrigger | Trigger activating a function by HTTP request |
| Idempotency | Property of an operation producing the same result even if executed multiple times |
| Isolated Worker Model | C# model running Functions in a .NET process separate from the Functions host |
| KEDA | Kubernetes-based Event Driven Autoscaling; enables event-driven scaling on Kubernetes |
| Key Vault Reference | Reference to a Key Vault secret in App Settings, avoiding storing the secret directly |
| Managed Identity | Azure AD identity automatically managed by Azure for a resource; eliminates secrets |
| Orchestrator Function | Function defining the workflow and coordinating activities in Durable Functions |
| Output Binding | Binding enabling a function to write data to an external service |
| Poison Queue | Queue receiving messages that exceeded the maximum number of processing attempts |
| RBAC | Role-Based Access Control; Azure role-based access control system |
| Service Bus | Azure enterprise messaging service; supports queues and publish-subscribe topics |
| Serverless | Paradigm where infrastructure is managed by the cloud provider; usage-based billing |
| Starter Function | Function starting a new Durable Functions orchestration |
| Task Hub | Durable Functions namespace for storing orchestration state |
| Timer Trigger | Trigger activating a function according to a scheduled CRON expression |
| Trigger | Trigger defining what activates the execution of an Azure Function |
| VNet Integration | Connecting a Function App to a private Azure virtual network |
| Webhook | Mechanism allowing an external service to notify your application via an HTTP request |
Key exam points
| Question | Answer |
|---|---|
| Cheapest plan for sporadic workloads? | Consumption |
| Avoid cold starts → which plan? | Elastic Premium |
| Maximum timeout for Consumption plan? | 5 min (configurable to 10) |
| HTTP trigger authorization levels | Anonymous, Function, Admin |
| Pass the key via header? | x-functions-key |
| Durable orchestration state storage? | Task Hub (Azure Storage by default) |
| Trigger + Output Bindings in the same function? | ✅ Yes, combinable |
| Local emulator for Storage triggers | Azurite |
Advanced Section 1 – Durable Functions (Deep Dive)
DurableTask framework architecture
Durable Functions is built on the DurableTask Framework (open source). The framework serializes the state of each orchestration in a Task Hub composed of three storage entities (Azure Storage by default):
| Entity | Role |
|---|---|
| Control Queue | Control messages (start, event, terminate) |
| Work-item Queue | Tasks to be executed by activity workers |
| History Table | Complete execution journal (replay) |
| Instances Table | Current state of each orchestration |
| Blobs | Large payloads (>64 KB) |
flowchart TD
A[Starter Function] -->|StartNewAsync| B[Control Queue]
B --> C[Orchestrator Function]
C -->|CallActivityAsync| D[Work-item Queue]
D --> E[Activity Function]
E -->|Result| B
C -->|Persists state| F["(History Table)"]
C -->|ContinueAsNew / completed| G["(Instances Table)"]
Orchestrator determinism rules
The orchestrator replays its history on each wake — it must therefore never:
- Call
DateTime.Now→ usecontext.CurrentUtcDateTime - Generate a random GUID → use
context.NewGuid() - Perform I/O (HTTP, DB) → delegate to an Activity
- Use
Task.Delay→ usecontext.CreateTimer() - Read environment variables during execution
All patterns in detail
Pattern 1: Function Chaining (sequential)
[Function("OrderOrchestrator")]
public async Task<OrderResult> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var order = context.GetInput<OrderInput>()!;
// Step 1 — inventory check
var stockOk = await context.CallActivityAsync<bool>("CheckInventory", order.Items);
if (!stockOk)
return new OrderResult { Status = "OutOfStock" };
// Step 2 — payment
var paymentId = await context.CallActivityAsync<string>("ProcessPayment", order.Payment);
// Step 3 — ticket generation
var ticketUrl = await context.CallActivityAsync<string>("GenerateTicket",
new TicketInput(order.OrderId, paymentId));
// Step 4 — email sending
await context.CallActivityAsync("SendConfirmationEmail",
new EmailInput(order.CustomerEmail, ticketUrl));
return new OrderResult { Status = "Completed", TicketUrl = ticketUrl };
}
Pattern 2: Fan-out / Fan-in (parallel)
[Function("BatchNotifyOrchestrator")]
public async Task RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var customerIds = context.GetInput<List<string>>()!;
// Fan-out: launch N activities in parallel
var tasks = customerIds
.Select(id => context.CallActivityAsync<NotificationResult>("NotifyCustomer", id))
.ToList();
// Fan-in: wait for ALL results
var results = await Task.WhenAll(tasks);
var failures = results.Count(r => !r.Success);
context.SetCustomStatus($"Completed. Failures: {failures}");
}
Pattern 3: Async HTTP API (long-running request)
sequenceDiagram
participant Client
participant HTTP Starter
participant Orchestrator
participant Activity
Client->>HTTP Starter: POST /api/start-job
HTTP Starter->>Orchestrator: StartNewAsync()
HTTP Starter-->>Client: 202 Accepted + statusQueryUrl
loop Polling
Client->>HTTP Starter: GET /api/status/{instanceId}
HTTP Starter-->>Client: 202 Running | 200 Completed
end
Orchestrator->>Activity: CallActivityAsync
Activity-->>Orchestrator: Result
Orchestrator-->>HTTP Starter: Completed
HTTP Starter-->>Client: 200 OK + result
[Function("StartLongJob")]
public async Task<HttpResponseData> HttpStart(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
var input = await req.ReadFromJsonAsync<JobInput>();
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
"LongJobOrchestrator", input);
// Returns 202 + tracking URLs
return await client.CreateCheckStatusResponseAsync(req, instanceId);
}
Pattern 4: Monitoring (Eternal Orchestration)
[Function("StockMonitor")]
public async Task RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var productId = context.GetInput<string>()!;
while (true)
{
var stockLevel = await context.CallActivityAsync<int>("GetStockLevel", productId);
if (stockLevel < 10)
{
await context.CallActivityAsync("SendLowStockAlert", productId);
}
// Wait 1 hour (durable timer, not Thread.Sleep)
var nextCheck = context.CurrentUtcDateTime.AddHours(1);
await context.CreateTimer(nextCheck, CancellationToken.None);
// ContinueAsNew clears history → avoids infinite growth
context.ContinueAsNew(productId);
}
}
Pattern 5: Human Interaction (with timeout)
[Function("ApprovalOrchestrator")]
public async Task RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var order = context.GetInput<OrderInput>()!;
// Trigger the approval request
await context.CallActivityAsync("RequestManagerApproval", order);
// Wait for the event OR a 48h timeout
using var cts = new CancellationTokenSource();
var approvalTask = context.WaitForExternalEvent<ApprovalResult>("OrderApproval");
var timeoutTask = context.CreateTimer(
context.CurrentUtcDateTime.AddHours(48), cts.Token);
var winner = await Task.WhenAny(approvalTask, timeoutTask);
if (winner == approvalTask)
{
cts.Cancel(); // Cancel the timer
var result = await approvalTask;
if (result.Approved)
await context.CallActivityAsync("FulfillOrder", order);
else
await context.CallActivityAsync("CancelOrder", order);
}
else
{
// Timeout: automatically escalate
await context.CallActivityAsync("EscalateToDirector", order);
}
}
// Raise the event from any Function
[Function("ApproveOrder")]
public async Task<HttpResponseData> Approve(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "approve/{instanceId}")]
HttpRequestData req,
string instanceId,
[DurableClient] DurableTaskClient client)
{
var approval = await req.ReadFromJsonAsync<ApprovalResult>();
await client.RaiseEventAsync(instanceId, "OrderApproval", approval);
return req.CreateResponse(HttpStatusCode.OK);
}
Pattern 6: Aggregator (Durable Entity)
Durable Entities allow maintaining mutable shared state between multiple calls.
// Entity definition
[Function(nameof(CounterEntity))]
public static void CounterEntity(
[EntityTrigger] TaskEntityDispatcher dispatcher)
{
dispatcher.DispatchAsync<Counter>();
}
public class Counter
{
public int Value { get; set; } = 0;
public void Add(int amount) => Value += amount;
public void Reset() => Value = 0;
public int Get() => Value;
}
// Usage from an orchestrator
[Function("AggregatorOrchestrator")]
public async Task RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var entityId = new EntityInstanceId(nameof(CounterEntity), "myCounter");
// Call an operation on the entity
await context.Entities.CallEntityAsync(entityId, "Add", 5);
// Read the state
int total = await context.Entities.CallEntityAsync<int>(entityId, "Get");
context.SetCustomStatus($"Total: {total}");
}
Sub-orchestrations
[Function("MainOrchestrator")]
public async Task RunMain(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var orders = context.GetInput<List<string>>()!;
// Launch multiple sub-orchestrations in parallel
var subTasks = orders.Select(orderId =>
context.CallSubOrchestratorAsync("SingleOrderOrchestrator", orderId));
await Task.WhenAll(subTasks);
}
[Function("SingleOrderOrchestrator")]
public async Task RunSingle(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var orderId = context.GetInput<string>()!;
await context.CallActivityAsync("ValidateOrder", orderId);
await context.CallActivityAsync("FulfillOrder", orderId);
}
Error handling and retries
var retryOptions = new TaskOptions(
new TaskRetryOptions(new RetryPolicy(
maxNumberOfAttempts: 3,
firstRetryInterval: TimeSpan.FromSeconds(5),
backoffCoefficient: 2.0, // Exponential
maxRetryInterval: TimeSpan.FromMinutes(1))));
try
{
await context.CallActivityAsync("RiskyOperation", input, retryOptions);
}
catch (TaskFailedException ex)
{
// Handle failure after all retries
await context.CallActivityAsync("CompensatingAction", input);
}
Advanced Section 2 – Advanced Triggers
Timer Trigger – Complete NCRONTAB expressions
Azure Functions uses NCRONTAB (6 fields: seconds included), unlike standard Unix cron.
{second} {minute} {hour} {day-of-month} {month} {day-of-week}
| Expression | Trigger |
|---|---|
0 */5 * * * * | Every 5 minutes |
0 0 9 * * 1-5 | 9am, Monday–Friday |
0 30 8 1 * * | 1st of each month at 8:30 |
0 0 0 * * 0 | Midnight every Sunday |
0 0 */6 * * * | Every 6 hours |
%TimerSchedule% | Value from app settings |
[Function("WeeklyReport")]
public async Task Run(
[TimerTrigger("%WeeklyReportSchedule%")] TimerInfo timer, // Expression in settings
FunctionContext context)
{
var logger = context.GetLogger<WeeklyReportFunction>();
if (timer.IsPastDue)
logger.LogWarning("Timer executed late!");
logger.LogInformation("Next trigger: {Next}",
timer.ScheduleStatus?.Next);
}
%SettingName%: references an app setting (useful for modifying the schedule without redeploying).
Blob Trigger – Filtering and variants
// Triggered on every new blob in uploads/images/
[Function("ProcessUploadedImage")]
public async Task Run(
[BlobTrigger("uploads/images/{name}", Connection = "StorageConnection")]
Stream blobStream,
string name,
BlobProperties blobProperties, // Blob metadata
FunctionContext context)
{
var logger = context.GetLogger<BlobProcessingFunction>();
logger.LogInformation("New blob: {Name}, Size: {Size} bytes",
name, blobProperties.ContentLength);
// Processing...
}
// Triggered only on .csv files in a subfolder
[Function("ProcessCsvFile")]
public async Task ProcessCsv(
[BlobTrigger("data/imports/{subdir}/{filename}.csv", Connection = "StorageConnection")]
string csvContent,
string subdir,
string filename)
{
// subdir and filename are extracted from the path via binding expressions
}
Limitation: The Blob trigger can experience a delay of several minutes on the Consumption plan. For latency-sensitive scenarios, use Event Grid with an Event Grid-based blob trigger.
Queue Trigger – Advanced configuration
// host.json — Queue trigger configuration
{
"extensions": {
"queues": {
"batchSize": 16, // Number of messages processed in parallel (default: 16)
"newBatchThreshold": 8, // Threshold to fetch a new batch
"maxDequeueCount": 5, // Attempts before sending to poison queue
"visibilityTimeout": "00:01:30", // 90 seconds of invisibility during processing
"messageEncoding": "base64" // Message encoding
}
}
}
[Function("ProcessOrderMessage")]
public async Task Run(
[QueueTrigger("new-orders", Connection = "StorageConnection")]
QueueMessage message, // Rich type: access to metadata
FunctionContext context)
{
var logger = context.GetLogger<OrderProcessor>();
logger.LogInformation("DequeueCount: {Count}", message.DequeueCount);
// Deserialize the body
var order = message.Body.ToObjectFromJson<OrderDto>();
// ...
}
Poison queue: after maxDequeueCount failures, the message is automatically moved to {queue-name}-poison. Monitor this queue to detect problematic messages.
Service Bus Trigger
[Function("ProcessServiceBusMessage")]
public async Task Run(
[ServiceBusTrigger(
"orders", // Queue or topic name
"high-value-subscription", // Subscription name (topics only)
Connection = "ServiceBusConnection",
IsSessionsEnabled = true)] // Sessions for ordered processing
ServiceBusReceivedMessage message,
ServiceBusMessageActions messageActions, // Complete, Abandon, DeadLetter...
FunctionContext context)
{
var logger = context.GetLogger<ServiceBusProcessor>();
try
{
var order = message.Body.ToObjectFromJson<OrderDto>();
await ProcessOrderAsync(order);
await messageActions.CompleteMessageAsync(message);
}
catch (BusinessException ex)
{
// Send directly to dead-letter with reason
await messageActions.DeadLetterMessageAsync(message,
deadLetterReason: "BusinessError",
deadLetterErrorDescription: ex.Message);
}
}
Event Hub Trigger
[Function("ProcessTelemetry")]
public async Task Run(
[EventHubTrigger(
"telemetry-hub",
Connection = "EventHubConnection",
ConsumerGroup = "functions-consumer",
IsBatched = true)] // Batch processing
EventData[] events,
PartitionContext partitionContext,
FunctionContext context)
{
var logger = context.GetLogger<TelemetryProcessor>();
logger.LogInformation(
"Partition {Id} — {Count} events received",
partitionContext.PartitionId,
events.Length);
foreach (var @event in events)
{
var telemetry = @event.EventBody.ToObjectFromJson<DeviceTelemetry>();
// Process each event...
}
}
Cosmos DB Trigger – Advanced Change Feed
[Function("ProcessCosmosChanges")]
public async Task Run(
[CosmosDBTrigger(
databaseName: "shop",
containerName: "orders",
Connection = "CosmosConnection",
LeaseContainerName = "leases",
LeaseContainerPrefix = "func-", // Prefix to isolate multiple consumers
StartFromBeginning = false, // Don't replay history on startup
FeedPollDelay = 1000, // Polling delay in ms
MaxItemsPerInvocation = 100)] // Limit per batch
IReadOnlyList<Order> changedOrders,
FunctionContext context)
{
foreach (var order in changedOrders)
{
// React to inserts AND updates
await SyncToSearchIndexAsync(order);
}
}
The Cosmos DB trigger uses the service’s Change Feed. The
leasescontainer stores the current read position (checkpoint).
Advanced Section 3 – Input/Output Bindings Deep Dive
Cosmos DB Input – Query Binding
[Function("GetOrdersByCustomer")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "get",
Route = "customers/{customerId}/orders")]
HttpRequestData req,
string customerId,
// Input binding with parameterized SQL query
[CosmosDBInput(
databaseName: "shop",
containerName: "orders",
Connection = "CosmosConnection",
SqlQuery = "SELECT * FROM c WHERE c.customerId = {customerId} ORDER BY c._ts DESC",
PartitionKey = "{customerId}")]
IReadOnlyList<Order> orders)
{
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(orders);
return response;
}
Table Storage Binding
// Input: read a Table Storage entity
[Function("GetConfig")]
public async Task<HttpResponseData> GetConfig(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "config/{key}")]
HttpRequestData req,
string key,
[TableInput("AppConfig", "Config", "{key}", Connection = "StorageConnection")]
ConfigEntity? configEntry)
{
var response = req.CreateResponse(
configEntry != null ? HttpStatusCode.OK : HttpStatusCode.NotFound);
if (configEntry != null)
await response.WriteAsJsonAsync(configEntry);
return response;
}
// Output: write to Table Storage
[Function("SaveAuditLog")]
[TableOutput("AuditLogs", Connection = "StorageConnection")]
public AuditLogEntity Run(
[QueueTrigger("audit-events", Connection = "StorageConnection")]
AuditEvent auditEvent)
{
return new AuditLogEntity
{
PartitionKey = auditEvent.UserId,
RowKey = Guid.NewGuid().ToString(),
Action = auditEvent.Action,
Timestamp = DateTimeOffset.UtcNow
};
}
Blob Storage Output – Advanced writing
// Output binding with dynamic name via binding expression
[Function("GenerateReport")]
[BlobOutput("reports/{DateTime:yyyy}/{DateTime:MM}/{name}-report.json",
Connection = "StorageConnection")]
public async Task<string> Run(
[TimerTrigger("0 0 1 * * *")] TimerInfo timer,
FunctionContext context)
{
var report = await BuildReportAsync();
return JsonSerializer.Serialize(report);
}
Multiple Output Bindings – Complete pattern
// Return class wrapping all outputs
public class ProcessOrderOutput
{
// Output 1: confirmation message in a queue
[QueueOutput("confirmations", Connection = "StorageConnection")]
public ConfirmationMessage? QueueMessage { get; set; }
// Output 2: document in Cosmos DB
[CosmosDBOutput("shop", "orders", Connection = "CosmosConnection")]
public Order? CosmosDocument { get; set; }
// Output 3: blob with the ticket PDF
[BlobOutput("tickets/{orderId}.pdf", Connection = "StorageConnection")]
public byte[]? TicketPdf { get; set; }
// Output 4: HTTP response
public HttpResponseData? HttpResponse { get; set; }
}
[Function("ProcessNewOrder")]
public async Task<ProcessOrderOutput> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "orders")]
HttpRequestData req)
{
var order = await req.ReadFromJsonAsync<Order>() ?? throw new BadRequestException();
order.Id = Guid.NewGuid().ToString();
var pdfBytes = await GenerateTicketPdfAsync(order);
var response = req.CreateResponse(HttpStatusCode.Created);
await response.WriteAsJsonAsync(new { orderId = order.Id });
return new ProcessOrderOutput
{
QueueMessage = new ConfirmationMessage(order.Id),
CosmosDocument = order,
TicketPdf = pdfBytes,
HttpResponse = response
};
}
Binding Expressions – Complete reference
| Expression | Description |
|---|---|
{parameter} | Value from the trigger (route, blob name…) |
{DateTime} | Current UTC date/time |
{DateTime:yyyy-MM-dd} | Formatted date |
{rand-guid} | Generated random GUID |
{sys.utcnow} | UTC timestamp |
%SettingName% | Value from application settings |
Advanced Section 4 – Isolated Worker Process (.NET)
In-process vs Isolated Worker
flowchart LR
subgraph "In-Process (legacy)"
H1[Azure Functions Host] --- C1[Your C# code]
style H1 fill:#f96,color:#000
style C1 fill:#f96,color:#000
end
subgraph "Isolated Worker (recommended)"
H2[Azure Functions Host] <-->|gRPC| C2[Your .NET process]
style H2 fill:#6af,color:#000
style C2 fill:#6af,color:#000
end
| Criterion | In-Process | Isolated Worker |
|---|---|---|
| .NET support | .NET 6 (EOL) | .NET 8, .NET 9, .NET Framework 4.8 |
| ASP.NET Core Middleware | ❌ | ✅ |
| Dependency injection | Partial | Complete (IHostBuilder) |
| NuGet packages | Coupled to host | Independent |
| Platform future | ❌ Deprecated | ✅ |
Program.cs – Complete configuration
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
// ASP.NET Core Integration mode (recommended)
.ConfigureFunctionsWebApplication(workerOptions =>
{
// Add Azure Functions middleware
workerOptions.UseMiddleware<CorrelationIdMiddleware>();
workerOptions.UseMiddleware<ExceptionHandlingMiddleware>();
})
.ConfigureServices((context, services) =>
{
// Application Insights
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
// Business services
services.AddHttpClient<IOrderService, OrderService>(client =>
{
client.BaseAddress = new Uri(
context.Configuration["OrderServiceUrl"]
?? throw new InvalidOperationException("OrderServiceUrl missing"));
});
services.AddSingleton<IEmailService, SendGridEmailService>();
// Entity Framework Core
services.AddDbContextPool<ShopDbContext>(options =>
options.UseSqlServer(
context.Configuration.GetConnectionString("SqlDatabase")));
// Options pattern
services.Configure<FeatureFlags>(
context.Configuration.GetSection("FeatureFlags"));
})
.ConfigureAppConfiguration(config =>
{
// Azure App Configuration (optional)
config.AddAzureAppConfiguration(options =>
options.Connect(
Environment.GetEnvironmentVariable("AppConfigConnectionString"))
.UseFeatureFlags());
})
.Build();
await host.RunAsync();
Middleware Pipeline
// Middleware for global exception handling
public class ExceptionHandlingMiddleware : IFunctionsWorkerMiddleware
{
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(ILogger<ExceptionHandlingMiddleware> logger)
=> _logger = logger;
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
try
{
await next(context);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Unhandled error in {FunctionName}", context.FunctionDefinition.Name);
// Return HTTP 500 response for HTTP triggers
var httpReqData = await context.GetHttpRequestDataAsync();
if (httpReqData != null)
{
var response = httpReqData.CreateResponse(HttpStatusCode.InternalServerError);
await response.WriteAsJsonAsync(new { error = "Internal server error" });
context.GetInvocationResult().Value = response;
}
}
}
}
ASP.NET Core Integration Mode
With ConfigureFunctionsWebApplication(), HTTP functions can return IActionResult like in classic ASP.NET Core:
// With ASP.NET Core Integration, IActionResult can be used
[Function("GetOrder")]
public async Task<IActionResult> GetOrder(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "orders/{id}")]
HttpRequest req, // ← ASP.NET Core HttpRequest, not HttpRequestData
string id)
{
var order = await _orderService.GetByIdAsync(id);
return order is null
? new NotFoundResult()
: new OkObjectResult(order);
}
Advanced Section 5 – Configuration and Secrets
local.settings.json — NEVER commit this file
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=...",
"CosmosDbConnectionString": "AccountEndpoint=https://localhost:8081/;...",
"SendGridApiKey": "SG.xxx",
"FeatureFlags__NewCheckout": "true"
},
"ConnectionStrings": {
"SqlDatabase": "Server=localhost;Database=shop;Trusted_Connection=True;"
}
}
Mandatory: add
local.settings.jsonto.gitignore. This file contains development credentials.
# .gitignore
local.settings.json
Managed Identity for secrets access
# 1. Enable Managed Identity on the Function App
az functionapp identity assign \
--name my-function-app \
--resource-group myRG
# 2. Retrieve the Object ID of the identity
$principalId = az functionapp identity show \
--name my-function-app \
--resource-group myRG \
--query principalId -o tsv
# 3. Assign the Key Vault Secrets User role
az role assignment create \
--role "Key Vault Secrets User" \
--assignee $principalId \
--scope /subscriptions/<sub>/resourceGroups/myRG/providers/Microsoft.KeyVault/vaults/myvault
// Accessing Key Vault from code with DefaultAzureCredential
// (works locally with az login AND in production with Managed Identity)
services.AddSingleton(sp =>
{
var credential = new DefaultAzureCredential();
var vaultUri = new Uri($"https://{configuration["KeyVaultName"]}.vault.azure.net/");
return new SecretClient(vaultUri, credential);
});
Advanced Section 6 – Cold Starts and Performance
Causes of Cold Starts
sequenceDiagram
participant Client
participant Azure
participant Functions Host
participant Worker Process
Note over Azure: No active instance (scale-to-zero)
Client->>Azure: HTTP request
Azure->>Functions Host: Start host (⏱ 2–5 sec)
Functions Host->>Worker Process: Start .NET worker (⏱ 1–3 sec)
Worker Process->>Worker Process: Load assemblies (⏱ 0.5–2 sec)
Worker Process-->>Client: First response (~5–10 sec total)
Note over Azure: Already warm instances
Client->>Azure: Next request
Azure-->>Client: Response (<100 ms)
| Factor | Impact |
|---|---|
| Deployment size (number of DLLs) | High — minimize dependencies |
| Consumption plan | High — scale-to-zero |
| .NET framework (version) | .NET 8+: faster startup thanks to ReadyToRun |
| Number of injected services | Medium — optimize DI |
| DB connections at startup | High — use lazy loading |
Mitigation strategies
1. Elastic Premium plan — Pre-warmed instances
# Configure pre-warmed instances (always ready)
az functionapp plan create \
--name my-premium-plan \
--resource-group myRG \
--sku EP1 \
--min-instances 1 \ # Always active instance
--max-burst 10 # Scale up to 10 instances
2. Always Ready (Flex Consumption)
az functionapp update \
--name my-function-app \
--resource-group myRG \
--set properties.siteConfig.minimumElasticInstanceCount=2
3. Keep-alive pings (Consumption plan)
// Timer function that "keeps alive" the application
[Function("WarmUp")]
public void Run(
[TimerTrigger("0 */4 * * * *")] TimerInfo timer) // Every 4 minutes
{
// Do nothing — just keep the instance active
}
4. Startup optimizations
// ❌ Avoid: DB connection in constructor
public MyFunction(IDbConnection db)
{
db.Open(); // Opens connection at startup → slows cold start
}
// ✅ Recommended: lazy connection or connection pool
services.AddDbContextPool<ShopDbContext>(options =>
options.UseSqlServer(connectionString)); // Pool managed automatically
// ✅ Use ReadyToRun in .csproj
// <PublishReadyToRun>true</PublishReadyToRun>
// ✅ Enable trimming to reduce size
// <PublishTrimmed>true</PublishTrimmed> (caution: testing required)
Measuring cold starts with Application Insights
// KQL query to identify cold starts
requests
| where timestamp > ago(24h)
| extend coldStart = tobool(customDimensions["ColdStart"])
| summarize
total = count(),
coldStarts = countif(coldStart == true),
avgDuration = avg(duration),
p95Duration = percentile(duration, 95)
by bin(timestamp, 1h)
| render timechart
Advanced Section 7 – Monitoring with Application Insights
Automatic integration
Application Insights is integrated without additional code if APPLICATIONINSIGHTS_CONNECTION_STRING is defined. It automatically captures:
- Requests: function invocations (duration, status, exceptions)
- Dependencies: outgoing HTTP calls, SQL queries, Storage access
- Exceptions: all unhandled exceptions
- Traces: logs via
ILogger<T>
Configuration in Program.cs
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
})
.ConfigureLogging(logging =>
{
// Filter verbose Application Insights categories
logging.AddFilter<ApplicationInsightsLoggerProvider>(
"Microsoft.Azure.Functions", LogLevel.Warning);
logging.AddFilter<ApplicationInsightsLoggerProvider>(
"Host.Results", LogLevel.Error);
})
// host.json — sampling and log level configuration
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"maxTelemetryItemsPerSecond": 20,
"excludedTypes": "Request;Exception" // Don't sample requests or exceptions
},
"enableLiveTelemetry": true
},
"logLevel": {
"default": "Information",
"Host.Results": "Error",
"Function": "Information",
"Host.Aggregator": "Trace"
}
}
}
Distributed Tracing
Azure Functions automatically propagates the correlation ID (operation_Id) between related functions. This allows tracing a request through the entire processing chain:
HTTP Request → Function A → Queue Message → Function B → Cosmos DB
←────────────────── same operation_Id ──────────────────→
// KQL query: trace a complete operation by its ID
union requests, dependencies, traces, exceptions
| where operation_Id == "abc123"
| order by timestamp asc
| project timestamp, itemType, name, duration, success, message
Live Metrics and Alerts
# Create an alert on the error rate
az monitor metrics alert create \
--name "Functions-High-Error-Rate" \
--resource-group myRG \
--scopes /subscriptions/<sub>/resourceGroups/myRG/providers/microsoft.insights/components/myAppInsights \
--condition "avg requests/failed > 5" \
--window-size 5m \
--evaluation-frequency 1m \
--action /subscriptions/<sub>/resourceGroups/myRG/providers/microsoft.insights/actionGroups/DevOpsTeam
Advanced Section 8 – Advanced Deployment
Deployment Slots
flowchart LR
DEV[Development] -->|git push| STAGING[Staging Slot]
STAGING -->|Integration tests| SWAP{Swap}
SWAP -->|"az functionapp deployment slot swap"| PROD[Production]
SWAP -.->|Immediate rollback| STAGING
# Create a staging slot (requires Standard plan or higher)
az functionapp deployment slot create \
--name my-function-app \
--resource-group myRG \
--slot staging
# Deploy to staging slot (not production)
func azure functionapp publish my-function-app --slot staging
# Swap staging → production
az functionapp deployment slot swap \
--name my-function-app \
--resource-group myRG \
--slot staging \
--target-slot production
# Traffic splitting: 10% to staging (canary release)
az functionapp traffic-routing set \
--name my-function-app \
--resource-group myRG \
--distribution staging=10
Container Deployment
# Dockerfile — Azure Functions isolated worker .NET 8
FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0 AS base
WORKDIR /home/site/wwwroot
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyFunctions.csproj", "."]
RUN dotnet restore
COPY . .
RUN dotnet build -c Release -o /app/build
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish
FROM base AS final
COPY --from=publish /app/publish .
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
AzureFunctionsJobHost__Logging__Console__IsEnabled=true
# Build and push to ACR
az acr build --registry myRegistry --image my-functions:latest .
# Deploy on Azure Functions from ACR
az functionapp create \
--name my-function-app \
--resource-group myRG \
--plan my-premium-plan \
--deployment-container-image-name myregistry.azurecr.io/my-functions:latest \
--docker-registry-server-url https://myregistry.azurecr.io
GitHub Actions Workflow
# .github/workflows/deploy-functions.yml
name: Deploy Azure Functions
on:
push:
branches: [main]
workflow_dispatch:
env:
AZURE_FUNCTIONAPP_NAME: my-function-app
DOTNET_VERSION: '8.0.x'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # For OIDC (Managed Identity)
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore, Build & Test
run: |
dotnet restore
dotnet build --configuration Release
dotnet test --configuration Release --no-build
- name: Publish
run: dotnet publish --configuration Release --output ./publish
- name: Login to Azure (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to Azure Functions
uses: azure/functions-action@v1
with:
app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }}
package: ./publish
slot-name: staging # Deploy to staging first
- name: Run Smoke Tests
run: |
curl -f https://${{ env.AZURE_FUNCTIONAPP_NAME }}-staging.azurewebsites.net/api/health
- name: Swap to Production
run: |
az functionapp deployment slot swap \
--name ${{ env.AZURE_FUNCTIONAPP_NAME }} \
--resource-group ${{ secrets.RESOURCE_GROUP }} \
--slot staging --target-slot production
Advanced Section 9 – Security
Authentication – Easy Auth (Azure AD)
# Enable Easy Auth with Azure AD
az webapp auth microsoft update \
--name my-function-app \
--resource-group myRG \
--client-id <app-registration-client-id> \
--tenant-id <tenant-id> \
--issuer "https://login.microsoftonline.com/<tenant-id>/v2.0" \
--allowed-audiences "api://<client-id>"
# Configure unauthenticated action (redirect or 401)
az webapp auth update \
--name my-function-app \
--resource-group myRG \
--unauthenticated-client-action Return401
With Easy Auth enabled, Azure manages token validation before your code executes. Your function can read claims from the injected headers:
[Function("GetProfile")]
public IActionResult GetProfile(
[HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
{
// Easy Auth injects claims into X-MS-CLIENT-PRINCIPAL-* headers
var userId = req.Headers["X-MS-CLIENT-PRINCIPAL-ID"].FirstOrDefault();
var userName = req.Headers["X-MS-CLIENT-PRINCIPAL-NAME"].FirstOrDefault();
return new OkObjectResult(new { userId, userName });
}
IP Restrictions
# Allow only certain IP ranges
az functionapp config access-restriction add \
--name my-function-app \
--resource-group myRG \
--rule-name "AllowOfficeNetwork" \
--action Allow \
--ip-address 203.0.113.0/24 \
--priority 100
# Block everything else implicitly (default rule = Deny)
Private Endpoints (inbound traffic)
# Create a Private Endpoint for the Function App
az network private-endpoint create \
--name pe-my-function-app \
--resource-group myRG \
--vnet-name myVNet \
--subnet private-endpoints-subnet \
--private-connection-resource-id $(az functionapp show --name my-function-app --resource-group myRG --query id -o tsv) \
--group-id sites \
--connection-name my-function-app-connection
# Disable public access (Function App only accessible via Private Endpoint)
az functionapp update \
--name my-function-app \
--resource-group myRG \
--set publicNetworkAccess=Disabled
VNet Integration (outbound traffic)
flowchart LR
FA[Function App] -->|VNet Integration| VNet[Virtual Network]
VNet --> DB["(Azure SQL\nPrivate)"]
VNet --> KV[Key Vault\nPrivate]
VNet --> SB[Service Bus\nPrivate]
Internet -->|Blocked| DB
Internet -->|Blocked| KV
# Enable VNet Integration for outbound traffic
az functionapp vnet-integration add \
--name my-function-app \
--resource-group myRG \
--vnet myVNet \
--subnet functions-integration-subnet
# Force all outbound traffic through VNet
az functionapp config appsettings set \
--name my-function-app \
--resource-group myRG \
--settings WEBSITE_VNET_ROUTE_ALL=1
Security levels summary
| Layer | Mechanism | Required plan |
|---|---|---|
| Inbound network | IP restrictions, Private Endpoint | Standard+ |
| Outbound network | VNet Integration | Premium+ |
| Authentication | Easy Auth (Azure AD, GitHub…) | All |
| Authorization | Function Keys (Anonymous/Function/Admin) | All |
| mTLS | Client Certificates | Standard+ |
| Secrets | Key Vault References + Managed Identity | All |
| API Gateway | Azure API Management | Separate |
Search Terms
azure · functions · deep · dive · serverless · microsoft · pattern · trigger · storage · http · output · binding · bindings · deployment · durable · monitoring · blob · configuration · integration · security · architecture · cold · consumption · function