Running Azure Functions locally with .NET Aspire + Azurite

You can orchestrate an Azure Function right inside .NET Aspire, complete with a local Azurite storage emulator and a single place (the Aspire dashboard) to observe everything. This post walks through a minimal setup using the isolated worker model on .NET 9.

What we’ll build

  • A simple HTTP-triggered Azure Function (/api/Hello)
  • An Azurite container for Storage (Blob/Queue/Table)
  • An Aspire AppHost that:
    • starts Azurite,
    • wires the Function’s AzureWebJobsStorage,
    • exposes the Function on port 7071

Create the project

# Templates
dotnet new install Microsoft.Azure.Functions.Worker.ProjectTemplates
dotnet new func -n AzureFunction -F net9.0

dotnet new install Microsoft.Azure.Functions.Worker.ItemTemplates
dotnet new http -n Hello -A Anonymous -p:n AzureFunction

# Aspire orchestration
dotnet new aspire-apphost -n AppHost
dotnet new aspire-servicedefaults -n ServiceDefaults

# (macOS) Core Tools for local runs
brew tap azure/functions
brew install azure-functions-core-tools@4
echo 'export PATH="/opt/homebrew/opt/azure-functions-core-tools@4/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

func --version

Folder layout

09_AzureFunction/
├─ AppHost/
├─ ServiceDefaults/
├─ AzureFunction.csproj
├─ Program.cs
├─ Hello.cs
├─ host.json
├─ local.settings.json
└─ README.md

Azure Function (isolated worker)

Hello.cs

using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace AzureFunction;

public class Hello
{
    private readonly ILogger<Hello> _logger;

    public Hello(ILogger<Hello> logger) => _logger = logger;

    [Function("Hello")]
    public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
    {
        _logger.LogInformation("C# HTTP trigger function processed a request.");
        return new OkObjectResult("Welcome to Azure Functions!");
    }
}

local.settings.json (used by Core Tools; Aspire will override the storage connection)

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

Aspire orchestration (AppHost)

AppHost/Program.cs

using Aspire.Hosting;

var builder = DistributedApplication.CreateBuilder(args);

// Azurite (Storage emulator)
var azurite = builder.AddContainer("azurite", "mcr.microsoft.com/azure-storage/azurite", "3.31.0")
    .WithEndpoint(10000, 10000, "blob")
    .WithEndpoint(10001, 10001, "queue")
    .WithEndpoint(10002, 10002, "table");

// Build a Storage connection string from the allocated endpoints
string BuildAzuriteConn() =>
    "DefaultEndpointsProtocol=http;" +
    "AccountName=devstoreaccount1;" +
    "AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;" +
    $"BlobEndpoint={azurite.GetEndpoint(\"blob\").Url}devstoreaccount1;" +
    $"QueueEndpoint={azurite.GetEndpoint(\"queue\").Url}devstoreaccount1;" +
    $"TableEndpoint={azurite.GetEndpoint(\"table\").Url}devstoreaccount1;";

builder.AddProject<Projects.AzureFunction>("azure-func")
    .WithHttpEndpoint(port: 7071, name: "http")
    .WithEnvironment("AzureWebJobsStorage", BuildAzuriteConn) // computed at runtime
    .WithEnvironment("FUNCTIONS_WORKER_RUNTIME", "dotnet-isolated")
    .WaitFor(azurite);

builder. Build().Run();

What’s happening

  • Azurite runs as a container with three endpoints (Blob/Queue/Table).
  • The connection string is computed at runtime (after ports are known) and injected into the Function via env var.
  • The Function is exposed on http://localhost:7071; Aspire dashboard will show health/logs.

Run it

From the solution root:

dotnet run --project AppHost

Then open:

  • Function: http://localhost:7071/api/Hello
  • (If Core Tools UI shows a different port, Aspire’s mapping still forwards 7071 to the function.)

You should see:

Welcome to Azure Functions!

Notes & tips

  • Core Tools are required to run the Function locally; Aspire handles orchestration, not the host runtime itself.
  • If 7071 is busy, change .WithHttpEndpoint(port: …) in AppHost.
  • The Azurite key here is the standard dev key—safe for local only.
  • You can add Bindings (Queues/Blobs) and they’ll use the same AzureWebJobsStorage provided by Aspire.

Troubleshooting

  • “Host not running” / 500s
    Confirm Core Tools v4 is on PATH (func --version), and your Function targets .NET 9 isolated.
  • Storage connection errors
    Ensure Azurite ports (10000–10002) are free. On macOS, lsof -i :10000.
  • CORS in frontends
    Add "extensions": { "http": { "cors": { "allowedOrigins": [ "https://localhost:xxxx" ] } } } to host.json for local FE calls.

Next steps

  • Add a QueueTrigger or BlobTrigger function—no code changes in AppHost required.
  • Wire a Web or API project that calls the Function, all inside Aspire.
  • Swap Azurite for a real Azure Storage connection via Aspire user secrets or environment overrides.

That’s it—Azure Functions running locally with Azurite, orchestrated by .NET Aspire for a clean, one-command dev loop.

[Source code]

That’s all folks!

Cheers!
Gašper Rupnik

{End.}

Leave a comment

Website Powered by WordPress.com.

Up ↑