Back to Blog
7 min read

Azure Functions with .NET 6: In-Process and Isolated Worker Models

Azure Functions now fully supports .NET 6, bringing all the new language and runtime improvements to serverless development. Let’s explore how to leverage .NET 6 in both the in-process and isolated worker models.

In-Process vs Isolated Worker

With .NET 6, you have two hosting models:

In-Process: Functions run in the same process as the Functions host

  • Tighter integration with bindings
  • Better cold start performance
  • Limited to Functions-supported .NET versions

Isolated Worker: Functions run in a separate worker process

  • Full control over dependencies
  • Use any .NET version
  • Slightly higher latency

Creating an In-Process Function

# Create in-process function project
func init InProcessFunctions --worker-runtime dotnet
cd InProcessFunctions

# Add an HTTP trigger
func new --name Hello --template "HTTP trigger"

The project file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.0.1" />
  </ItemGroup>
</Project>

A basic HTTP function:

using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;

namespace InProcessFunctions;

public static class Hello
{
    [FunctionName("Hello")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");

        string name = req.Query["name"];

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        name = name ?? data?.name;

        return name != null
            ? new OkObjectResult($"Hello, {name}. Welcome to Azure Functions on .NET 6!")
            : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
    }
}

Creating an Isolated Worker Function

# Create isolated worker project
func init IsolatedFunctions --worker-runtime dotnet-isolated
cd IsolatedFunctions

# Add an HTTP trigger
func new --name Hello --template "HTTP trigger"

The project file for isolated worker:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.6.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.3.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
  </ItemGroup>
</Project>

Program.cs with dependency injection:

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Azure.Functions.Worker.Configuration;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(builder =>
    {
        // Add middleware
        builder.UseMiddleware<ExceptionHandlingMiddleware>();
    })
    .ConfigureServices(services =>
    {
        // Register services
        services.AddSingleton<IWeatherService, WeatherService>();
        services.AddHttpClient();

        // Add Application Insights
        services.AddApplicationInsightsTelemetryWorkerService();
    })
    .Build();

host.Run();

HTTP function in isolated model:

using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;

namespace IsolatedFunctions;

public class Hello
{
    private readonly ILogger _logger;
    private readonly IWeatherService _weatherService;

    public Hello(ILoggerFactory loggerFactory, IWeatherService weatherService)
    {
        _logger = loggerFactory.CreateLogger<Hello>();
        _weatherService = weatherService;
    }

    [Function("Hello")]
    public async Task<HttpResponseData> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
    {
        _logger.LogInformation("C# HTTP trigger function processed a request.");

        var queryParams = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
        string name = queryParams["name"] ?? "World";

        var weather = await _weatherService.GetCurrentWeather();

        var response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteAsJsonAsync(new
        {
            Message = $"Hello, {name}!",
            Weather = weather,
            Runtime = ".NET 6 Isolated Worker"
        });

        return response;
    }
}

public interface IWeatherService
{
    Task<WeatherInfo> GetCurrentWeather();
}

public class WeatherService : IWeatherService
{
    private readonly HttpClient _httpClient;

    public WeatherService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<WeatherInfo> GetCurrentWeather()
    {
        // Implementation
        return new WeatherInfo { Temperature = 22, Condition = "Sunny" };
    }
}

public record WeatherInfo
{
    public int Temperature { get; init; }
    public string Condition { get; init; } = "";
}

Custom Middleware

Isolated worker functions support middleware:

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
        {
            _logger.LogInformation("Function {Name} starting", context.FunctionDefinition.Name);

            await next(context);

            _logger.LogInformation("Function {Name} completed", context.FunctionDefinition.Name);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in function {Name}", context.FunctionDefinition.Name);

            // Handle HTTP responses
            var httpReqData = await context.GetHttpRequestDataAsync();
            if (httpReqData != null)
            {
                var response = httpReqData.CreateResponse(HttpStatusCode.InternalServerError);
                await response.WriteAsJsonAsync(new { error = "An error occurred" });

                var invocationResult = context.GetInvocationResult();
                invocationResult.Value = response;
            }
            else
            {
                throw;
            }
        }
    }
}

Timer Trigger with .NET 6 Features

public class ScheduledTasks
{
    private readonly ILogger _logger;
    private readonly HttpClient _httpClient;

    public ScheduledTasks(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory)
    {
        _logger = loggerFactory.CreateLogger<ScheduledTasks>();
        _httpClient = httpClientFactory.CreateClient();
    }

    [Function("DailyReport")]
    public async Task RunDailyReport(
        [TimerTrigger("0 0 8 * * *")] TimerInfo timer)
    {
        _logger.LogInformation("Daily report started at: {time}", DateTime.Now);

        // Using C# 10 file-scoped namespaces and records
        var report = await GenerateReport();

        // Using .NET 6 DateOnly
        var reportDate = DateOnly.FromDateTime(DateTime.Today);

        _logger.LogInformation("Report for {date}: {count} items processed",
            reportDate, report.ItemCount);

        if (timer.ScheduleStatus is not null)
        {
            _logger.LogInformation("Next occurrence: {next}",
                timer.ScheduleStatus.Next);
        }
    }

    private async Task<ReportData> GenerateReport()
    {
        // Generate report logic
        await Task.Delay(100);
        return new ReportData(ItemCount: 42, GeneratedAt: DateTime.UtcNow);
    }
}

public record ReportData(int ItemCount, DateTime GeneratedAt);

Queue Trigger with Output Bindings

public class QueueProcessor
{
    private readonly ILogger _logger;

    public QueueProcessor(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<QueueProcessor>();
    }

    [Function("ProcessOrder")]
    [QueueOutput("processed-orders")]
    public async Task<ProcessedOrder?> Run(
        [QueueTrigger("incoming-orders")] Order order)
    {
        _logger.LogInformation("Processing order {OrderId}", order.Id);

        // Validate order using pattern matching
        if (order is not { Id: > 0, Items.Count: > 0 })
        {
            _logger.LogWarning("Invalid order received");
            return null;
        }

        // Process the order
        var total = order.Items.Sum(item => item.Price * item.Quantity);

        var processed = new ProcessedOrder
        {
            OrderId = order.Id,
            Total = total,
            ProcessedAt = DateTime.UtcNow,
            Status = total > 1000 ? "HighValue" : "Standard"
        };

        _logger.LogInformation("Order {OrderId} processed. Total: {Total}",
            order.Id, total);

        return processed;
    }
}

public record Order
{
    public int Id { get; init; }
    public List<OrderItem> Items { get; init; } = new();
}

public record OrderItem
{
    public string ProductName { get; init; } = "";
    public decimal Price { get; init; }
    public int Quantity { get; init; }
}

public record ProcessedOrder
{
    public int OrderId { get; init; }
    public decimal Total { get; init; }
    public DateTime ProcessedAt { get; init; }
    public string Status { get; init; } = "";
}

Durable Functions on .NET 6

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

        // Fan-out/fan-in pattern
        var validationResult = await context.CallActivityAsync<bool>(
            "ValidateOrder", order);

        if (!validationResult)
        {
            return new OrderResult(false, "Validation failed");
        }

        // Process items in parallel
        var processingTasks = order.Items
            .Select(item => context.CallActivityAsync<ItemResult>("ProcessItem", item))
            .ToList();

        var results = await Task.WhenAll(processingTasks);

        // Final step
        var finalResult = await context.CallActivityAsync<OrderResult>(
            "FinalizeOrder",
            new FinalizeRequest(order.Id, results));

        return finalResult;
    }

    [Function("ValidateOrder")]
    public static bool ValidateOrder([ActivityTrigger] Order order, FunctionContext context)
    {
        var logger = context.GetLogger("ValidateOrder");
        logger.LogInformation("Validating order {OrderId}", order.Id);

        return order.Items.Count > 0 && order.Items.All(i => i.Quantity > 0);
    }

    [Function("ProcessItem")]
    public static ItemResult ProcessItem([ActivityTrigger] OrderItem item, FunctionContext context)
    {
        var logger = context.GetLogger("ProcessItem");
        logger.LogInformation("Processing item {ProductName}", item.ProductName);

        return new ItemResult(item.ProductName, item.Price * item.Quantity);
    }

    [Function("FinalizeOrder")]
    public static OrderResult FinalizeOrder(
        [ActivityTrigger] FinalizeRequest request,
        FunctionContext context)
    {
        var logger = context.GetLogger("FinalizeOrder");
        var total = request.Results.Sum(r => r.Total);
        logger.LogInformation("Order {OrderId} finalized. Total: {Total}",
            request.OrderId, total);

        return new OrderResult(true, $"Order completed. Total: {total:C}");
    }
}

public record ItemResult(string ProductName, decimal Total);
public record FinalizeRequest(int OrderId, ItemResult[] Results);
public record OrderResult(bool Success, string Message);

Local Development

Configure local settings:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=..."
  }
}

Run locally:

# Start Azurite for local storage emulation
azurite --silent --location ./azurite --debug ./azurite/debug.log &

# Start the function app
func start

.NET 6 brings modern C# features and improved performance to Azure Functions. Whether you choose in-process for tighter integration or isolated worker for maximum flexibility, you’ll benefit from the improvements in .NET 6.

Resources

Michael John Pena

Michael John Pena

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.