Integrating .NET Aspire With Azure Storage
Today, we will talk about integrating our Aspire apps with Azure Storage. We will start by looking at Azure Table Storage and then move on to Blob Storage.
A sample of a complete Azure Table Storage application can be found via the following link:
If you are new to Azure Storage, here is a brief description of what it is.
Azure has a concept of a Storage Account. This is a type of Azure service dedicated to, you guessed it, data storage. Each storage account has a number of sub-services, which include the following:
Table Storage, which is a type of NoSQL database that stores data in separate tables. These tables are flat and aren’t related to each other. It’s particularly useful for storing reference data.
Blob Storage, which is equivalent to storing files in the cloud. We can store any files containing any data. BLOB is an acronym that stands for Binary Large Objects. Although we can store such data in an SQL database, Azure Blob Storage makes it much easier to do.
Queue Storage, which is a service that allows services to exchange messages with each other by using queues. This service acts more like a message broker rather than a database type; therefore we will cover it in a separate post where we will be talking about message brokers.
So, let’s look at how we can integrate .NET Aspire with Table Storage.
Hosting Azure Storage Component
Because all Azure Storage services are accessible via a storage account, there is a common Aspire component that hosts them all. It’s enabled by adding the following NuGet package to the Aspire host application project:
Aspire.Hosting.Azure.Storage
Then, if we want to host a storage account with Table Storage, we can add the following code to the Program.cs
file:
var tables = builder.AddAzureStorage("storage")
.RunAsEmulator()
.AddTables("tables");
Please note that we are invoking the RunAsEmulator()
method to run the tables in the emulated mode. Otherwise, we can use overrides of the AddAzureStorage()
and AddTables()
to connect the component to real instances of a storage account and Table Storage, respectively.
Then, as always, we will need to pass the Table Storage reference to the service that we want to use it in:
var apiService = builder.AddProject<
Projects.AspireApp_ApiService>(
"apiservice")
.WithReference(tables);
Next, let’s see how we can interact with Table Storage from another service hosted by Aspire.
Interacting with Azure Table Storage
A project that represents any Aspire-hosted service that needs to interact with Azure Table Storage needs to have the following NuGet package installed:
Aspire.Azure.Data.Tables
Then, to register the relevant dependency, we will need to add the following invocation to the Program.cs
file:
builder.AddAzureTableClient("tables");
While dealing with Table Storage, the entity that represents a table entry needs to have some special columns. Therefore, if we want to work with weather forecasts entities we looked at previously, this is what the class would look like:
public class WeatherForecastEntity : ITableEntity
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public string Date { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
public ETag ETag { get; set; } = ETag.All;
public DateTimeOffset? Timestamp { get; set; }
}
We will need two special columns that every tale in the Table Storage has that act as unique identifiers of each entity. Those are PartitionKey
and RowKey
. The former is needed because the data can be partitioned, which is needed if there is a lot of it. The latter represents a unique identifier of each row.
We also need to have ETag
and Timestamp
. The former helps us to manage concurrency, as several rows can be inserted simultaneously. The latter is the time when the row was inserted or updated.
Next, lest’s see how the table storage can be seeded with data. To do so, we will insert the following block into the Program.cs
file:
using (var scope = app.Services.CreateScope())
{
}
Inside this block, we will add this code, which resolves an instance of the TableServiceClient
type registered previously and uses this type to create a table if it doesn’t exists already:
var tableServiceClient = scope.ServiceProvider
.GetRequiredService<TableServiceClient>();
var tableClient = tableServiceClient.GetTableClient("weather");
await tableClient.CreateIfNotExistsAsync();
Next, we query the table to see if it’s empty by executing the following code:
var queryResult = tableClient
.QueryAsync<WeatherForecastEntity>(filter: $"PartitionKey eq 'weather'");
bool hasData = queryResult.ToBlockingEnumerable().Any();
Finally, if it’s empty, we will execute the following code to populate it with the seed data:
if (!hasData)
{
var weatherForecasts =
new List<WeatherForecastEntity>();
foreach (var index in
Enumerable.Range(1, 5))
{
var date = DateOnly.FromDateTime(DateTime.Now.AddDays(index));
var temperatureC = Random.Shared.Next(-20, 55);
var summary = summaries[Random.Shared.Next(summaries.Length)];
var weatherForecast = new WeatherForecastEntity
{
PartitionKey = "weather",
RowKey = Guid.NewGuid().ToString(),
Date = date.ToString(),
TemperatureC = temperatureC,
Summary = summary
};
weatherForecasts.Add(weatherForecast);
}
foreach (var weatherForecast in
weatherForecasts)
{
await tableClient
.AddEntityAsync(weatherForecast);
}
}
Our table has now been populated. All we need to do is set up an API endpoint to retrieve data from it and return it in an HTTP response. This is how we can do it:
app.MapGet("/weatherforecast",
async (TableServiceClient tableServiceClient) =>
{
var tableClient = tableServiceClient
.GetTableClient("weather");
var weatherForecasts =
new List<WeatherForecastEntity>();
var entities = tableClient
.QueryAsync<WeatherForecastEntity>();
await foreach (var entity in entities)
{
weatherForecasts.Add(entity);
}
return weatherForecasts.ToArray();
});
This completes the overview of how we can integrate .NET Aspire with Azure Table Storage. Let’s look at Blob Storage next.
Integrating Azure Blob Storage
A complete application sample that integrates Azure Blob Storage with Aspire can be found here:
Blob Storage is part of an Azure storage account. Therefore, to host it, we will use the same NuGet package as we used for Table Storage, which is as follows:
Aspire.Hosting.Azure.Storage
Then, to host a Blob Storage instance in the emulated mode, we will have the following code in the Program.cs
file of the host application project:
var blobs = builder
.AddAzureStorage("storage")
.RunAsEmulator()
.AddBlobs("blobs");
var apiService = builder.AddProject<
Projects.AspireApp_ApiService>(
"apiservice")
.WithReference(blobs);
Let’s now see how we can interact with it.
Interacting With Azure Blob Storage
In the service that will interact with the data in the Blob Storage, the NuGet package we need to install is the following:
Aspire.Azure.Storage.Blobs
In our example, we will be storing data in a CSV file. To make it easier to work with this format, another NuGet package we will install is the following:
CsvHelper
Once we added both NuGet package references to the project file, we will need to insert the following code to the content of the Progrm.cs
file:
builder.AddAzureBlobClient("blobs");
Then, as before, we will have the following block we will use to seed the storage with the initial data:
using (var scope = app.Services.CreateScope())
{
}
Inside this block, we will insert the following code:
var blobServiceClient = scope.ServiceProvider
.GetRequiredService<BlobServiceClient>();
var containerClient = blobServiceClient
.GetBlobContainerClient("weather");
// Create the container if it doesn't exist
await containerClient.CreateIfNotExistsAsync();
var blobClient = containerClient
.GetBlobClient("weather-forecasts.csv");
// Check if the blob already exists
if (await blobClient.ExistsAsync())
{
return;
}
This code performs the following:
Checks that a blob container called `weather` already exists. If not, the container is created.
Does the same for a file inside the container called `weather-forecasts.csv`. If a file with this name doesn’t exist, it gets created.
If the file already exists, we just exit the sequence.
Next, we have the following code that populates a collection with weather forecasts:
var weatherForecasts =
new List<WeatherForecast>();
foreach (var index in Enumerable.Range(1, 5))
{
var date = DateOnly
.FromDateTime(DateTime.Now.AddDays(index));
var temperatureC = Random.Shared.Next(-20, 55);
var summary = summaries[Random.Shared.Next(summaries.Length)];
weatherForecasts.Add(new WeatherForecast
{
Date = date,
TemperatureC = temperatureC,
Summary = summary
});
}
After this, we write this data into the CSV file by using the following code:
using (var memoryStream = new MemoryStream())
using (var writer = new StreamWriter(memoryStream, Encoding.UTF8))
using (var csv = new CsvWriter(writer,
new CsvConfiguration(CultureInfo.InvariantCulture)))
{
csv.WriteRecords(weatherForecasts);
writer.Flush();
// Upload the CSV to the blob
memoryStream.Position = 0;
await blobClient.UploadAsync(memoryStream, overwrite: true);
}
Finally, we will modify the API endpoint to read the data from the file and return it to the caller:
app.MapGet("/weatherforecast", async (
BlobServiceClient blobServiceClient) =>
{
var containerClient = blobServiceClient
.GetBlobContainerClient("weather");
var blobClient = containerClient
.GetBlobClient("weather-forecasts.csv");
if (!await blobClient.ExistsAsync())
{
return new List<WeatherForecast>().ToArray();
}
var weatherForecasts =
new List<WeatherForecast>();
// Download the CSV from the blob
var downloadResponse = await blobClient.DownloadAsync();
using (var stream = downloadResponse.Value.Content)
using (var reader = new StreamReader(stream))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
weatherForecasts = csv.GetRecords<WeatherForecast>().ToList();
}
return weatherForecasts.ToArray();
});
This concludes the overview of integrating Azure Blob Storage with .NET Aspire.
Wrapping Up
We saw that various Azure services can use emulators, so we don’t have to connect our local development machine to a real Azure instance while using .NET Aspire.
Aspire has several components that allow for seamless integration with both Azure-specific and cloud-agnostic services. Anything that isn’t covered by Aspire components can be enabled via standard Docker containers.
P.S. If you want me to help you improve your software development skills, you can check out my courses and my books. You can also book me for one-on-one mentorship.