☁️

Azure Functions Guide

Cloud & Azure Intermediate 6 min read 1200 words

Azure Functions Guide for .NET Developers

Introduction

Azure Functions is Microsoft’s serverless compute platform that enables you to run event-driven code without managing infrastructure. This guide covers triggers, bindings, Durable Functions, hosting plans, and best practices for building scalable .NET applications.


Table of Contents


Serverless Architecture Concepts

Serverless doesn’t mean β€œno servers” - it means you don’t manage them. The cloud provider handles infrastructure, scaling, and availability.

Serverless Benefits

Benefit Description
No infrastructure management Focus on code, not servers
Auto-scaling Scales from 0 to thousands automatically
Pay-per-execution Only pay when code runs
Built-in high availability Platform handles failures
Event-driven Respond to events from many sources

When to Use Serverless

Good fit for serverless:
β”œβ”€β”€ Event-driven workloads (file uploads, queue messages)
β”œβ”€β”€ Scheduled tasks (cleanup jobs, reports)
β”œβ”€β”€ Webhooks and API endpoints
β”œβ”€β”€ Data processing pipelines
β”œβ”€β”€ Microservices with variable traffic
└── Background processing

Poor fit for serverless:
β”œβ”€β”€ Long-running processes (>10 min on Consumption)
β”œβ”€β”€ Consistent, high-throughput workloads
β”œβ”€β”€ Applications requiring WebSockets
β”œβ”€β”€ Workloads sensitive to cold starts
└── Applications needing persistent connections

Triggers and Bindings

Triggers cause a function to run. Bindings connect resources to your function declaratively.

Common Triggers

HTTP Trigger

public class HttpFunctions
{
    private readonly ILogger<HttpFunctions> _logger;

    public HttpFunctions(ILogger<HttpFunctions> logger)
    {
        _logger = logger;
    }

    [Function("GetUser")]
    public async Task<IActionResult> GetUser(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "users/{id}")] HttpRequest req,
        string id)
    {
        _logger.LogInformation("Getting user {UserId}", id);

        var user = await GetUserByIdAsync(id);

        return user != null
            ? new OkObjectResult(user)
            : new NotFoundResult();
    }

    [Function("CreateUser")]
    public async Task<IActionResult> CreateUser(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = "users")] HttpRequest req)
    {
        var user = await req.ReadFromJsonAsync<User>();

        if (user == null)
            return new BadRequestObjectResult("Invalid user data");

        await SaveUserAsync(user);

        return new CreatedResult($"/api/users/{user.Id}", user);
    }
}

Timer Trigger

public class TimerFunctions
{
    private readonly ICleanupService _cleanupService;

    public TimerFunctions(ICleanupService cleanupService)
    {
        _cleanupService = cleanupService;
    }

    // Run every hour at minute 0
    [Function("HourlyCleanup")]
    public async Task HourlyCleanup(
        [TimerTrigger("0 0 * * * *")] TimerInfo timer)
    {
        await _cleanupService.CleanupExpiredSessionsAsync();
    }

    // Run every day at 2 AM UTC
    [Function("DailyReport")]
    public async Task DailyReport(
        [TimerTrigger("0 0 2 * * *")] TimerInfo timer)
    {
        await _reportService.GenerateDailyReportAsync();
    }

    // Run every 5 minutes
    [Function("HealthCheck")]
    public async Task HealthCheck(
        [TimerTrigger("0 */5 * * * *")] TimerInfo timer)
    {
        await _healthService.CheckExternalServicesAsync();
    }
}

// CRON Expression Reference:
// {second} {minute} {hour} {day} {month} {day-of-week}
// 0 0 * * * *     = Every hour
// 0 */5 * * * *   = Every 5 minutes
// 0 0 0 * * *     = Daily at midnight
// 0 0 0 * * 0     = Weekly on Sunday
// 0 0 0 1 * *     = Monthly on the 1st

Queue Trigger (Azure Storage Queue)

public class QueueFunctions
{
    private readonly IOrderProcessor _orderProcessor;

    public QueueFunctions(IOrderProcessor orderProcessor)
    {
        _orderProcessor = orderProcessor;
    }

    [Function("ProcessOrder")]
    public async Task ProcessOrder(
        [QueueTrigger("orders", Connection = "AzureWebJobsStorage")] Order order)
    {
        await _orderProcessor.ProcessAsync(order);
    }

    // With output binding to another queue
    [Function("ValidateOrder")]
    [QueueOutput("validated-orders", Connection = "AzureWebJobsStorage")]
    public async Task<ValidatedOrder> ValidateOrder(
        [QueueTrigger("new-orders", Connection = "AzureWebJobsStorage")] Order order)
    {
        var validatedOrder = await _orderProcessor.ValidateAsync(order);
        return validatedOrder; // Automatically sent to output queue
    }
}

Service Bus Trigger

public class ServiceBusFunctions
{
    [Function("ProcessServiceBusMessage")]
    public async Task ProcessMessage(
        [ServiceBusTrigger("orders", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message,
        ServiceBusMessageActions messageActions)
    {
        var order = message.Body.ToObjectFromJson<Order>();

        try
        {
            await ProcessOrderAsync(order);
            await messageActions.CompleteMessageAsync(message);
        }
        catch (Exception ex)
        {
            // Dead-letter the message for investigation
            await messageActions.DeadLetterMessageAsync(message, ex.Message);
        }
    }

    // Topic subscription
    [Function("ProcessTopicMessage")]
    public async Task ProcessTopicMessage(
        [ServiceBusTrigger("order-events", "order-processor", Connection = "ServiceBusConnection")]
        OrderEvent orderEvent)
    {
        await HandleOrderEventAsync(orderEvent);
    }
}

Blob Trigger

public class BlobFunctions
{
    private readonly IImageProcessor _imageProcessor;

    public BlobFunctions(IImageProcessor imageProcessor)
    {
        _imageProcessor = imageProcessor;
    }

    // Trigger when file uploaded to container
    [Function("ProcessImage")]
    [BlobOutput("thumbnails/{name}", Connection = "AzureWebJobsStorage")]
    public async Task<byte[]> ProcessImage(
        [BlobTrigger("images/{name}", Connection = "AzureWebJobsStorage")] Stream imageStream,
        string name)
    {
        return await _imageProcessor.CreateThumbnailAsync(imageStream);
    }

    // Read blob content with input binding
    [Function("AnalyzeDocument")]
    public async Task<IActionResult> AnalyzeDocument(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "documents/{fileName}")] HttpRequest req,
        [BlobInput("documents/{fileName}", Connection = "AzureWebJobsStorage")] string documentContent,
        string fileName)
    {
        var analysis = await _analyzer.AnalyzeAsync(documentContent);
        return new OkObjectResult(analysis);
    }
}

Cosmos DB Trigger (Change Feed)

public class CosmosDbFunctions
{
    [Function("ProcessCosmosChanges")]
    public async Task ProcessChanges(
        [CosmosDBTrigger(
            databaseName: "OrdersDb",
            containerName: "Orders",
            Connection = "CosmosDbConnection",
            LeaseContainerName = "leases",
            CreateLeaseContainerIfNotExists = true)] IReadOnlyList<Order> changedOrders)
    {
        foreach (var order in changedOrders)
        {
            await HandleOrderChangeAsync(order);
        }
    }

    // Output binding to Cosmos DB
    [Function("SaveOrder")]
    [CosmosDBOutput(
        databaseName: "OrdersDb",
        containerName: "Orders",
        Connection = "CosmosDbConnection")]
    public Order SaveOrder(
        [HttpTrigger(AuthorizationLevel.Function, "post")] Order order)
    {
        order.Id = Guid.NewGuid().ToString();
        order.CreatedAt = DateTime.UtcNow;
        return order; // Automatically saved to Cosmos DB
    }
}

Event Hub Trigger

public class EventHubFunctions
{
    [Function("ProcessTelemetry")]
    public async Task ProcessTelemetry(
        [EventHubTrigger("telemetry", Connection = "EventHubConnection")] EventData[] events)
    {
        foreach (var eventData in events)
        {
            var telemetry = JsonSerializer.Deserialize<TelemetryData>(eventData.Body.Span);
            await ProcessTelemetryAsync(telemetry);
        }
    }

    // With partition context
    [Function("ProcessTelemetryBatch")]
    public async Task ProcessBatch(
        [EventHubTrigger("telemetry", Connection = "EventHubConnection")] EventData[] events,
        FunctionContext context)
    {
        var logger = context.GetLogger("ProcessTelemetryBatch");
        logger.LogInformation("Processing batch of {Count} events", events.Length);

        await Parallel.ForEachAsync(events, async (eventData, ct) =>
        {
            await ProcessEventAsync(eventData);
        });
    }
}

Triggers Summary Table

Trigger Use Case Scaling
HTTP APIs, webhooks Based on concurrent requests
Timer Scheduled tasks Single instance
Queue Async processing Based on queue length
Service Bus Enterprise messaging Based on message count
Blob File processing Based on blob count
Cosmos DB Change feed processing Based on partition count
Event Hub High-throughput streaming Based on partition count
Event Grid Event-driven Based on event rate

Function Hosting Plans

Plan Comparison

Feature Consumption Premium Dedicated
Default timeout 5 min (max 10) 30 min (unlimited) 30 min (unlimited)
Max instances 200 100 10-30
Cold starts Yes No (pre-warmed) No
VNet support No Yes Yes
Scale to zero Yes Optional No
Pricing Per execution Per second + instances Per hour
Best for Sporadic workloads Low-latency, VNet Predictable workloads

Consumption Plan

// host.json - Consumption plan configuration
{
    "version": "2.0",
    "functionTimeout": "00:10:00",
    "extensions": {
        "queues": {
            "batchSize": 16,
            "maxPollingInterval": "00:00:02",
            "visibilityTimeout": "00:00:30",
            "maxDequeueCount": 5
        }
    }
}

Premium Plan

# Create Premium plan
az functionapp plan create \
  --name my-premium-plan \
  --resource-group my-rg \
  --location eastus \
  --sku EP1 \
  --min-instances 1 \
  --max-burst 10

# Configure always-ready instances
az functionapp update \
  --name my-function-app \
  --resource-group my-rg \
  --set siteConfig.minimumElasticInstanceCount=2

Flex Consumption (Preview 2025)

# New Flex Consumption plan - combines best of both
az functionapp create \
  --name my-flex-app \
  --resource-group my-rg \
  --storage-account mystorageaccount \
  --flexconsumption-location eastus \
  --runtime dotnet-isolated \
  --runtime-version 8

Durable Functions

Durable Functions extends Azure Functions to write stateful workflows in a serverless environment.

Function Types

Type Purpose Example
Orchestrator Defines workflow logic Coordinates activity execution
Activity Unit of work Database call, API request
Entity Stateful entity Shopping cart, counter
Client Starts orchestrations HTTP trigger that kicks off workflow

Pattern 1: Function Chaining

Execute activities in sequence.

public class OrderWorkflow
{
    [Function("OrderOrchestrator")]
    public static async Task<OrderResult> RunOrchestrator(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        var order = context.GetInput<Order>();

        // Chain activities sequentially
        var validated = await context.CallActivityAsync<Order>("ValidateOrder", order);
        var reserved = await context.CallActivityAsync<Order>("ReserveInventory", validated);
        var charged = await context.CallActivityAsync<Order>("ProcessPayment", reserved);
        var shipped = await context.CallActivityAsync<OrderResult>("ShipOrder", charged);

        return shipped;
    }

    [Function("ValidateOrder")]
    public static Order ValidateOrder(
        [ActivityTrigger] Order order)
    {
        // Validate order logic
        if (order.Items.Count == 0)
            throw new InvalidOperationException("Order has no items");

        order.Status = "Validated";
        return order;
    }

    [Function("ReserveInventory")]
    public static async Task<Order> ReserveInventory(
        [ActivityTrigger] Order order,
        FunctionContext context)
    {
        var inventoryService = context.InstanceServices.GetRequiredService<IInventoryService>();
        await inventoryService.ReserveAsync(order.Items);
        order.Status = "InventoryReserved";
        return order;
    }

    // Client function to start orchestration
    [Function("StartOrder")]
    public static async Task<IActionResult> StartOrder(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
        [DurableClient] DurableTaskClient client)
    {
        var order = await req.ReadFromJsonAsync<Order>();

        string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
            "OrderOrchestrator",
            order);

        return new AcceptedResult(
            $"/api/orders/{instanceId}",
            new { instanceId });
    }
}

Pattern 2: Fan-Out/Fan-In

Execute activities in parallel and aggregate results.

[Function("ParallelProcessingOrchestrator")]
public static async Task<ProcessingResult> RunParallel(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var documents = context.GetInput<List<Document>>();

    // Fan-out: Process all documents in parallel
    var tasks = documents.Select(doc =>
        context.CallActivityAsync<ProcessedDocument>("ProcessDocument", doc));

    var results = await Task.WhenAll(tasks);

    // Fan-in: Aggregate results
    var aggregated = await context.CallActivityAsync<ProcessingResult>(
        "AggregateResults",
        results.ToList());

    return aggregated;
}

[Function("ProcessDocument")]
public static async Task<ProcessedDocument> ProcessDocument(
    [ActivityTrigger] Document document)
{
    // Process individual document
    return new ProcessedDocument
    {
        Id = document.Id,
        ProcessedAt = DateTime.UtcNow,
        Status = "Completed"
    };
}

Pattern 3: Async HTTP APIs

Long-running operations with status polling.

[Function("StartLongRunningProcess")]
public static async Task<HttpResponseData> Start(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
    [DurableClient] DurableTaskClient client,
    FunctionContext context)
{
    var input = await req.ReadFromJsonAsync<ProcessInput>();

    string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
        "LongRunningOrchestrator",
        input);

    // Return URLs for status checking
    var response = req.CreateResponse(HttpStatusCode.Accepted);

    var baseUrl = $"{req.Url.Scheme}://{req.Url.Host}";
    await response.WriteAsJsonAsync(new
    {
        id = instanceId,
        statusQueryGetUri = $"{baseUrl}/api/status/{instanceId}",
        sendEventPostUri = $"{baseUrl}/api/events/{instanceId}",
        terminatePostUri = $"{baseUrl}/api/terminate/{instanceId}"
    });

    return response;
}

[Function("GetStatus")]
public static async Task<HttpResponseData> GetStatus(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "status/{instanceId}")] HttpRequestData req,
    [DurableClient] DurableTaskClient client,
    string instanceId)
{
    var status = await client.GetInstanceAsync(instanceId);

    var response = req.CreateResponse(
        status == null ? HttpStatusCode.NotFound : HttpStatusCode.OK);

    if (status != null)
    {
        await response.WriteAsJsonAsync(new
        {
            status.Name,
            RuntimeStatus = status.RuntimeStatus.ToString(),
            status.CreatedAt,
            status.LastUpdatedAt,
            Output = status.ReadOutputAs<object>()
        });
    }

    return response;
}

Pattern 4: Human Interaction (Approval Workflow)

[Function("ApprovalOrchestrator")]
public static async Task<ApprovalResult> RunApproval(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var request = context.GetInput<ApprovalRequest>();

    // Send approval notification
    await context.CallActivityAsync("SendApprovalRequest", request);

    // Wait for approval with timeout
    using var cts = new CancellationTokenSource();
    var approvalTask = context.WaitForExternalEvent<ApprovalResponse>("ApprovalEvent");
    var timeoutTask = context.CreateTimer(TimeSpan.FromHours(24), cts.Token);

    var winner = await Task.WhenAny(approvalTask, timeoutTask);

    if (winner == approvalTask)
    {
        cts.Cancel(); // Cancel the timer
        var response = approvalTask.Result;

        if (response.IsApproved)
        {
            await context.CallActivityAsync("ProcessApprovedRequest", request);
            return new ApprovalResult { Status = "Approved" };
        }
        else
        {
            await context.CallActivityAsync("HandleRejection", request);
            return new ApprovalResult { Status = "Rejected" };
        }
    }
    else
    {
        // Timeout - escalate
        await context.CallActivityAsync("EscalateRequest", request);
        return new ApprovalResult { Status = "Escalated" };
    }
}

// Endpoint to submit approval decision
[Function("SubmitApproval")]
public static async Task<IActionResult> SubmitApproval(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = "approve/{instanceId}")] HttpRequest req,
    [DurableClient] DurableTaskClient client,
    string instanceId)
{
    var response = await req.ReadFromJsonAsync<ApprovalResponse>();

    await client.RaiseEventAsync(instanceId, "ApprovalEvent", response);

    return new OkResult();
}

Pattern 5: Monitor (Polling)

[Function("MonitorOrchestrator")]
public static async Task<MonitorResult> RunMonitor(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var jobId = context.GetInput<string>();
    var expiryTime = context.CurrentUtcDateTime.AddHours(2);

    while (context.CurrentUtcDateTime < expiryTime)
    {
        var status = await context.CallActivityAsync<JobStatus>("CheckJobStatus", jobId);

        if (status.IsComplete)
        {
            return new MonitorResult { Status = "Completed", Result = status.Result };
        }

        // Wait before next check (don't use Task.Delay in orchestrators!)
        var nextCheck = context.CurrentUtcDateTime.AddSeconds(30);
        await context.CreateTimer(nextCheck, CancellationToken.None);
    }

    return new MonitorResult { Status = "Timeout" };
}

Orchestrator Code Constraints

// ❌ DON'T: Use non-deterministic APIs in orchestrators
[Function("BadOrchestrator")]
public static async Task BadOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
{
    // ❌ Wrong: Non-deterministic
    var now = DateTime.UtcNow;  // Use context.CurrentUtcDateTime instead
    var random = new Random().Next();  // Don't use random
    var guid = Guid.NewGuid();  // Use context.NewGuid() instead

    // ❌ Wrong: I/O operations
    await File.ReadAllTextAsync("file.txt");  // Use activity function
    await httpClient.GetAsync("https://api.com");  // Use activity function
}

// βœ… DO: Use deterministic APIs
[Function("GoodOrchestrator")]
public static async Task GoodOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
{
    // βœ… Correct: Use context methods
    var now = context.CurrentUtcDateTime;
    var guid = context.NewGuid();

    // βœ… Correct: Delegate I/O to activities
    var data = await context.CallActivityAsync<string>("ReadData", "file.txt");
    var response = await context.CallActivityAsync<string>("CallApi", "https://api.com");
}

Cold Start Mitigation

Cold starts occur when a function app scales from zero instances.

Strategies by Plan

Strategy Consumption Premium Dedicated
Pre-warmed instances N/A βœ… N/A
Always-ready instances N/A βœ… βœ… (Always On)
Keep-alive pings βœ… N/A N/A
Warmup triggers βœ… βœ… N/A
Reduce package size βœ… βœ… βœ…

Premium Plan Configuration

# Set minimum always-ready instances
az functionapp update \
  --name my-function-app \
  --resource-group my-rg \
  --set siteConfig.minimumElasticInstanceCount=2

# Set prewarmed instance count
az functionapp update \
  --name my-function-app \
  --resource-group my-rg \
  --set siteConfig.preWarmedInstanceCount=1

Warmup Trigger

public class WarmupFunctions
{
    private readonly IServiceProvider _services;

    public WarmupFunctions(IServiceProvider services)
    {
        _services = services;
    }

    [Function("Warmup")]
    public void Warmup([WarmupTrigger] object warmupContext, FunctionContext context)
    {
        var logger = context.GetLogger("Warmup");
        logger.LogInformation("Function app warming up");

        // Pre-initialize expensive services
        var dbContext = _services.GetRequiredService<ApplicationDbContext>();
        dbContext.Database.CanConnect();

        // Pre-load cached data
        var cache = _services.GetRequiredService<ICacheService>();
        cache.WarmupAsync().GetAwaiter().GetResult();

        logger.LogInformation("Warmup complete");
    }
}

Code Optimization for Faster Starts

// Program.cs - Optimize startup
var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices((context, services) =>
    {
        // Use lazy initialization for expensive services
        services.AddSingleton<IExpensiveService>(sp =>
            new Lazy<ExpensiveService>(() => new ExpensiveService()).Value);

        // Avoid synchronous initialization in constructors
        services.AddSingleton<ICacheService, CacheService>();
    })
    .Build();

// Avoid in constructors:
// - Network calls
// - File I/O
// - Database connections
// - Heavy computations

// ❌ Bad: Blocking constructor
public class BadService
{
    private readonly Dictionary<string, string> _data;

    public BadService(IConfiguration config)
    {
        // This blocks cold start
        _data = LoadDataFromDatabase().GetAwaiter().GetResult();
    }
}

// βœ… Good: Lazy initialization
public class GoodService
{
    private readonly Lazy<Task<Dictionary<string, string>>> _dataTask;

    public GoodService(IConfiguration config)
    {
        _dataTask = new Lazy<Task<Dictionary<string, string>>>(() => LoadDataFromDatabaseAsync());
    }

    public async Task<string> GetValueAsync(string key)
    {
        var data = await _dataTask.Value;
        return data[key];
    }
}

Dependency Injection

Setting Up DI

// Program.cs
var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices((context, services) =>
    {
        // Configuration
        services.AddOptions<MyOptions>()
            .Bind(context.Configuration.GetSection("MyOptions"));

        // Database
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(context.Configuration.GetConnectionString("Database")));

        // HTTP clients
        services.AddHttpClient<IExternalApiClient, ExternalApiClient>(client =>
        {
            client.BaseAddress = new Uri(context.Configuration["ExternalApi:BaseUrl"]);
        });

        // Custom services
        services.AddScoped<IOrderService, OrderService>();
        services.AddScoped<IInventoryService, InventoryService>();
        services.AddSingleton<ICacheService, RedisCacheService>();

        // Azure services with managed identity
        services.AddSingleton(_ =>
        {
            return new BlobServiceClient(
                new Uri(context.Configuration["Storage:BlobEndpoint"]),
                new DefaultAzureCredential());
        });
    })
    .Build();

await host.RunAsync();

Using DI in Functions

public class OrderFunctions
{
    private readonly IOrderService _orderService;
    private readonly ILogger<OrderFunctions> _logger;

    // Constructor injection
    public OrderFunctions(IOrderService orderService, ILogger<OrderFunctions> logger)
    {
        _orderService = orderService;
        _logger = logger;
    }

    [Function("ProcessOrder")]
    public async Task<IActionResult> ProcessOrder(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req)
    {
        var order = await req.ReadFromJsonAsync<Order>();
        await _orderService.ProcessAsync(order);
        return new OkResult();
    }
}

Local Development and Debugging

Project Setup

<!-- Project file -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.21.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.17.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.1.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Timer" Version="4.3.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues" Version="5.3.0" />
    <PackageReference Include="Microsoft.DurableTask.Client" Version="1.1.0" />
    <PackageReference Include="Microsoft.DurableTask.Worker" Version="1.1.0" />
  </ItemGroup>
</Project>

Local Settings

// local.settings.json
{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
        "ServiceBusConnection": "Endpoint=sb://...",
        "CosmosDbConnection": "AccountEndpoint=https://...",
        "ASPNETCORE_ENVIRONMENT": "Development"
    },
    "ConnectionStrings": {
        "Database": "Server=localhost;Database=MyDb;..."
    }
}

Running Locally

# Start Azure Storage Emulator (Azurite)
azurite --silent --location ./azurite --debug ./azurite/debug.log

# Run functions locally
func start

# Or with Visual Studio / VS Code debugging
dotnet run

Debugging Tips

// Add detailed logging for debugging
[Function("DebugFunction")]
public async Task<IActionResult> Debug(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req,
    FunctionContext context)
{
    var logger = context.GetLogger("DebugFunction");

    logger.LogInformation("Invocation ID: {InvocationId}", context.InvocationId);
    logger.LogInformation("Function Name: {FunctionName}", context.FunctionDefinition.Name);

    // Log all headers
    foreach (var header in req.Headers)
    {
        logger.LogDebug("Header: {Key} = {Value}", header.Key, header.Value);
    }

    return new OkObjectResult(new { Status = "OK" });
}

Deployment Strategies

GitHub Actions

name: Deploy Azure Functions

on:
  push:
    branches: [main]

env:
  AZURE_FUNCTIONAPP_NAME: my-function-app
  AZURE_FUNCTIONAPP_PACKAGE_PATH: './src/Functions'
  DOTNET_VERSION: '8.0.x'

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Build
        run: |
          cd ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}
          dotnet build --configuration Release
          dotnet publish -c Release -o ./output

      - name: Deploy
        uses: Azure/functions-action@v1
        with:
          app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }}
          package: '${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output'
          publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}

Deployment Slots

# Create staging slot
az functionapp deployment slot create \
  --name my-function-app \
  --resource-group my-rg \
  --slot staging

# Deploy to staging
func azure functionapp publish my-function-app --slot staging

# Swap to production
az functionapp deployment slot swap \
  --name my-function-app \
  --resource-group my-rg \
  --slot staging \
  --target-slot production

Monitoring with Application Insights

Configuration

// host.json
{
    "version": "2.0",
    "logging": {
        "applicationInsights": {
            "samplingSettings": {
                "isEnabled": true,
                "maxTelemetryItemsPerSecond": 20,
                "excludedTypes": "Request"
            }
        },
        "logLevel": {
            "default": "Information",
            "Host.Results": "Error",
            "Function": "Information",
            "Host.Aggregator": "Trace"
        }
    }
}

Custom Telemetry

public class TelemetryFunctions
{
    private readonly TelemetryClient _telemetry;

    public TelemetryFunctions(TelemetryClient telemetry)
    {
        _telemetry = telemetry;
    }

    [Function("ProcessWithTelemetry")]
    public async Task Process([QueueTrigger("items")] Item item)
    {
        using var operation = _telemetry.StartOperation<RequestTelemetry>("ProcessItem");

        try
        {
            _telemetry.TrackEvent("ItemProcessingStarted", new Dictionary<string, string>
            {
                ["ItemId"] = item.Id,
                ["ItemType"] = item.Type
            });

            await ProcessItemAsync(item);

            _telemetry.TrackMetric("ItemProcessingDuration", operation.Telemetry.Duration.TotalMilliseconds);
            operation.Telemetry.Success = true;
        }
        catch (Exception ex)
        {
            _telemetry.TrackException(ex);
            operation.Telemetry.Success = false;
            throw;
        }
    }
}

Security Best Practices

Function-Level Authorization

// Authorization levels
public enum AuthorizationLevel
{
    Anonymous,    // No key required
    Function,     // Function-specific key
    Admin         // Master key only
}

// Use Function level for APIs
[Function("SecureEndpoint")]
public async Task<IActionResult> SecureEndpoint(
    [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req)
{
    // Caller must include x-functions-key header or ?code= query param
    return new OkResult();
}

Managed Identity

// Access Azure services without storing credentials
var credential = new DefaultAzureCredential();

// Key Vault
var secretClient = new SecretClient(
    new Uri("https://myvault.vault.azure.net/"),
    credential);

// Storage
var blobClient = new BlobServiceClient(
    new Uri("https://mystorage.blob.core.windows.net/"),
    credential);

// SQL Database (with Azure AD auth)
var connectionString =
    "Server=myserver.database.windows.net;Database=mydb;Authentication=Active Directory Managed Identity;";

Comparison with Alternatives

Feature Azure Functions AWS Lambda Google Cloud Functions
Max timeout 10min (Consumption) / Unlimited 15 minutes 9 minutes (HTTP) / 60 min
Cold start Higher on Consumption Moderate Lower
Durable workflows Native (Durable Functions) Step Functions (separate) Cloud Workflows (separate)
Languages C#, JS, Python, Java, PowerShell Many Node, Python, Go, Java
Triggers 15+ native 6+ 6+
Container support Yes Yes Yes
Local dev Core Tools SAM CLI Functions Framework

Interview Questions

1. What are the differences between Consumption, Premium, and Dedicated plans?

Answer:

  • Consumption: Pay-per-execution, auto-scales 0-200 instances, has cold starts, 10-minute max timeout. Best for sporadic workloads.
  • Premium: Pre-warmed instances (no cold starts), VNet support, 100 max instances, unlimited timeout. Best for production workloads needing low latency.
  • Dedicated: Runs on App Service plan (always-on), no auto-scale by default, predictable pricing. Best for existing App Service customers or predictable workloads.

2. Explain the constraint β€œorchestrator functions must be deterministic.”

Answer: Orchestrator functions are replayed multiple times during execution (after every await). To produce consistent results:

Don’t use:

  • DateTime.Now or DateTime.UtcNow (use context.CurrentUtcDateTime)
  • Guid.NewGuid() (use context.NewGuid())
  • Random class
  • Network/file I/O (use activity functions)
  • Thread.Sleep or Task.Delay (use context.CreateTimer)

3. How would you handle a long-running process that exceeds the function timeout?

Answer:

  1. Durable Functions: Break into orchestrator + activities. Checkpoints automatically on each await.
  2. Fan-out pattern: Split work into parallel activities.
  3. Queue-based: Process items one at a time via queue trigger.
  4. Upgrade plan: Premium/Dedicated for longer timeouts.
  5. External service: Offload to Azure Batch or Container Instances for very long jobs.

4. What strategies would you use to mitigate cold starts?

Answer:

  1. Premium Plan: Pre-warmed and always-ready instances
  2. Warmup trigger: Pre-initialize dependencies
  3. Keep package small: Reduce deployment size
  4. Lazy initialization: Don’t block on startup
  5. Avoid heavy constructors: No I/O in constructors
  6. Keep-alive pings: (Consumption) External service to ping function regularly

5. When would you use Event Hub vs Service Bus vs Storage Queue?

Answer:

Scenario Best Choice
High-throughput streaming (millions/sec) Event Hub
Guaranteed message delivery Service Bus
Simple async processing Storage Queue
Ordered messages within partition Event Hub or Service Bus (sessions)
Dead-letter handling Service Bus
Publish-subscribe (topics) Service Bus
IoT telemetry Event Hub
Enterprise integration Service Bus

Key Takeaways

  1. Choose the right plan - Consumption for sporadic, Premium for production
  2. Use Durable Functions - For workflows, long-running, and stateful processes
  3. Design for idempotency - Functions may execute multiple times
  4. Handle cold starts - Pre-warm instances, optimize startup code
  5. Monitor with App Insights - Essential for production troubleshooting
  6. Use managed identity - No credentials in code
  7. Follow orchestrator constraints - Keep orchestrators deterministic

Further Reading

πŸ“š Related Articles