Using dependency injection with Azure .NET SDK

Register your Azure clients the proper way

I love how the Azure SDKs have evolved over the years. In the past, there was no consistency between the various Azure SDKs. However, that's not longer the case (at least for most Azure libraries), as they now adhere to the same principles and follow a set of well-defined guidelines.

💡
You can learn more about these guidelines and how the Azure .NET SDKs work in this video from 2021 which is I think still relevant today.

Having consistency between libraries, it's easier to handle things like authentication and dependency injection consistently when you are using multiple Azure SDKs in your project.

One aspect often overlooked by people using Azure SDKs is the use of Microsoft.Extensions.Azure. This package facilitates registering and configuring the service clients for interacting with Azure APIs.

Let's see why using this package could be beneficial for your project.

Avoid making mistakes when registering service clients

It's mentioned in the documentation to use this package for dependency injection with the Azure SDK for .NET. Still, many people don't read the documentation and manually register the Azure service clients.

It's not a problem in itself if you know what you are doing. Otherwise,

  • you might choose the wrong lifetime for the Azure service clients, they must be singleton

  • you may forget to register a dependency that is needed for your use of the SDK

using Azure.Identity;
using DIWithAzureSDK;
using Microsoft.Extensions.Azure;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Services.AddAzureClients(clientBuilder =>
{
    clientBuilder.AddBlobServiceClient(new Uri("https://stdiwithazuresdk.blob.core.windows.net/"));
    clientBuilder.UseCredential(new DefaultAzureCredential());
});

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

In this sample, the AddBlobServiceClient handles the registration of all dependencies for us so that the BlobServiceClient can then be injected directly where needed.

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly BlobServiceClient _blobServiceClient;

    public Worker(ILogger<Worker> logger, BlobServiceClient blobServiceClient)
    {
        _logger = logger;
        _blobServiceClient = blobServiceClient;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var blobContainer in _blobServiceClient.GetBlobContainersAsync(cancellationToken: stoppingToken))
        {
            _logger.LogInformation(blobContainer.Name);
        }
    }
}
💬
You may find it convenient to configure the dependency injection for all Azure service clients in a central place with the AddAzureClients method. When applications become larger with different csproj, I often prefer to separate service registration by business domain/module so having everything in a central place does not always suit my needs. That's not a problem, as the internal methods of the library make use of the TryAddd methods for registering services, I can call AddAzureClients in multiple places with only the services I want to register.

Easily manage the authentication to Azure services

All the SDKs use the Azure.Identity package to authenticate to Azure. There are different authentication methods available and you can easily specify which one to use with each client. Additionally, you can define a default authentication method for all clients, as demonstrated in the previous example.

builder.Services.AddAzureClients(clientBuilder =>
{
    clientBuilder.AddServiceBusClient("https://sb-diwithazuresdk.servicebus.windows.net/")
        .WithCredential(new ManagedIdentityCredential());

    clientBuilder.AddTableServiceClient(new Uri("https://stdiwithazuresdk.table.core.windows.net"))
        .WithCredential(new EnvironmentCredential());

    clientBuilder.AddBlobServiceClient(new Uri("https://stdiwithazuresdk.blob.core.windows.net/"));

    clientBuilder.UseCredential(new DefaultAzureCredential());
});

In the example above, we configured:

  • the service bus client to use the managed identity of the application to obtain a valid token for the service bus

  • the table client to use environment variables to obtain a valid token for the storage table

  • the blob client without any credentials so that it will use the one that we configured by default (with the UseCredential method)

Effortlessly configure the Azure clients' options

All Azure clients have options that can be effortlessly configured when registering them in the AddAzureClients method.

builder.Services.AddAzureClients(clientBuilder =>
{
    clientBuilder.AddBlobServiceClient(new Uri("https://stdiwithazuresdk.blob.core.windows.net/"))
        .WithCredential(new DefaultAzureCredential())
        .ConfigureOptions(options =>
        {
            options.TrimBlobNameSlashes = true;
            options.Retry.MaxRetries = 10;
            options.Diagnostics.IsLoggingEnabled = false;
        });
});

Some options are specific to the client (like the TrimBlobNameSlashes here for Blob client). Others can be configured globally and overridden on a client if necessary.

builder.Services.AddAzureClients(clientBuilder =>
{
   clientBuilder.AddBlobServiceClient(new Uri("https://stdiwithazuresdk.blob.core.windows.net/"))
        .WithCredential(new DefaultAzureCredential())
        .ConfigureOptions(options =>
        {
            options.TrimBlobNameSlashes = true;
            options.Retry.MaxRetries = 10;
            options.Diagnostics.IsLoggingEnabled = false;
        });

    clientBuilder.ConfigureDefaults(options =>
    {
        options.Retry.MaxRetries = 5;
        options.Retry.Mode = RetryMode.Exponential;
        options.Diagnostics.IsDistributedTracingEnabled = true;
    });
});

That's the purpose of the ConfigureDefaults method.

💡
Please note that all this configuration (as well as the Uris of each client) can be loaded from the configuration like this clientBuilder.AddTableServiceClient(builder.Configuration.GetSection("Inventory:Tables"));

Use named clients for different Azure resources

Usually, you only need one client of each SDK in your application. Let's say you have multiple Azure Storage tables that are used in your application, you will only need to have one TableServiceClient. However, if you are interacting with tables in two different storage accounts, you will need multiple table clients.

To do that you can register your clients with a specific name:

builder.Services.AddAzureClients(clientBuilder =>
{
    clientBuilder.AddTableServiceClient(builder.Configuration.GetSection("Shop:Inventory"))
        .WithName("Shop");
    clientBuilder.AddTableServiceClient(builder.Configuration.GetSection("Warehouse:Inventory"))
        .WithName("Warehouse");
}

This way, you will be able to retrieve the specific client you need in your code:

public class WarehouseDeliveryService
{
    private readonly TableServiceClient _tableServiceClient;

    public WarehouseDeliveryService(IAzureClientFactory<TableServiceClient> azureClientFactory)
    {
        _tableServiceClient = azureClientFactory.CreateClient("Warehouse");
    }
}

Register a custom client factory

If you have specific needs, the AddClient method can help you register your Azure client while letting you control how you instantiate the client.

For instance, the Azure Cosmos Db .NET SDK is not built on the same foundation (Azure.Core) as the other SDKs. So at the time of writing, there is no AddCosmosServiceClient you can use in the AddAzureClients (there is an issue about that though). However, you can use the AddClient I've just mentioned.

builder.Services.AddOptions<CosmosDbConfiguration>().BindConfiguration("Warehouse:CosmosDb");
builder.Services.AddAzureClients(clientBuilder =>
{
    clientBuilder.AddClient<CosmosClient, CosmosClientOptions>((_, _, serviceProvider) =>
    {
        var cosmosConfiguration = serviceProvider.GetRequiredService<IOptions<CosmosDbConfiguration>>().Value;
        return new CosmosClientBuilder(cosmosConfiguration.Endpoint, cosmosConfiguration.AuthKey)
            .WithSerializerOptions(new () { PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase })
            .Build();
    }).WithName("Warehouse");
}

You can note that using the AddClient method allows us to take profit from the named clients' feature.

Wrapping up

As you have seen, the use of the Microsoft.Extensions.Azure package simplifies the registration and configuration of Azure clients. While providing you with a consistent way of handling the dependency injection for Azure SDKs, it also allows you to easily customize the authentication and other options available.

I hope you learned something. Don't hesitate to share your tips or what you like about the Azure SDKs in the comments.