Best practices for integrating the Azure Storage SDK into your .NET applications
Tags: dotnet, azure, programming, csharp
Published: 2024-01-23 12:30:00

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 clients? Should I create my own option classes? 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](https://anthonysimmon.com/defaultazurecredential-local-development-optimization/). 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](https://www.nuget.org/packages/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:

- [Microsoft.Extensions.Configuration.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.Abstractions/) and [Microsoft.Extensions.Options](https://www.nuget.org/packages/Microsoft.Extensions.Options/) for configuration,
- [Microsoft.Extensions.DependencyInjection.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection.Abstractions/) for dependency injection,
- [Microsoft.Extensions.Logging.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Abstractions/) for logging.

If you are using [ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/webapplication?view=aspnetcore-8.0), or if your application uses the [.NET generic host](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host), then you already have these dependencies in your project.

Install Microsoft.Extensions.Azure in your project:

```bash
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.

```csharp
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:

```js
{
  "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:

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

Microsoft.Extensions.Azure [uses reflection](https://github.com/Azure/azure-sdk-for-net/blob/Microsoft.Extensions.Azure_1.7.1/sdk/extensions/Microsoft.Extensions.Azure/src/Internal/ClientFactory.cs#L22) 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](https://learn.microsoft.com/en-us/dotnet/core/extensions/options#options-interfaces), it is possible to register multiple Azure Blob Storage clients in the dependency injection service. These are named clients:

```csharp
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:

```csharp
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](https://github.com/Azure/azure-sdk-for-net/blob/Microsoft.Extensions.Azure_1.7.1/sdk/extensions/Microsoft.Extensions.Azure/src/AzureClientFactoryBuilder.cs#L21).

### 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:

```csharp
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:

```csharp
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`:

```csharp
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`:

```csharp
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](https://github.com/Azure/azure-sdk-for-net/blob/Microsoft.Extensions.Azure_1.7.1/sdk/extensions/Microsoft.Extensions.Azure/src/Internal/AzureEventSourceLogForwarder.cs) to an `ILogger` instance.

## References

- [Microsoft.Extensions.Azure documentation](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/microsoft.extensions.azure-readme)
- [Microsoft.Extensions.Azure source code](https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/extensions/Microsoft.Extensions.Azure)