Azure Functions with .NET 7: Isolated Worker Process Model
With .NET 7’s release, Azure Functions running on the isolated worker process model gets even better. The isolated model is now the recommended approach for new .NET Azure Functions projects. Let’s explore what this means and how to get started.
Why Isolated Process?
The isolated worker process model separates your function code from the Azure Functions host:
- Version independence: Use any .NET version, including .NET 7
- Full control over dependencies: No conflicts with host dependencies
- Middleware support: Add custom middleware to your function pipeline
- Native AOT support: Potential for faster cold starts
Getting Started
Create a new isolated Azure Functions project:
dotnet new func --name MyFunctionApp --worker-runtime dotnet-isolated --target-framework net7.0
cd MyFunctionApp
The project file should look like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.10.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.7.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
</ItemGroup>
</Project>
Program.cs Configuration
The isolated model uses a standard .NET host:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults(worker =>
{
worker.UseMiddleware<ExceptionHandlingMiddleware>();
worker.UseMiddleware<AuthenticationMiddleware>();
})
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
// Add your services
services.AddSingleton<ICosmosDbService, CosmosDbService>();
services.AddHttpClient<IExternalApiClient, ExternalApiClient>();
})
.Build();
host.Run();
HTTP Trigger Function
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
public class HttpFunctions
{
private readonly ILogger<HttpFunctions> _logger;
private readonly ICosmosDbService _cosmosDb;
public HttpFunctions(ILogger<HttpFunctions> logger, ICosmosDbService cosmosDb)
{
_logger = logger;
_cosmosDb = cosmosDb;
}
[Function("GetProducts")]
public async Task<HttpResponseData> GetProducts(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "products")]
HttpRequestData req)
{
_logger.LogInformation("Getting all products");
var products = await _cosmosDb.GetProductsAsync();
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,
string id)
{
_logger.LogInformation("Getting product {ProductId}", id);
var product = await _cosmosDb.GetProductAsync(id);
if (product is null)
{
return req.CreateResponse(HttpStatusCode.NotFound);
}
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(product);
return response;
}
[Function("CreateProduct")]
public async Task<HttpResponseData> CreateProduct(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "products")]
HttpRequestData req)
{
var product = await req.ReadFromJsonAsync<Product>();
if (product is null)
{
var badRequest = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequest.WriteAsJsonAsync(new { error = "Invalid product data" });
return badRequest;
}
var created = await _cosmosDb.CreateProductAsync(product);
var response = req.CreateResponse(HttpStatusCode.Created);
await response.WriteAsJsonAsync(created);
return response;
}
}
public record Product(string Id, string Name, decimal Price, string Category);
Custom Middleware
One of the best features of isolated functions - middleware:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.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
{
await next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception in function {FunctionName}",
context.FunctionDefinition.Name);
var httpContext = await context.GetHttpRequestDataAsync();
if (httpContext is not null)
{
var response = httpContext.CreateResponse(HttpStatusCode.InternalServerError);
await response.WriteAsJsonAsync(new
{
error = "An internal error occurred",
traceId = context.InvocationId
});
context.GetInvocationResult().Value = response;
}
}
}
}
public class AuthenticationMiddleware : IFunctionsWorkerMiddleware
{
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
var httpContext = await context.GetHttpRequestDataAsync();
if (httpContext is not null)
{
// Extract and validate JWT token
if (httpContext.Headers.TryGetValues("Authorization", out var values))
{
var token = values.FirstOrDefault()?.Replace("Bearer ", "");
// Validate token and set user context
context.Items["UserId"] = ValidateToken(token);
}
}
await next(context);
}
private string? ValidateToken(string? token)
{
// Token validation logic
return token is not null ? "user-123" : null;
}
}
Queue Trigger with Output Binding
public class QueueFunctions
{
private readonly ILogger<QueueFunctions> _logger;
public QueueFunctions(ILogger<QueueFunctions> logger)
{
_logger = logger;
}
[Function("ProcessOrder")]
[BlobOutput("processed-orders/{rand-guid}.json")]
public async Task<string> ProcessOrder(
[QueueTrigger("orders")] OrderMessage order)
{
_logger.LogInformation("Processing order {OrderId}", order.OrderId);
// Process the order
var result = new ProcessedOrder
{
OrderId = order.OrderId,
ProcessedAt = DateTime.UtcNow,
Status = "Completed",
Total = order.Items.Sum(i => i.Price * i.Quantity)
};
// Return value goes to blob output
return JsonSerializer.Serialize(result);
}
}
public record OrderMessage(string OrderId, List<OrderItem> Items);
public record OrderItem(string ProductId, int Quantity, decimal Price);
public record ProcessedOrder
{
public string OrderId { get; init; } = "";
public DateTime ProcessedAt { get; init; }
public string Status { get; init; } = "";
public decimal Total { get; init; }
}
Timer Trigger with Cosmos DB
public class TimerFunctions
{
private readonly ICosmosDbService _cosmosDb;
private readonly ILogger<TimerFunctions> _logger;
public TimerFunctions(ICosmosDbService cosmosDb, ILogger<TimerFunctions> logger)
{
_cosmosDb = cosmosDb;
_logger = logger;
}
[Function("DailyReport")]
public async Task DailyReport(
[TimerTrigger("0 0 8 * * *")] TimerInfo timer)
{
_logger.LogInformation("Generating daily report at {Time}", DateTime.UtcNow);
var yesterday = DateTime.UtcNow.Date.AddDays(-1);
var orders = await _cosmosDb.GetOrdersByDateAsync(yesterday);
var report = new DailyReport
{
Date = yesterday,
TotalOrders = orders.Count,
TotalRevenue = orders.Sum(o => o.Total),
AverageOrderValue = orders.Count > 0 ? orders.Average(o => o.Total) : 0
};
await _cosmosDb.SaveReportAsync(report);
_logger.LogInformation("Daily report saved: {TotalOrders} orders, {Revenue:C} revenue",
report.TotalOrders, report.TotalRevenue);
}
}
Dependency Injection Best Practices
// Services registration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
// Configuration
services.AddOptions<CosmosDbOptions>()
.Configure<IConfiguration>((options, config) =>
{
config.GetSection("CosmosDb").Bind(options);
});
// Services with different lifetimes
services.AddSingleton<ICosmosDbService, CosmosDbService>();
services.AddScoped<IOrderService, OrderService>();
services.AddTransient<IEmailService, EmailService>();
// HttpClient with Polly resilience
services.AddHttpClient<IPaymentService, PaymentService>()
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(300)));
return services;
}
}
// Program.cs
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
services.AddApplicationServices();
})
.Build();
Testing Isolated Functions
using Microsoft.Azure.Functions.Worker.Http;
using Moq;
using Xunit;
public class HttpFunctionsTests
{
[Fact]
public async Task GetProduct_ReturnsProduct_WhenExists()
{
// Arrange
var mockLogger = new Mock<ILogger<HttpFunctions>>();
var mockCosmosDb = new Mock<ICosmosDbService>();
var expectedProduct = new Product("1", "Test Product", 19.99m, "Test");
mockCosmosDb.Setup(x => x.GetProductAsync("1"))
.ReturnsAsync(expectedProduct);
var function = new HttpFunctions(mockLogger.Object, mockCosmosDb.Object);
var request = CreateMockRequest();
// Act
var response = await function.GetProduct(request, "1");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
private HttpRequestData CreateMockRequest()
{
var context = new Mock<FunctionContext>();
var request = new Mock<HttpRequestData>(context.Object);
// Setup request mock...
return request.Object;
}
}
Conclusion
The isolated worker process model is the future of .NET Azure Functions. With .NET 7 support, middleware capabilities, and full dependency injection, it provides a modern, testable, and flexible platform for building serverless applications. If you’re starting a new Azure Functions project, the isolated model should be your default choice.