Develop Aspire and Azure Functions Projects together.

I'm Tomohisa Takaoka, @tomohisa, the CTO of J-Tech Creations, Inc.. I am developing Sekiban - Event Sourcing and CQRS Framework It is written in C# and it can make Cosmos DB, Dynamo DB or PostgreSQL as a event store.

.NET Aspire is a cloud-native stack being developed open-source by Microsoft.

💡
NOTE As it has not been officially released yet, this information is current as of April 5, 2024. Better methods may have been provided since then, so please check the documentation and Github project.

Aspire has the following features:

  • Running multiple web services, front-ends, etc. from AppHost.

  • The environment variable injection feature allows resolving connection information and other details at runtime

  • It also has the ability to start containers, allowing simultaneous startup of databases and tools

  • Using container and program configurations, it provides a feature to easily deploy to the cloud (currently Azure, but will support other cloud providers in the future)

In essence, it is a stack that conveniently provides one-stop service from development to deployment. Personally, I am currently focused on Aspire as a feature to streamline local development, apart from deployment. Of course, it would be very convenient if it could handle everything up to deployment, but For deployments, it is possible to set up each service separately using Github Actions. Even when developing locally with Aspire, the services only inject environment variables into each other, so deployments can be done for each service independently of Aspire.

Hosting Azure Queue Storage and Blobs

When it comes to adding Azure Queue Storage to Azure Functions, it is provided by Aspire and can be written in the Host using the following code:

var azurite = builder.AddAzureStorage("storageAzuriteMyName")
    .RunAsEmulator(
        container =>
            container.UseBlobPort(11000)
                .UseQueuePort(11001)
                .UseTablePort(11002)
                .WithVolumeMount("VolumeMount.myStorageName.Azurite", "/data"));
var blob = azurite.AddBlobs("SekibanBlob");
var queue = azurite.AddQueues("QueueStorage");

With the above code, blobs can be executed and shared with other processes.

Edited below with David Fowler 's comment. Thanks David!

However, with this way of writing, currently it is not possible to generate a Volume in the container and retain data. This makes it difficult to check the contents later when performing complex processing. Therefore, as shown in the Functions example above, you can develop in the desired way by launching a regular container and using it, instead of using the AzureStorage feature within Aspire.

// This code is directly launch azurite, but not need using above code.
var azurite = builder.AddContainer("myStorageName", "mcr.microsoft.com/azure-storage/azurite")
    .WithVolumeMount("VolumeMount.myStorageName.Azurite", "/data")
    .WithEndpoint(10000, name: "blob", hostPort: 11000)
    .WithEndpoint(10001, name: "queue", hostPort: 11001)
    .WithEndpoint(10001, name: "table", hostPort: 11002);

By doing this, you can launch Blob and Queue on any desired host port.

A connection string will be required, but since the security key for the development storage is fixed, it can be generated as shown in the following code.

Inject ConnectionString

Injecting connectionString is easy as below for WebAPI.


var apiService = builder.AddProject<YourProjectName_ApiService>("apiservice");
        .WithReference(postgres)
        .WithReference(queue)
        .WithReference(blob)
        .WithEnvironment("Sekiban__Infrastructure", "Postgres");

By using WithEnvironment like this, you can inject values into any desired environment variables. On each application side, the values of environment variables are overwritten in appsettings.json and can be used freely. This eliminates the need to write settings in appsettings.json for each application.

When we use WithReference, connection string name will be the name set when you create the AddBlobs , AddQueues Methods.

💡
when we use WithReference, environment variables will inject under ConnectionStrings__ .
var blob = azurite.AddBlobs("SekibanBlob");
var queue = azurite.AddQueues("QueueStorage");

Aspire + Azure Functions

When developing for deployment to Azure using .NET, the simple configuration is to set up a WebAPI with AppService and send delayed processing to Azure Queue Storage to be handled by Azure Functions. While it is mentioned that Aspire will support Azure Functions in the future, currently there are no features available for this.

Are Azure Functions supported in .NET Aspire?

However, the model for child processes in Aspire is simple and basically follows this pattern:

  • Execute the executable as a process (this method is used for WebAPI, frontend, etc.)

  • Start the container (Postgres, Redis, etc. are executed using this method)

Therefore, for Azure Functions projects as well, if the executable file can be run as a process, it can be easily added to Aspire.

Support for Azure Functions is being discussed in the following issue:

Here, a simple Azure Function compatible extension is introduced, and there are people who have actually used it. The repository is here:

The simple Azure Functions support functionality here can be copied to a local project to implement it.

namespace AspireStarter.AppHost;

public static class Extensions
{
    public static IResourceBuilder<ExecutableResource> AddAzureFunction<TServiceMetadata>(
        this IDistributedApplicationBuilder builder,
        string name,
        int port,
        int debugPort)
        where TServiceMetadata : IProjectMetadata, new()
    {
        var serviceMetadata = new TServiceMetadata();
        var projectPath = serviceMetadata.ProjectPath;
        var projectDirectory = Path.GetDirectoryName(projectPath)!;

        var args = new[]
        {
            "host",
            "start",
            "--port",
            port.ToString(),
            "--nodeDebugPort",
            debugPort.ToString()
        };

        return builder.AddResource(new ExecutableResource(name, "func", projectDirectory, args))
            .WithOtlpExporter();
    }
}

It can be executed as follows:

builder.AddAzureFunction<YourProjectName_Common_Functions>("functions", 7121, 7122)
    .WithReference(postgres)
    .WithReference(blob)
    .WithEnvironment(
        "AzureWebJobsStorage",
        () => queue.Resource.GetConnectionString() ??
            throw new InvalidOperationException("Connection string is null"));

When you want to inject AzureWebJobsStorage, it will not be under ConnectionStrings__, thus need to use WithEnvironment. Also Value can not be obtain with GetConnectionString method before builder.Build().Run(); That is why WithEnvironment will have callback method that will be calling while being building process.

Debugging Function Process.

Debugging was possible from Rider on Mac by performing debugging of the executed process with attach to process. Also, appService can be debug with same way. When you select process, Rider has convenient tree view that shows process by its calling tree. I can select under dcp process, to easily find aspire processes.

Summary

Basically, it can be executed by imitating the above repository. The structure of Aspire is basically simple and provides a mechanism to simultaneously execute various things locally. In the application we are working on, there are requirements such as authentication by Azure Entra Id being performed in external services, so it looks like the following image.

With the above configuration, we can perform batch execution in Functions, and carry out local development, allowing both frontend developers and backend developers to run environment setup easily and develop each components.

Although it has not been officially released yet, Aspire can be used for local execution even before the official release, and without using the deployment function, so we consider it to be already usable.

We would also like to introduce any convenient features if there are any.

10
Subscribe to my newsletter

Read articles from Tomohisa Takaoka directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tomohisa Takaoka
Tomohisa Takaoka

CTO of J-Tech Creations, Inc. Recently working on the development of the event sourcing and CQRS framework Sekiban. Enthusiast of DIY keyboards and trackballs.