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:
Install the Microsoft.Extensions.Azure package in your project:
dotnet add package Microsoft.Extensions.Azure
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
- andstring
-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:
- The top-level key names,
AzureDefaults
,KeyVault
,ServiceBus
, andStorage
, are arbitrary. All other key names hold significance, and JSON serialization is performed in a case-insensitive manner. - The
AzureDefaults.Retry
object literal:- Represents the retry policy configuration settings.
- Corresponds to the Retry property. Within that object literal, you find the
MaxRetries
key, which corresponds to the MaxRetries property.
- The
KeyVault:VaultUri
,ServiceBus:Namespace
, andStorage:ServiceUri
key values map to theUri
- andstring
-typed arguments of the Azure.Security.KeyVault.Secrets.SecretClient.SecretClient(Uri, TokenCredential, SecretClientOptions), Azure.Messaging.ServiceBus.ServiceBusClient.ServiceBusClient(String), and Azure.Storage.Blobs.BlobServiceClient.BlobServiceClient(Uri, TokenCredential, BlobClientOptions) constructor overloads, respectively. TheTokenCredential
variants of the constructors are used because a defaultTokenCredential
is set via the Microsoft.Extensions.Azure.AzureClientFactoryBuilder.UseCredential(TokenCredential) method call.
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
}
}
}