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
- Triggers and Bindings
- Function Hosting Plans
- Durable Functions
- Cold Start Mitigation
- Dependency Injection
- Local Development and Debugging
- Deployment Strategies
- Monitoring with Application Insights
- Security Best Practices
- Performance Optimization
- Comparison with Alternatives
- Interview Questions
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.NoworDateTime.UtcNow(usecontext.CurrentUtcDateTime)Guid.NewGuid()(usecontext.NewGuid())Randomclass- Network/file I/O (use activity functions)
Thread.SleeporTask.Delay(usecontext.CreateTimer)
3. How would you handle a long-running process that exceeds the function timeout?
Answer:
- Durable Functions: Break into orchestrator + activities. Checkpoints automatically on each await.
- Fan-out pattern: Split work into parallel activities.
- Queue-based: Process items one at a time via queue trigger.
- Upgrade plan: Premium/Dedicated for longer timeouts.
- External service: Offload to Azure Batch or Container Instances for very long jobs.
4. What strategies would you use to mitigate cold starts?
Answer:
- Premium Plan: Pre-warmed and always-ready instances
- Warmup trigger: Pre-initialize dependencies
- Keep package small: Reduce deployment size
- Lazy initialization: Donβt block on startup
- Avoid heavy constructors: No I/O in constructors
- 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
- Choose the right plan - Consumption for sporadic, Premium for production
- Use Durable Functions - For workflows, long-running, and stateful processes
- Design for idempotency - Functions may execute multiple times
- Handle cold starts - Pre-warm instances, optimize startup code
- Monitor with App Insights - Essential for production troubleshooting
- Use managed identity - No credentials in code
- Follow orchestrator constraints - Keep orchestrators deterministic