Tutorial: Connect an ASP.NET Core app to .NET Aspire storage integrations

Cloud-native apps often require scalable storage solutions that provide capabilities like blob storage, queues, or semi-structured NoSQL databases. .NET Aspire integrations simplify connections to various storage services, such as Azure Blob Storage. In this tutorial, you'll create an ASP.NET Core app that uses .NET Aspire integrations to connect to Azure Blob Storage and Azure Queue Storage to submit support tickets. The app sends the tickets to a queue for processing and uploads an attachment to storage. You'll learn how to:

  • Create a basic .NET app that is set up to use .NET Aspire integrations
  • Add .NET Aspire integrations to connect to multiple storage services
  • Configure and use .NET Aspire Component features to send and receive data

Prerequisites

To work with .NET Aspire, you need the following installed locally:

For more information, see .NET Aspire setup and tooling.

Explore the completed sample app

A completed version of the sample app from this tutorial is available on GitHub. The project is also structured as a template for the Azure Developer CLI, meaning you can use the azd up command to automate Azure resource provisioning if you have the tool installed.

git clone https://github.com/Azure-Samples/dotnet-aspire-connect-storage.git

Set up the Azure Storage resources

For this article, you'll need data contributor access to an Azure Storage account with a blob container and storage queue. Ensure you have the following resources and configurations available:

For this article, you'll need to create a blob container and storage queue resource in your local development environment using an emulator. To do so, use Azurite. Azurite is a free, open source, cross-platform Azure Storage API compatible server (emulator) that runs in a Docker container.

To use the emulator you need to install Azurite.

  1. An Azure Storage account - Create a storage account.
  2. A Blob Storage container named fileuploads - Create a blob storage container.
  3. A Storage Queue named tickets - Create a storage queue.

Run the following commands in the Azure CLI or CloudShell to set up the required Azure Storage resources:

az group create --name aspirestorage --location eastus2
az storage account create -n aspirestorage -g aspirestorage -l eastus2
az storage container create -n fileuploads --account-name aspirestorage
az storage queue create -n tickets --account-name aspirestorage

You also need to assign the following roles to the user account you are logged into Visual Studio with:

The Azure Developer CLI enables you to provision and deploy Azure resources using a template system. This tutorial provides a complete template that provisions the required Azure resources and includes the completed sample application code. Run the following commands to initialize and run the template:

  1. Run azd auth login to sign-in to Azure:

    azd auth login
    
  2. Run azd init to clone and initialize the sample template:

    azd init --template dotnet-aspire-connect-storage
    
  3. Run azd up to provision the Azure resources:

    azd up
    
  4. When prompted, select the subscription and Azure region for the provisioned resources. The template runs and completes the following tasks for you:

    • Creates an Azure Storage Account with blob and queue services enabled
    • Creates a blob storage container named fileUploads
    • Creates a queue named tickets
    • Assigns the following roles to the user account that ran the template.
      • Storage Blob Data Contributor
      • Storage Queue Data Contributor

After the operation completes successfully, you have two options moving forward:

  • Option 1: Run the .NET sample app in the template src directory to experiment with the completed app.
  • Option 2: Build the sample app step by step using the sections ahead and connect it to the Azure resources provisioned by azd.

Create the sample solution

Create a .NET Aspire project using either Visual Studio or the .NET CLI.

  1. At the top of Visual Studio, navigate to File > New > Project.
  2. In the dialog window, search for Aspire and select .NET Aspire Starter Application. Choose Next.
  3. On the Configure your new project screen:
    • Enter a Solution Name of AspireStorage and select Next.
  4. On the Additional information screen:
    • Uncheck Use Redis for caching (not required for this tutorial).
    • Select Create.

Visual Studio creates a new ASP.NET Core solution that is structured to use .NET Aspire.

The solution consists of the following projects:

  • AspireStorage.ApiService - An API project with default .NET Aspire service configurations.
  • AspireStorage.AppHost - An orchestrator project designed to connect and configure the different projects and services of your app. The orchestrator should be set as the startup project.
  • AspireStorage.ServiceDefaults - A shared class library to hold code that can be reused across the projects in your solution.
  • AspireStorage.Web - A Blazor Server project that serves as the front end of your app.

Add the Worker Service project

Next, add a Worker Service project to the solution to retrieve and process messages as they are added to the Azure Storage queue.

  1. In the solution explorer, right click on the top level AspireStorage solution node and select Add > New project.
  2. Search for and select the Worker Service template and choose Next.
  3. For the Project name, enter AspireStorage.WorkerService and select Next.
  4. On the Additional information screen:
    • Make sure .NET 8.0 is selected.
    • Make sure Enlist in .NET Aspire orchestration is checked and select Create.

Visual Studio adds the project to your solution and updates the Program.cs file of the AspireStorage.AppHost project with a new line of code:

builder.AddProject<Projects.AspireStorage_WorkerService>(
    "aspirestorage-workerservice");

Visual Studio tooling added this line of code to register your new project with the IDistributedApplicationBuilder object, which enables orchestration features. For more information, see .NET Aspire orchestration overview.

The completed solution structure should resemble the following:

A screenshot showing the structure of the .NET Aspire storage sample solution.

Add the .NET Aspire integrations to the Blazor app

Add the .NET Aspire Azure Blob Storage integration and .NET Aspire Azure Queue Storage integration packages to your AspireStorage.Web project:

dotnet add package Aspire.Azure.Storage.Blobs
dotnet add package Aspire.Azure.Storage.Queues

Your AspireStorage.Web project is now set up to use .NET Aspire integrations. Here's the updated AspireStorage.Web.csproj file:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\AspireStorage.ServiceDefaults\AspireStorage.ServiceDefaults.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Aspire.Azure.Storage.Blobs" Version="8.2.2" />
    <PackageReference Include="Aspire.Azure.Storage.Queues" Version="8.2.2" />
  </ItemGroup>

</Project>

The next step is to add the integrations to the app.

In the Program.cs file of the AspireStorage.Web project, add calls to the AddAzureBlobClient and AddAzureQueueClient extension methods after the creation of the builder but before the call to AddServiceDefaults. For more information, see .NET Aspire service defaults. Provide the name of your connection string as a parameter.

using AspireStorage.Web;
using AspireStorage.Web.Components;

using Azure.Storage.Blobs;
using Azure.Storage.Queues;

var builder = WebApplication.CreateBuilder(args);

builder.AddAzureBlobClient("BlobConnection");
builder.AddAzureQueueClient("QueueConnection");

// Add service defaults & Aspire components.
builder.AddServiceDefaults();

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddOutputCache();

builder.Services.AddHttpClient<WeatherApiClient>(client =>
    {
        // This URL uses "https+http://" to indicate HTTPS is preferred over HTTP.
        // Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes.
        client.BaseAddress = new("https+http://apiservice");
    });

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}
else
{
    // In development, create the blob container and queue if they don't exist.
    var blobService = app.Services.GetRequiredService<BlobServiceClient>();
    var docsContainer = blobService.GetBlobContainerClient("fileuploads");

    await docsContainer.CreateIfNotExistsAsync();

    var queueService = app.Services.GetRequiredService<QueueServiceClient>();
    var queueClient = queueService.GetQueueClient("tickets");

    await queueClient.CreateIfNotExistsAsync();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

app.UseOutputCache();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.MapDefaultEndpoints();

app.Run();
using AspireStorage.Web;
using AspireStorage.Web.Components;

using Azure.Storage.Blobs;
using Azure.Storage.Queues;

var builder = WebApplication.CreateBuilder(args);

builder.AddAzureBlobClient("BlobConnection");
builder.AddAzureQueueClient("QueueConnection");

// Add service defaults & Aspire components.
builder.AddServiceDefaults();

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddOutputCache();

builder.Services.AddHttpClient<WeatherApiClient>(client =>
    {
        // This URL uses "https+http://" to indicate HTTPS is preferred over HTTP.
        // Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes.
        client.BaseAddress = new("https+http://apiservice");
    });

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

app.UseOutputCache();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.MapDefaultEndpoints();

app.Run();

With the additional using statements, these methods accomplish the following tasks:

When the AspireStorage.Web project starts, it will create a fileuploads container in Azurite Blob Storage and a tickets queue in Azurite Queue Storage. This is conditional when the app is running in a development environment. When the app is running in a production environment, the container and queue are assumed to have already been created.

Add the .NET Aspire integration to the Worker Service

The worker service handles pulling messages off of the Azure Storage queue for processing. Add the .NET Aspire Azure Queue Storage integration integration package to your AspireStorage.WorkerService app:

dotnet add package Aspire.Azure.Storage.Queues

In the Program.cs file of the AspireStorage.WorkerService project, add a call to the AddAzureQueueClient extension method after the creation of the builder but before the call to AddServiceDefaults:

using AspireStorage.WorkerService;

var builder = Host.CreateApplicationBuilder(args);

builder.AddAzureQueueClient("QueueConnection");

builder.AddServiceDefaults();
builder.Services.AddHostedService<WorkerService>();

var host = builder.Build();
host.Run();

This method handles the following tasks:

  • Register a QueueServiceClient with the DI container for connecting to Azure Storage Queues.
  • Automatically enable corresponding health checks, logging, and telemetry for the respective services.

Create the form

The app requires a form for the user to be able to submit support ticket information and upload an attachment. The app uploads the attached file on the Document (IFormFile) property to Azure Blob Storage using the injected BlobServiceClient. The QueueServiceClient sends a message composed of the Title and Description to the Azure Storage Queue.

Use the following Razor markup to create a basic form, replacing the contents of the Home.razor file in the AspireStorage.Web/Components/Pages directory:

@page "/"

@using System.ComponentModel.DataAnnotations
@using Azure.Storage.Blobs
@using Azure.Storage.Queues

@inject BlobServiceClient BlobClient
@inject QueueServiceClient QueueServiceClient

<PageTitle>Home</PageTitle>

<div class="text-center">
    <h1 class="display-4">Request Support</h1>
</div>

<EditForm Model="@Ticket" FormName="Tickets" method="post"
          OnValidSubmit="@HandleValidSubmit" enctype="multipart/form-data">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="mb-4">
        <label>Issue Title</label>
        <InputText class="form-control" @bind-Value="@Ticket.Title" />
        <ValidationMessage For="() => Ticket.Title" />
    </div>
    <div class="mb-4">
        <label>Issue Description</label>
        <InputText class="form-control" @bind-Value="@Ticket.Description" />
        <ValidationMessage For="() => Ticket.Description" />
    </div>
    <div class="mb-4">
        <label>Attachment</label>
        <InputFile class="form-control" name="Ticket.Document" />
        <ValidationMessage For="() => Ticket.Document" />
    </div>
    <button class="btn btn-primary" type="submit">Submit</button>
    <button class="btn btn-danger mx-2" type="reset" @onclick=@ClearForm>Clear</button>
</EditForm>

@code {
    [SupplyParameterFromForm(FormName = "Tickets")]
    private SupportTicket Ticket { get; set; } = new();

    private async Task HandleValidSubmit()
    {
        var docsContainer = BlobClient.GetBlobContainerClient("fileuploads");

        // Upload file to blob storage
        await docsContainer.UploadBlobAsync(
            Ticket.Document.FileName,
            Ticket.Document.OpenReadStream());

        // Send message to queue
        var queueClient = QueueServiceClient.GetQueueClient("tickets");

        await queueClient.SendMessageAsync(
             $"{Ticket.Title} - {Ticket.Description}");

        ClearForm();
    }

    private void ClearForm() => Ticket = new();

    private class SupportTicket()
    {
        [Required] public string Title { get; set; } = default!;
        [Required] public string Description { get; set; } = default!;
        [Required] public IFormFile Document { get; set; } = default!;
    }
}

For more information about creating forms in Blazor, see ASP.NET Core Blazor forms overview.

Update the AppHost

The AspireStorage.AppHost project is the orchestrator for your app. It's responsible for connecting and configuring the different projects and services of your app. The orchestrator should be set as the startup project.

To add Azure Storage hosting support to your IDistributedApplicationBuilder, install the Aspire.Hosting.Azure.Storage NuGet package.

dotnet add package Aspire.Hosting.Azure.Storage

Replace the contents of the Program.cs file in the AspireStorage.AppHost project with the following code:

using Microsoft.Extensions.Hosting;

var builder = DistributedApplication.CreateBuilder(args);

var storage = builder.AddAzureStorage("Storage");

if (builder.Environment.IsDevelopment())
{
    storage.RunAsEmulator();
}

var blobs = storage.AddBlobs("BlobConnection");
var queues = storage.AddQueues("QueueConnection");

var apiService = builder.AddProject<Projects.AspireStorage_ApiService>("apiservice");

builder.AddProject<Projects.AspireStorage_Web>("webfrontend")
    .WithExternalHttpEndpoints()
    .WithReference(apiService)
    .WithReference(blobs)
    .WithReference(queues); 

builder.AddProject<Projects.AspireStorage_WorkerService>("aspirestorage-workerservice")
    .WithReference(queues);

builder.Build().Run();

The preceding code adds Azure storage, blobs, and queues, and when in development mode, it uses the emulator. Each project defines references for these resources that they depend on.

using Microsoft.Extensions.Hosting;

var builder = DistributedApplication.CreateBuilder(args);

var storage = builder.AddAzureStorage("Storage");

var blobs = storage.AddBlobs("BlobConnection");
var queues = storage.AddQueues("QueueConnection");

var apiService = builder.AddProject<Projects.AspireStorage_ApiService>("apiservice");

builder.AddProject<Projects.AspireStorage_Web>("webfrontend")
    .WithExternalHttpEndpoints()
    .WithReference(apiService)
    .WithReference(blobs)
    .WithReference(queues); 

builder.AddProject<Projects.AspireStorage_WorkerService>("aspirestorage-workerservice")
    .WithReference(queues);

builder.Build().Run();

The preceding code adds Azure storage, blobs, and queues, and defines references for these resources within each project that depend on them.

Process the items in the queue

When a new message is placed on the tickets queue, the worker service should retrieve, process, and delete the message. Update the Worker.cs class, replacing the contents with the following code:

using Azure.Storage.Queues;
using Azure.Storage.Queues.Models;

namespace AspireStorage.WorkerService;

public sealed class WorkerService(
    QueueServiceClient client,
    ILogger<WorkerService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var queueClient = client.GetQueueClient("tickets");
        await queueClient.CreateIfNotExistsAsync(cancellationToken: stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            QueueMessage[] messages =
                await queueClient.ReceiveMessagesAsync(
                    maxMessages: 25, cancellationToken: stoppingToken);

            foreach (var message in messages)
            {
                logger.LogInformation(
                    "Message from queue: {Message}", message.MessageText);

                await queueClient.DeleteMessageAsync(
                    message.MessageId,
                    message.PopReceipt,
                    cancellationToken: stoppingToken);
            }

            // TODO: Determine an appropriate time to wait 
            // before checking for more messages.
            await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);
        }
    }
}

Before the worker service can process messages, it needs to be able to connect to the Azure Storage queue. With Azurite, you need to ensure that the queue is available before the worker service starts executing message queue processing.

using Azure.Storage.Queues;
using Azure.Storage.Queues.Models;

namespace AspireStorage.WorkerService;

public sealed class WorkerService(
    QueueServiceClient client,
    ILogger<WorkerService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var queueClient = client.GetQueueClient("tickets");
        while (!stoppingToken.IsCancellationRequested)
        {
            QueueMessage[] messages =
                await queueClient.ReceiveMessagesAsync(
                    maxMessages: 25, cancellationToken: stoppingToken);

            foreach (var message in messages)
            {
                logger.LogInformation(
                    "Message from queue: {Message}", message.MessageText);

                await queueClient.DeleteMessageAsync(
                    message.MessageId,
                    message.PopReceipt,
                    cancellationToken: stoppingToken);
            }

            // TODO: Determine an appropriate time to wait 
            // before checking for more messages.
            await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);
        }
    }
}

The worker service processes messages by connecting to the Azure Storage queue, and pulling messages off the queue.

The worker service processes message in the queue and deletes them when they've been processed.

Configure the connection strings

The AspireStorage and AspireStorage.Worker projects must be configured to connect to the correct Azure Storage Account you created earlier. You can specify the endpoints for the blob and queue services in the storage account using the appsettings.json file in each project.

  1. In the AspireStorage project, add the following configuration to the appsettings.Development.json file:

      "ConnectionStrings": {
        "BlobConnection": "https://<your-storage-account-name>.blob.core.windows.net/",
        "QueueConnection": "https://<your-storage-account-name>.queue.core.windows.net/"
      }
    
  2. In the AspireStorage.Worker project, add the following configuration to the appsettings.Development.json file:

      "ConnectionStrings": {
        "QueueConnection": "https://<your-storage-account-name>.queue.core.windows.net/"
      }
    

Run and test the app locally

The sample app is now ready for testing. Verify that the submitted form data is sent to Azure Blob Storage and Azure Queue Storage by completing the following steps:

  1. Press the run button at the top of Visual Studio to launch your .NET Aspire project dashboard in the browser.

  2. On the resources page, in the aspirestorage.web row, click the link in the Endpoints column to open the UI of your app.

    A screenshot showing the home page of the .NET Aspire support application.

  3. Enter sample data into the Title and Description form fields and select a simple file to upload.

  4. Select the Submit button, and the form submits the support ticket for processing — and clears the form.

  5. In a separate browser tab, use the Azure portal to navigate to the Storage browser in your Azure Storage Account.

  6. Select Containers and then navigate into the Documents container to see the uploaded file.

  7. You can verify the message on the queue was processed by looking at the Project logs of the .NET Aspire dashboard, and selecting the aspirestorage.workerservice from the dropdown.

    A screenshot showing the console output of the Worker app.

Summary

The example app that you built demonstrates persisting blobs from an ASP.NET Core Blazor Web App and processing queues in a .NET Worker Service. Your app connects to Azure Storage using .NET Aspire integrations. The app sends the support tickets to a queue for processing and uploads an attachment to storage.

Since you choose to use Azurite, there's no need to clean up these resources when you're done testing them, as you created them locally in the context of an emulator. The emulator enabled you to test your app locally without incurring any costs, as no Azure resources were provisioned or created.

Clean up resources

Run the following Azure CLI command to delete the resource group when you no longer need the Azure resources you created. Deleting the resource group also deletes the resources contained inside of it.

az group delete --name <your-resource-group-name>

For more information, see Clean up resources in Azure.