Advanced

Azure Functions Deep Dive

Triggers, bindings, deployment, Durable Functions, security, observability and KEDA on Kubernetes.

Level: Intermediate to Advanced
Technologies: .NET 8 / C# / Isolated Worker Model / Azure Functions v4

Table of Contents

  1. Introduction and Fundamental Concepts
  2. HTTP Triggers – Functions triggered by HTTP
  3. Exploring Azure Functions Triggers
  4. Bindings – Connections to Azure Services
  5. Deployment to Azure
  6. Durable Functions – Resilient Workflows
  7. Azure Functions in Production
  8. Advanced Security
  9. Monitoring and Observability
  10. KEDA and Kubernetes
  11. Best Practices and Common Pitfalls
  12. 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.

PlanBillingCold StartsMax TimeoutVNet IntegrationMax InstancesTypical use cases
ConsumptionPer execution (1M free + 400K GB-sec/month)Yes (a few seconds)10 min (configurable)200Unpredictable workloads, prototyping, low volumes
Elastic Premium EP1Pre-warmed instances + scalingNo (pre-warmed)Unlimited100Latency-sensitive APIs, event processing
Elastic Premium EP2Same as EP1, more CPU/RAMNoUnlimited100CPU-intensive workloads
Elastic Premium EP3Same as EP2, even more powerfulNoUnlimited100Memory-intensive workloads
Dedicated (App Service)Monthly (existing ASP plan)NoUnlimitedPer planLong-running functions, sharing with web apps
Flex ConsumptionHybrid Consumption/PremiumLowVariableVariableOptimal cost/performance trade-off
Container AppsKubernetes-basedVariableVariableVariableContainerized 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:

AspectIn-Process (Legacy)Isolated Worker (Recommended)
ProcessSame process as Functions hostProcess separate from host
.NET RuntimeMust match Functions runtimeAny compatible .NET version
DependenciesShared with hostCompletely isolated
StartupFasterSlightly slower
FutureDeprecatedThis is the future of the platform
Dependency injectionIFunctionsHostBuilderStandard HostBuilder
MiddlewareLimitedFull 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:

  1. Implementing web APIs (RESTful APIs, GraphQL endpoints)
  2. 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

LevelDescriptionKey delivery methodUse case
AnonymousNo key requiredN/APublic webhooks, health checks, public endpoints
FunctionKey specific to this function?code=<key> or x-functions-key headerB2B integrations, secure webhooks
AdminApplication master key?code=<master-key> or x-functions-key headerAdministration 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:

TriggerNuGet PackageDescriptionUse case
HTTPExtensions.HttpHTTP/HTTPS requestsREST APIs, webhooks
TimerExtensions.TimerScheduled execution (cron)Periodic tasks, maintenance
Azure Storage QueueExtensions.Storage.QueuesMessages on Azure Queue StorageLightweight async processing
Azure Service BusExtensions.ServiceBusService Bus messagesEnterprise messaging, pub/sub
Azure Event GridExtensions.EventGridEvent Grid eventsReacting to Azure events
Azure Event HubsExtensions.EventHubsHigh-performance event streamsIoT, telemetry, streaming
Azure Blob StorageExtensions.Storage.BlobsBlob creation/modificationFile processing
Azure Cosmos DBExtensions.CosmosDBChanges in Cosmos DBSynchronization, ETL
Azure SQLExtensions.SqlChanges in Azure SQLCDC, synchronization
RabbitMQExtensions.RabbitMQRabbitMQ messagesOn-premise integration
IoT HubExtensions.EventHubsIoT eventsIoT processing
SignalRExtensions.SignalRSignalR messagesReal-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}
ExpressionDescription
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-5Monday through Friday at 9:30
0 0 12 1 * *First of every month at noon
0 0 0 * * 0Every 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 maxDequeueCount processing 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:

TypeDirectionDescription
Input BindingRead data from a service before execution
Output BindingWrite data to a service after execution
Trigger BindingTrigger 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 ProviderUse caseAdvantagesDisadvantages
Azure Storage (default)Most casesSimple, no extra config, low costLimited performance for very high load
NetheriteHigh performanceHigh throughput, low latencyMore complex configuration, Event Hubs cost
Microsoft SQL ServerExisting SQL environmentsFull control, standard backup, multi-tenancyRequires 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

CategoryBest practiceWhy
PerformanceKeep functions short and focusedReduce cold starts, optimize costs
PerformanceReuse HTTP connections (HttpClientFactory)Avoid socket exhaustion
PerformanceUse async/await everywhereDon’t block threads
PerformanceMinimize allocated memoryReduce GB-sec costs
SecurityUse Managed IdentitiesEliminate secrets from configs
SecurityKey Vault References for remaining secretsSecret rotation without redeployment
SecurityNever use AuthorizationLevel.Admin in productionExposing the master key = full access
ResilienceImplement idempotence in queue functionsMessages can be delivered multiple times
ResilienceMonitor poison queuesDetect problematic messages
ObservabilityStructured logs with context (OrderId, UserId, etc.)Ease debugging in production
ObservabilityConfigure alerts on key metricsReact before customers complain
DeploymentInfrastructure as Code (Bicep/Terraform)Reproducibility, living documentation
DeploymentBlue/Green or Slot deploymentZero downtime during updates
CostSet a daily quota on the Consumption planAvoid billing surprises
CostUse Premium plan only when necessaryConsumption 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}

TermDefinition
Activity FunctionFunction executing a concrete step in a Durable Functions workflow; can do I/O
App Service PlanHosting plan shared with Azure App Service; monthly billing
Application InsightsAzure monitoring service for application observability
AzuriteLocal emulator for Azure Storage (Blob, Queue, Table)
BicepDeclarative DSL language for Azure Resource Manager (Infrastructure as Code)
BindingDeclarative connection to an Azure service (input = read, output = write)
Cold StartInitialization delay when starting a Function instance after a period of inactivity
Consumption PlanAzure Functions serverless plan; per-execution billing with generous free quota
CORSCross-Origin Resource Sharing; browser security mechanism managing cross-domain requests
Durable FunctionsAzure Functions extension enabling stateful and resilient workflows
Elastic Premium PlanHosting plan with pre-warmed instances, VNet, unlimited execution
Event GridAzure publish-subscribe event routing service
Event HubHigh-performance Azure event streaming service (IoT, telemetry)
Fan-Out/Fan-InDurable Functions pattern for running activities in parallel then waiting for completion
Function AppAzure Functions deployment unit containing one or more functions
Host.jsonGlobal configuration file for the Azure Functions host
HttpTriggerTrigger activating a function by HTTP request
IdempotencyProperty of an operation producing the same result even if executed multiple times
Isolated Worker ModelC# model running Functions in a .NET process separate from the Functions host
KEDAKubernetes-based Event Driven Autoscaling; enables event-driven scaling on Kubernetes
Key Vault ReferenceReference to a Key Vault secret in App Settings, avoiding storing the secret directly
Managed IdentityAzure AD identity automatically managed by Azure for a resource; eliminates secrets
Orchestrator FunctionFunction defining the workflow and coordinating activities in Durable Functions
Output BindingBinding enabling a function to write data to an external service
Poison QueueQueue receiving messages that exceeded the maximum number of processing attempts
RBACRole-Based Access Control; Azure role-based access control system
Service BusAzure enterprise messaging service; supports queues and publish-subscribe topics
ServerlessParadigm where infrastructure is managed by the cloud provider; usage-based billing
Starter FunctionFunction starting a new Durable Functions orchestration
Task HubDurable Functions namespace for storing orchestration state
Timer TriggerTrigger activating a function according to a scheduled CRON expression
TriggerTrigger defining what activates the execution of an Azure Function
VNet IntegrationConnecting a Function App to a private Azure virtual network
WebhookMechanism allowing an external service to notify your application via an HTTP request

Key exam points

QuestionAnswer
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 levelsAnonymous, 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 triggersAzurite

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):

EntityRole
Control QueueControl messages (start, event, terminate)
Work-item QueueTasks to be executed by activity workers
History TableComplete execution journal (replay)
Instances TableCurrent state of each orchestration
BlobsLarge 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 → use context.CurrentUtcDateTime
  • Generate a random GUID → use context.NewGuid()
  • Perform I/O (HTTP, DB) → delegate to an Activity
  • Use Task.Delay → use context.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}
ExpressionTrigger
0 */5 * * * *Every 5 minutes
0 0 9 * * 1-59am, Monday–Friday
0 30 8 1 * *1st of each month at 8:30
0 0 0 * * 0Midnight 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 leases container 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

ExpressionDescription
{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
CriterionIn-ProcessIsolated Worker
.NET support.NET 6 (EOL).NET 8, .NET 9, .NET Framework 4.8
ASP.NET Core Middleware
Dependency injectionPartialComplete (IHostBuilder)
NuGet packagesCoupled to hostIndependent
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.json to .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)
FactorImpact
Deployment size (number of DLLs)High — minimize dependencies
Consumption planHigh — scale-to-zero
.NET framework (version).NET 8+: faster startup thanks to ReadyToRun
Number of injected servicesMedium — optimize DI
DB connections at startupHigh — 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

LayerMechanismRequired plan
Inbound networkIP restrictions, Private EndpointStandard+
Outbound networkVNet IntegrationPremium+
AuthenticationEasy Auth (Azure AD, GitHub…)All
AuthorizationFunction Keys (Anonymous/Function/Admin)All
mTLSClient CertificatesStandard+
SecretsKey Vault References + Managed IdentityAll
API GatewayAzure API ManagementSeparate

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

Interested in this course?

Contact us to book it or get a custom training plan for your team.