Featured image of post Best practices for integrating the Azure Storage SDK into your .NET applications

Best practices for integrating the Azure Storage SDK into your .NET applications

Learn best practices for uniform integration of Azure Storage SDK in modern .NET apps with Microsoft.Extensions.Azure.

I have often had to integrate the Azure Storage SDK into various applications, and each time, I’ve done it differently. Here’s an overview of the questions I’ve asked myself over time:

Should I register a BlobServiceClient instance in the dependency injection service? What if I need to use multiple storage accounts? How do I manage multiple clients for different resources, such as containers, tables, or individual queues? Should I create my own abstraction or factory on top of the SDK?

How do I configure the client from my configuration? How can I easily override the SDK options? What if I want to use the Azurite emulator locally, and a managed identity in the Azure cloud environment?

I realized that I often created instances of DefaultAzureCredential throughout my code, for different services such as Azure Storage, Event Grid, Key Vault, etc. DefaultAzureCredential can slow down application startup in a local environment. How can I avoid the multiplication of these TokenCredential derived instances?

If you’ve ever asked yourself these questions, then this article is for you. Just like for dependency injection, configuration, logging, and HTTP request resilience, Microsoft provides a library of extensions to uniformly integrate the Azure SDK into your applications.

Unsurprisingly, the name of this library is Microsoft.Extensions.Azure. In the remainder of this article, we will use it to integrate the Azure Blob Storage SDK, but similar code applies to other Azure services.

Prerequisites for using Microsoft.Extensions.Azure

Microsoft.Extensions.Azure is built on top of the Microsoft extension libraries ecosystem. Therefore, you need to install the following libraries:

If you are using ASP.NET Core, or if your application uses the .NET generic host, then you already have these dependencies in your project.

Install Microsoft.Extensions.Azure in your project:

dotnet add package Microsoft.Extensions.Azure

Various ways to configure the Azure Blob Storage SDK with Microsoft.Extensions.Azure

Microsoft.Extensions.Azure provides several ways to configure the Azure Storage SDK. We will explore them from the simplest to the most flexible.

Single client registration

The simplest way to configure the Azure Blob Storage SDK is as follows. Here, we assume that we want to create only one BlobServiceClient in an ASP.NET Core application.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAzureClients(azure =>
{
    azure.AddBlobServiceClient(builder.Configuration.GetRequiredSection("Azure:Storage:Blob"));
});

Next, in your configuration, you just need to create the corresponding section:

{
  "Azure": {
    "Storage": {
      "Blob": {
        // Either this way (ideally the value would be provided from a secret vault)
        "ConnectionString": "<your-connection-string>",

        // Or this way (if you want to use a managed identity or SAS token)
        "ServiceUri": "<your-service-uri>"
      }
    }
  }
}

Subsequently, you can retrieve the instance of BlobServiceClient via dependency injection:

public class MyController : ControllerBase
{
    public MyController(BlobServiceClient blobServiceClient)
    {
        // ...
    }
}

Microsoft.Extensions.Azure uses reflection to find the appropriate constructor to use to create the BlobServiceClient instance based on the provided configuration section.

Multiple client registration

Similarly to named options, it is possible to register multiple Azure Blob Storage clients in the dependency injection service. These are named clients:

services.AddAzureClients(azure =>
{
    azure.AddBlobServiceClient(configuration.GetRequiredSection("Azure:Storage:Blob:Invoices"))
        .WithName("Invoices");

    azure.AddBlobServiceClient(configuration.GetRequiredSection("Azure:Storage:Blob:Backups"))
        .WithName("Backups");
});

Then, you use the IAzureClientFactory<BlobServiceClient> interface to retrieve the client you want:

var clientFactory = serviceProvider.GetRequiredService<IAzureClientFactory<BlobServiceClient>>();

var invoicesClient = clientFactory.CreateClient("Invoices");
var backupsClient = clientFactory.CreateClient("Backups");

This approach allows for greater flexibility and control, enabling you to manage multiple Azure Blob Storage clients with different configurations within the same application.

It’s worth mentioning that clients registered without specifying a name are automatically named Default as we can see in the extensions source code.

Customizing the client factory with your own custom options

Instead of using the AddBlobServiceClient extension method, you can use the more granular AddClient<TClient, TOptions>. Consider the following custom options for demonstration purposes:

public class InvoiceClientOptions
{
    public string AzureStorageUri { get; set; } = string.Empty;
}

We can then write a custom BlobServiceClient factory compatible with both Azurite and a real Azure Storage account protected by managed identity:

services.AddAzureClients(azure =>
{
    azure.AddClient<BlobServiceClient, BlobClientOptions>((clientOptions, tokenCredential, serviceProvider) =>
        {
            var invoiceOptions = serviceProvider.GetRequiredService<IOptions<InvoiceClientOptions>>();
            var serviceUri = new Uri(invoiceOptions.Value.AzureStorageUri, UriKind.Absolute);

            return serviceUri.Scheme == "https"
                ? new BlobServiceClient(serviceUri, tokenCredential, clientOptions) // Using TokenCredential
                : new BlobServiceClient(serviceUri, clientOptions);                 // Using a shared access signature (SAS) for Azurite
        })
        .WithName("Invoices");
});

Customizing the Azure credential used by all Azure SDK clients created using Microsoft.Extensions.Azure

By default, Microsoft.Extensions.Azure uses DefaultAzureCredential to create the TokenCredential used by all Azure SDK clients. This can be customized by providing your own TokenCredential:

services.AddAzureClients(azure =>
{
    // Speed up the startup time of your application in local development by using Azure CLI in priority,
    // and falling back to DefaultAzureCredential if Azure CLI is not available.
    // See benchmarks at https://anthonysimmon.com/defaultazurecredential-local-development-optimization/
    azure.UseCredential(new ChainedTokenCredential(new AzureCliCredential(), new DefaultAzureCredential()));

    // ... client registrations
});

Customizing the options for a given client

You can customize the BlobClientOptions that will be used by the BlobServiceClient:

services.AddAzureClients(azure =>
{
    // Method 1: default client
    azure.AddBlobServiceClient(configuration.GetRequiredSection("Azure:Storage:Blob"))
        .ConfigureOptions(options => options.Retry.MaxRetries = 10);

    // Method 2: named client
    azure.AddBlobServiceClient(configuration.GetRequiredSection("Azure:Storage:Blob:Backups"))
        .ConfigureOptions(options => options.Retry.MaxRetries = 10)
        .WithName("Backups");
});

// Method 3: still configuring a named client, but directly using the IConfigureOptions<TOptions> interface,
// which can be done anywhere in your startup code
services.Configure<BlobClientOptions>("Backups", options =>
{
    options.Retry.Mode = RetryMode.Exponential;
});

Conclusion

Microsoft.Extensions.Azure is an extension library that allows for uniform integration of the Azure SDK into your applications, while giving you the necessary flexibility to customize the behavior of the created Azure SDK clients. The use of named clients is particularly convenient for supporting multiple instances of the same Azure resource type. You also get free logging as the Azure SDK events are automatically forwarded to an ILogger instance.

References

Licensed under CC BY 4.0
Ko-fi donations Buy me a coffee