Dependency injection with the Azure SDK for .NET

This article demonstrates how to register Azure service clients from the latest Azure client libraries for .NET for dependency injection in a .NET app. Every modern .NET app starts up by using the instructions provided in a Program.cs file.

Install packages

To register and configure service clients from an Azure.-prefixed package:

  1. Install the Microsoft.Extensions.Azure package in your project:

    dotnet add package Microsoft.Extensions.Azure
    
  2. Install the Azure.Identity package to configure a TokenCredential type to use for authenticating all registered clients that accept such a type:

    dotnet add package Azure.Identity
    

For demonstration purposes, the sample code in this article uses the Key Vault Secrets, Blob Storage, Service Bus, and Azure OpenAI libraries. Install the following packages to follow along:

dotnet add package Azure.Security.KeyVault.Secrets
dotnet add package Azure.Storage.Blobs
dotnet add package Azure.Messaging.ServiceBus
dotnet add package Azure.AI.OpenAI

Register clients and subclients

A service client is the entry point to the API for an Azure service – from it, library users can invoke all operations the service provides and can easily implement the most common scenarios. Where it will simplify an API's design, groups of service calls can be organized around smaller subclient types. For example, ServiceBusClient can register additional ServiceBusSender subclients for publishing messages or ServiceBusReceiver subclients for consuming messages.

In the Program.cs file, invoke the AddAzureClients extension method to register a client for each service. The following code samples provide guidance on application builders from the Microsoft.AspNetCore.Builder and Microsoft.Extensions.Hosting namespaces.

using Azure.Identity;
using Azure.Messaging.ServiceBus;
using Azure.Messaging.ServiceBus.Administration;
using Microsoft.Extensions.Azure;
using Azure.AI.OpenAI;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddAzureClients(async clientBuilder =>
{
    // Register clients for each service
    clientBuilder.AddSecretClient(new Uri("<key_vault_url>"));
    clientBuilder.AddBlobServiceClient(new Uri("<storage_url>"));
    clientBuilder.AddServiceBusClientWithNamespace(
        "<your_namespace>.servicebus.windows.net");

    // Set a credential for all clients to use by default
    DefaultAzureCredential credential = new();
    clientBuilder.UseCredential(credential);

    // Register a subclient for each Service Bus Queue
    List<string> queueNames = await GetQueueNames(credential);
    foreach (string queue in queueNames)
    {
        clientBuilder.AddClient<ServiceBusSender, ServiceBusClientOptions>(
            (_, _, provider) => provider.GetService<ServiceBusClient>()
                .CreateSender(queue)).WithName(queue);
    }

    // Register a custom client factory
    clientBuilder.AddClient<AzureOpenAIClient, AzureOpenAIClientOptions>(
        (options, _, _) => new AzureOpenAIClient(
            new Uri("<url_here>"), credential, options)); 
});

WebApplication app = builder.Build();

async Task<List<string>> GetQueueNames(DefaultAzureCredential credential)
{
    // Query the available queues for the Service Bus namespace.
    var adminClient = new ServiceBusAdministrationClient
        ("<your_namespace>.servicebus.windows.net", credential);
    var queueNames = new List<string>();

    // Because the result is async, the queue names need to be captured
    // to a standard list to avoid async calls when registering. Failure to
    // do so results in an error with the services collection.
    await foreach (QueueProperties queue in adminClient.GetQueuesAsync())
    {
        queueNames.Add(queue.Name);
    }

    return queueNames;
}

In the preceding code:

  • Key Vault Secrets, Blob Storage, and Service Bus clients are registered using the AddSecretClient, AddBlobServiceClient and AddServiceBusClientWithNamespace, respectively. The Uri- and string-typed arguments are passed. To avoid specifying these URLs explicitly, see the Store configuration separately from code section.
  • DefaultAzureCredential is used to satisfy the TokenCredential argument requirement for each registered client. When one of the clients is created, DefaultAzureCredential is used to authenticate.
  • Service Bus subclients are registered for each queue on the service using the subclient and corresponding options types. The queue names for the subclients are retrieved using a separate method outside of the service registration because the GetQueuesAsync method must be run asynchronously.
  • An Azure OpenAI client is registered using a custom client factory via the AddClient method, which provides control over how a client instance is created. Custom client factories are useful in the following cases:
    • You need to use other dependencies during the client construction.
    • A registration extension method doesn't exist for the service client you want to register.

Use the registered clients

With the clients registered, as described in the Register clients and subclients section, you can now use them. In the following example, constructor injection is used to obtain the Blob Storage client and a factory for Service Bus sender subclients in an ASP.NET Core API controller:

[ApiController]
[Route("[controller]")]
public class MyApiController : ControllerBase
{
    private readonly BlobServiceClient _blobServiceClient;
    private readonly ServiceBusSender _serviceBusSender;
  
    public MyApiController(
        BlobServiceClient blobServiceClient,
        IAzureClientFactory<ServiceBusSender> senderFactory)
    {
        _blobServiceClient = blobServiceClient;
        _serviceBusSender = senderFactory.CreateClient("myQueueName");
    }
  
    [HttpGet]
    public async Task<IEnumerable<string>> Get()
    {
        BlobContainerClient containerClient = 
            _blobServiceClient.GetBlobContainerClient("demo");
        var results = new List<string>();

        await foreach (BlobItem blob in containerClient.GetBlobsAsync())
        {
            results.Add(blob.Name);
        }

        return results.ToArray();
    }
}

Store configuration separately from code

In the Register clients and subclients section, you explicitly passed the Uri-typed variables to the client constructors. This approach could cause problems when you run code against different environments during development and production. The .NET team suggests storing such configurations in environment-dependent JSON files. For example, you can have an appsettings.Development.json file containing development environment settings. Another appsettings.Production.json file would contain production environment settings, and so on. The file format is:

{
  "AzureDefaults": {
    "Diagnostics": {
      "IsTelemetryDisabled": false,
      "IsLoggingContentEnabled": true
    },
    "Retry": {
      "MaxRetries": 3,
      "Mode": "Exponential"
    }
  },
  "KeyVault": {
    "VaultUri": "https://mykeyvault.vault.azure.net"
  },
  "ServiceBus": {
    "Namespace": "<your_namespace>.servicebus.windows.net"
  },
  "Storage": {
    "ServiceUri": "https://mydemoaccount.storage.windows.net"
  }
}

You can add any properties from the ClientOptions class into the JSON file. The settings in the JSON configuration file can be retrieved using IConfiguration.

builder.Services.AddAzureClients(clientBuilder =>
{
    clientBuilder.AddSecretClient(
        builder.Configuration.GetSection("KeyVault"));

    clientBuilder.AddBlobServiceClient(
        builder.Configuration.GetSection("Storage"));

    clientBuilder.AddServiceBusClientWithNamespace(
        builder.Configuration["ServiceBus:Namespace"]);

    clientBuilder.UseCredential(new DefaultAzureCredential());

    // Set up any default settings
    clientBuilder.ConfigureDefaults(
        builder.Configuration.GetSection("AzureDefaults"));
});

In the preceding JSON sample:

Configure multiple service clients with different names

Imagine you have two storage accounts: one for private information and another for public information. Your app transfers data from the public to the private storage account after some operation. You need to have two storage service clients. To differentiate those two clients, use the WithName extension method:

builder.Services.AddAzureClients(clientBuilder =>
{
    clientBuilder.AddBlobServiceClient(
        builder.Configuration.GetSection("PublicStorage"));

    clientBuilder.AddBlobServiceClient(
            builder.Configuration.GetSection("PrivateStorage"))
        .WithName("PrivateStorage");
});

Using an ASP.NET Core controller as an example, access the named service client using the IAzureClientFactory<TClient> interface:

public class HomeController : Controller
{
    private readonly BlobServiceClient _publicStorage;
    private readonly BlobServiceClient _privateStorage;

    public HomeController(
        BlobServiceClient defaultClient,
        IAzureClientFactory<BlobServiceClient> clientFactory)
    {
        _publicStorage = defaultClient;
        _privateStorage = clientFactory.CreateClient("PrivateStorage");
    }
}

The unnamed service client is still available in the same way as before. Named clients are additive.

Configure a new retry policy

At some point, you may want to change the default settings for a service client. For example, you may want different retry settings or to use a different service API version. You can set the retry settings globally or on a per-service basis. Assume you have the following appsettings.json file in your ASP.NET Core project:

{
  "AzureDefaults": {
    "Retry": {
      "maxRetries": 3
    }
  },
  "KeyVault": {
    "VaultUri": "https://mykeyvault.vault.azure.net"
  },
  "ServiceBus": {
    "Namespace": "<your_namespace>.servicebus.windows.net"
  },
  "Storage": {
    "ServiceUri": "https://store1.storage.windows.net"
  },
  "CustomStorage": {
    "ServiceUri": "https://store2.storage.windows.net"
  }
}

You can change the retry policy to suit your needs like so:

builder.Services.AddAzureClients(clientBuilder =>
{
    // Establish the global defaults
    clientBuilder.ConfigureDefaults(
        builder.Configuration.GetSection("AzureDefaults"));
    clientBuilder.UseCredential(new DefaultAzureCredential());

    // A Key Vault Secrets client using the global defaults
    clientBuilder.AddSecretClient(
        builder.Configuration.GetSection("KeyVault"));

    // A Blob Storage client with a custom retry policy
    clientBuilder.AddBlobServiceClient(
            builder.Configuration.GetSection("Storage"))
        .ConfigureOptions(options => options.Retry.MaxRetries = 10);

    clientBuilder.AddServiceBusClientWithNamespace(
            builder.Configuration["ServiceBus:Namespace"])
        .ConfigureOptions(options => options.RetryOptions.MaxRetries = 10);

    // A named storage client with a different custom retry policy
    clientBuilder.AddBlobServiceClient(
            builder.Configuration.GetSection("CustomStorage"))
        .WithName("CustomStorage")
        .ConfigureOptions(options =>
        {
            options.Retry.Mode = Azure.Core.RetryMode.Exponential;
            options.Retry.MaxRetries = 5;
            options.Retry.MaxDelay = TimeSpan.FromSeconds(120);
        });
});

You can also place retry policy overrides in the appsettings.json file:

{
  "KeyVault": {
    "VaultUri": "https://mykeyvault.vault.azure.net",
    "Retry": {
      "maxRetries": 10
    }
  }
}

See also