EngineeringSenior DeveloperTechnical Sessions

Session 21: Azure Functions & Serverless

Mục tiêu: Master Azure Functions triggers/bindings, Durable Functions, và serverless patterns cho .NET.


1. Triggers & Bindings

Trigger: khởi động function (HTTP, Timer, Queue, Blob, Event Grid...)
Input Binding: đọc data vào function
Output Binding: ghi data ra từ function

┌──────────┐   Trigger    ┌────────────┐   Output    ┌──────────┐
│  Event   │─────────────→│  Function  │────────────→│  Target  │
│  Source  │              │   Code     │             │  (DB,    │
└──────────┘   Input      │            │             │  Queue,  │
│  Cosmos  │─────────────→│            │             │  Blob)   │
└──────────┘              └────────────┘             └──────────┘

Isolated Worker Model (.NET 8/9)

csharp
// Program.cs - .NET 8+, Isolated Worker (recommended)
var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(services =>
    {
        services.AddDbContext<AppDbContext>();
        services.AddSingleton<IEmailService, SendGridEmailService>();
    })
    .Build();

host.Run();

// HTTP Trigger
public class ProductFunctions(AppDbContext db, ILogger<ProductFunctions> logger)
{
    [Function("GetProducts")]
    public async Task<HttpResponseData> GetProducts(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "products")] HttpRequestData req)
    {
        var products = await db.Products.AsNoTracking().ToListAsync();

        var response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteAsJsonAsync(products);
        return response;
    }

    [Function("GetProduct")]
    public async Task<HttpResponseData> GetProduct(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "products/{id}")] HttpRequestData req,
        Guid id)
    {
        var product = await db.Products.FindAsync(id);
        if (product is null)
            return req.CreateResponse(HttpStatusCode.NotFound);

        var response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteAsJsonAsync(product);
        return response;
    }
}

// Service Bus Trigger
public class OrderProcessor(ILogger<OrderProcessor> logger)
{
    [Function("ProcessOrder")]
    public async Task ProcessOrder(
        [ServiceBusTrigger("order-queue", Connection = "ServiceBusConnection")] OrderMessage message,
        FunctionContext context)
    {
        logger.LogInformation("Processing order {OrderId}", message.OrderId);
        // Process order...
    }
}

// Timer Trigger
public class ScheduledJobs(ILogger<ScheduledJobs> logger)
{
    [Function("DailyCleanup")]
    public async Task DailyCleanup(
        [TimerTrigger("0 0 2 * * *")] TimerInfo timer) // Daily at 2 AM UTC
    {
        logger.LogInformation("Running daily cleanup at {Time}", DateTime.UtcNow);
        // Cleanup expired records, temp files, etc.
    }
}

// Blob Trigger
public class FileProcessor
{
    [Function("ProcessUpload")]
    [BlobOutput("processed/{name}", Connection = "StorageConnection")]
    public async Task<byte[]> ProcessUpload(
        [BlobTrigger("uploads/{name}", Connection = "StorageConnection")] byte[] content,
        string name,
        FunctionContext context)
    {
        var logger = context.GetLogger<FileProcessor>();
        logger.LogInformation("Processing file: {FileName}, size: {Size}", name, content.Length);

        // Process file: resize image, convert format, etc.
        var processed = await ProcessImage(content);
        return processed; // Auto-saved to "processed" container
    }
}

2. Durable Functions

Orchestration Patterns

csharp
// Pattern 1: Function Chaining (sequential)
[Function("OrderOrchestration")]
public static async Task<OrderResult> RunOrchestrator(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var order = context.GetInput<OrderRequest>()!;

    // Each step runs in separate function, state persisted
    var validationResult = await context.CallActivityAsync<ValidationResult>(
        "ValidateOrder", order);

    if (!validationResult.IsValid)
        return OrderResult.Failed(validationResult.Errors);

    var inventoryResult = await context.CallActivityAsync<bool>(
        "ReserveInventory", order);

    var paymentResult = await context.CallActivityAsync<PaymentResult>(
        "ProcessPayment", order);

    var confirmation = await context.CallActivityAsync<string>(
        "SendConfirmation", order);

    return OrderResult.Success(confirmation);
}

// Activity functions
[Function("ValidateOrder")]
public static ValidationResult ValidateOrder(
    [ActivityTrigger] OrderRequest order) => /* validate */;

[Function("ReserveInventory")]
public static async Task<bool> ReserveInventory(
    [ActivityTrigger] OrderRequest order) => /* reserve stock */;

// Starter
[Function("StartOrderProcessing")]
public static async Task<HttpResponseData> Start(
    [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req,
    [DurableClient] DurableTaskClient client)
{
    var order = await req.ReadFromJsonAsync<OrderRequest>();
    string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
        "OrderOrchestration", order);

    return await client.CreateCheckStatusResponseAsync(req, instanceId);
}
csharp
// Pattern 2: Fan-out/Fan-in (parallel processing)
[Function("BatchProcessOrchestrator")]
public static async Task<int> BatchProcess(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var files = await context.CallActivityAsync<List<string>>("GetPendingFiles");

    // Fan-out: process ALL files in parallel
    var tasks = files.Select(file =>
        context.CallActivityAsync<ProcessResult>("ProcessFile", file));

    var results = await Task.WhenAll(tasks);

    // Fan-in: aggregate results
    return results.Count(r => r.IsSuccess);
}

// Pattern 3: Human Interaction (approval workflow)
[Function("ApprovalOrchestrator")]
public static async Task<string> ApprovalWorkflow(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var request = context.GetInput<ApprovalRequest>()!;

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

    // Wait for approval event (with timeout)
    using var cts = new CancellationTokenSource();
    var approvalTask = context.WaitForExternalEvent<bool>("ApprovalReceived");
    var timeoutTask = context.CreateTimer(
        context.CurrentUtcDateTime.AddDays(3), cts.Token);

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

    if (winner == approvalTask)
    {
        cts.Cancel();
        return approvalTask.Result ? "Approved" : "Rejected";
    }

    return "Timed out";
}

// External event sender (called from web app)
[Function("SubmitApproval")]
public static async Task SubmitApproval(
    [HttpTrigger] HttpRequestData req,
    [DurableClient] DurableTaskClient client)
{
    var body = await req.ReadFromJsonAsync<ApprovalResponse>();
    await client.RaiseEventAsync(body!.InstanceId, "ApprovalReceived", body.Approved);
}

3. Cold Start & Performance

Cold Start Times (approximate):
┌──────────────┬────────────┬───────────┐
│    Plan      │ .NET 8     │ .NET AOT  │
├──────────────┼────────────┼───────────┤
│ Consumption  │ 1-3 sec    │ <500ms    │
│ Premium      │ <1 sec     │ <200ms    │
│ Dedicated    │ Pre-warmed │ Pre-warmed│
└──────────────┴────────────┴───────────┘

Strategies to reduce cold start:
1. Premium Plan: pre-warmed instances
2. Native AOT: minimal startup time
3. Keep-alive pings (Timer trigger every 5 min)
4. Minimize dependencies in function project
5. Use ReadyToRun compilation

Production Best Practices

csharp
// 1. host.json tuning
{
  "version": "2.0",
  "extensions": {
    "serviceBus": {
      "maxConcurrentCalls": 16,
      "prefetchCount": 100,
      "maxAutoLockRenewalDuration": "00:30:00"
    },
    "queues": {
      "maxPollingInterval": "00:00:02",
      "batchSize": 32
    }
  },
  "functionTimeout": "00:10:00",
  "logging": {
    "logLevel": {
      "default": "Warning",
      "Function": "Information"
    }
  }
}

// 2. Retry policy
[Function("ProcessOrder")]
[FixedDelayRetry(5, "00:00:30")]
public async Task ProcessOrder(
    [ServiceBusTrigger("orders")] OrderMessage message) { }

// 3. Monitor with Application Insights
builder.Services.AddApplicationInsightsTelemetryWorkerService();

4. Ví Dụ: File Processing Pipeline

Blob Upload → Blob Trigger → Validate → Process → Store Result → Notify

[Upload]        [Validate]        [Process]        [Store]        [Notify]
   │                │                 │               │               │
   │  Blob          │  Queue          │  Queue        │  Queue       │
   │  Trigger       │  Trigger        │  Trigger      │  Trigger     │
   ▼                ▼                 ▼               ▼              ▼
Upload to      Validate file    Convert/resize   Save to DB     Send email
"uploads"      format & size    image             + metadata     notification
container      → "validated"    → "processed"     
               queue            queue             

Tổng Kết Session 21

ConceptKey Takeaway
Isolated Worker.NET 8/9 recommended model, full DI support
TriggersHTTP, Timer, Service Bus, Blob, Cosmos, Event Grid
Durable FunctionsStateful orchestrations, fan-out/fan-in, human interaction
Cold StartPremium plan hoặc AOT cho latency-sensitive
RetryBuilt-in [FixedDelayRetry], [ExponentialBackoffRetry]

Bài tập: Tạo Durable Function orchestration: upload file (HTTP) → validate → process → store result → send notification. Deploy lên Azure Functions Premium Plan.