Working with Azure Storage Blobs client library

The Azure Storage Blobs is the client library for accessing Azure Blob Storage, I use it to save uploaded images for this blog in production. In this post, I'm going to show how to work with it from the perspectives of tools, code and test.

Tools

Before any code, I need to get the following tools ready, so that I could upload and verify resources in my local storage account.

  • Azurite is now the new emulator for local development. The previous Azure Storage Emulator has been abandoned.
    • install it npm install -g azurite
    • create a directory where you want to store your local data
    • cd into the directory and run azurite --silent to start it
  • Azure Storage Explorer is the desktop tool for managing local and remote Azure cloud storage resources.

Code

The previous Microsoft.Azure.Storage.Blob nuget package has been deprecated, the new package to install is now  Azure.Storage.Blobs and it has an updated API. For code sample, check out its documentation and GitHub repo.

When working with this library, there are 3 important concepts to know

  • storage account used via BlobServiceClient
  • container in the storage account used via BlobContainerClient
  • blob in a container used via BlobClient
Azure Storage Blobs client library
Azure Storage Blobs client library

I have a class AzureBlobStorageProvider that interacts with this library, in its constructor I instantiate both the storage account and container, and either or both of them needs the connection string for creation which is stored in the appsettings.json. For local development the connection string is "UseDevelopmentStorage=true".

public AzureBlobStorageProvider(IConfiguration configuration)
{
    var connectionString = configuration.GetConnectionString("BlobStorageConnectionString");
    var containerName = configuration.GetValue<string>("AppSettings:MediaContainerName");

    _storageAccount = new BlobServiceClient(connectionString);
    _container = new BlobContainerClient(connectionString, containerName);
    _container.CreateIfNotExists(PublicAccessType.Blob);
}

The container is responsible to create blob

private BlobClient GetBlob(string fileName, string path) => _container.GetBlobClient($"{path}/{fileName}");

The storage account gives me the storage endpoint which is the absolute URI to a stored resource.

public string StorageEndpoint => _storageAccount.Uri.AbsoluteUri.ToString();

Before uploading the file, it's important to set its ContentType correctly. Images without property content types may show up as blank. And the blob is responsible for operations like Upload and Delete resources.

public async Task SaveFileAsync(Stream source, string fileName, string path)
{
    var blob = GetBlob(fileName, path);
    var options = new BlobUploadOptions
    {
        HttpHeaders = new BlobHttpHeaders
        {
            ContentType = MimeTypeMap.GetMimeType(Path.GetExtension(fileName)),
            CacheControl = "public, max-age=31536000" // 1 yr
        }
    };
    await blob.UploadAsync(source, options);
}

Test

I use integration tests for AzureBlobStorageProvider because I want to make sure my saving and deleting to and from the blob storage actually work. To do that the first thing I need is to mock IConfiguration which is used by the constructor for the values of BlobStorageConnectionString and MediaContainerName. There is a memory configuration provider that takes in a dictionary of string values for this purpose, and here is a VS Magazine post with more details.

public AzureBlobStorageProviderTest()
{
    var configValues = new Dictionary<string, string>
    {
        {"ConnectionStrings:BlobStorageConnectionString", "UseDevelopmentStorage=true"},
        {"AppSettings:MediaContainerName", "media-test"}
    };

    var configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues).Build();
    provider = new AzureBlobStorageProvider(configuration);
}

Since my tests are integration tests that need to run manually, they will fail on build server. Therefore I need a way to skip these tests unless I manually turn them on. I use xUnit for testing and it has a Skip property on the FactAttribute for this purpose.

But placing [Fact(Skip = "A message")] direct on these tests would not work as xUnit emits warnings. Instead I need to create a subclass that derives from FactAttribute. Here is the code for my IgnoreAttribute class, and notice before I run these test locally I need to turn the flag of skip to false.

public class IgnoreAttribute : FactAttribute
{
    /// <summary>
    /// Certain tests are to run locally only, when ready turn the flag to false.
    /// </summary>
    private readonly bool _skipManualRun = true;
    public IgnoreAttribute()
    {
        if (_skipManualRun)
        {
            Skip = "Skipped for manual run integration tests.";
        }
    }
}

Here are my tests for interacting with Azure Blob Storage and my local Azurite emulator, they are all labeled with the Ignore attribute.

/// <summary>
/// Creates a text file named "test.txt" with the following URI
/// http://127.0.0.1:10000/devstoreaccount1/media-test/files/test.txt
/// </summary>
[Ignore]
public async void SaveFileAsyncTest()
{
    string path = FileTestHelper.CreateTempFile();
    using FileStream file = File.OpenRead(path);
    await provider.SaveFileAsync(file, FileName, FilePath);
}

/// <summary>
/// Deletes the file created.
/// </summary>
[Ignore]
public async void DeleteFileAsyncTest()
{
    await provider.DeleteFileAsync(FileName, FilePath);
}

[Ignore]
public void StorageEndpointTest()
{
    Assert.Equal("http://127.0.0.1:10000/devstoreaccount1", provider.StorageEndpoint);
}

Finally, to tie everything together by verifying a file I upload with the SaveFileAsyncTest with Azurite running and Azure Storage Explorer open.

Azurite and Azure Storage Explorer
Azurite and Azure Storage Explorer