Azure Functions Isolated Worker Model: The Modern Approach
The in-process hosting model for Azure Functions is on its way out. Microsoft has made it clear: the isolated worker model is the future. If you're still running in-process functions, now is the time to migrate. If you're starting fresh, there's no reason to look back.
The isolated worker model runs your function code in a separate .NET process from the Azure Functions host. This gives you full control over your application's startup, dependency injection, middleware pipeline, and — critically — which version of .NET you target.
Setting Up a New Project
Start with the func CLI or a standard console application. The entry point is a Program.cs file, just like any other modern .NET application:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
services.AddSingleton<IOrderService, OrderService>();
})
.Build();
host.Run();
Notice ConfigureFunctionsWebApplication() — this enables ASP.NET Core integration, giving you access to HttpRequest and HttpResponse types you already know. If you don't need HTTP triggers, use ConfigureFunctionsWorkerDefaults() instead.
Writing Functions
Functions themselves are plain classes with constructor injection:
public class OrderFunctions
{
private readonly IOrderService _orderService;
private readonly ILogger<OrderFunctions> _logger;
public OrderFunctions(IOrderService orderService, ILogger<OrderFunctions> logger)
{
_orderService = orderService;
_logger = logger;
}
[Function("ProcessOrder")]
public async Task<IActionResult> ProcessOrder(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "orders")] HttpRequest req)
{
var order = await req.ReadFromJsonAsync<OrderRequest>();
if (order is null)
return new BadRequestObjectResult("Invalid order payload.");
var result = await _orderService.ProcessAsync(order);
_logger.LogInformation("Order {OrderId} processed successfully", result.Id);
return new OkObjectResult(result);
}
}
With ASP.NET Core integration, you return IActionResult and read from HttpRequest — exactly as you would in a controller or minimal API.
Middleware
One of the most powerful features of the isolated model is the middleware pipeline. You can intercept function invocations for cross-cutting concerns:
var host = new HostBuilder()
.ConfigureFunctionsWebApplication(worker =>
{
worker.UseMiddleware<CorrelationIdMiddleware>();
worker.UseMiddleware<ExceptionHandlingMiddleware>();
})
.Build();
A simple exception-handling middleware looks like this:
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 {FunctionName}", context.FunctionDefinition.Name);
throw;
}
}
}
This mirrors ASP.NET Core middleware patterns. If you've built middleware for a web API, you already know how this works.
Timer and Queue Triggers
HTTP isn't the only game in town. Timer-triggered functions for scheduled work:
[Function("DailyCleanup")]
public async Task RunCleanup(
[TimerTrigger("0 0 2 * * *")] TimerInfo timer)
{
_logger.LogInformation("Cleanup started at {Time}", DateTime.UtcNow);
await _orderService.CleanupStaleOrdersAsync();
if (timer.ScheduleStatus?.Next is not null)
_logger.LogInformation("Next cleanup at {Next}", timer.ScheduleStatus.Next);
}
Queue-triggered functions for message processing:
[Function("ProcessMessage")]
public async Task ProcessMessage(
[ServiceBusTrigger("orders", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
{
var order = message.Body.ToObjectFromJson<OrderRequest>();
await _orderService.ProcessAsync(order);
}
Key Differences from In-Process
There are a few things to be aware of when migrating:
- No direct binding to SDK types — some bindings require you to work with raw data and deserialise manually, though this is improving with each release.
- Separate dependency graph — your function app has its own
IServiceCollection, independent of the host. No more version conflicts with the host's dependencies. - Logging — use
ILogger<T>as usual, but be aware that Application Insights integration requires explicit configuration viaConfigureFunctionsApplicationInsights().
When to Use Functions
Azure Functions shine for event-driven workloads: processing queue messages, reacting to blob uploads, running scheduled tasks, and handling lightweight HTTP endpoints. They are not a replacement for a full web API — if you need complex routing, authentication pipelines, or WebSocket support, reach for App Service or Container Apps.
The isolated worker model removes the last major friction point. You get the serverless scaling model with the full power of modern .NET. There's no longer a compelling reason to avoid it.