Injeção de dependência com o SDK do Azure para .NET

Este artigo demonstra como registrar clientes de serviço do Azure dos SDKs do Azure mais recentes para .NET para injeção de dependência em um aplicativo .NET. Todo aplicativo .NET moderno é inicializado usando as instruções fornecidas em um arquivo Program.cs.

Instalar Pacotes

Para registrar e configurar clientes de serviço de um Azure.-pacote prefixado:

  1. Instale o pacote Microsoft.Extensions.Azure em seu projeto:

    dotnet add package Microsoft.Extensions.Azure
    
  2. Instale o pacote Azure.Identity para configurar um tipo TokenCredential a ser usado para autenticar todos os clientes registrados que aceitam esse tipo:

    dotnet add package Azure.Identity
    

Para fins de demonstração, o código de exemplo neste artigo usa as bibliotecas Key Vault Secrets, Blob Storage, Service Bus e Azure OpenAI. Instale os seguintes pacotes para acompanhar:

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

Registrar clientes e subclientes

Um cliente de serviço é o ponto de entrada para a API de um serviço do Azure. A partir dele, os usuários da biblioteca podem invocar todas as operações que o serviço fornece e implementar facilmente os cenários mais comuns. Em locais onde ele simplificará o design de uma API, grupos de chamadas de serviço podem ser organizados em torno de tipos de subclientes menores. Por exemplo, ServiceBusClient pode registrar mais ServiceBusSender subclientes para publicar mensagens ou ServiceBusReceiver subclientes para consumir mensagens.

No arquivo Program.cs, invoque o método de extensão AddAzureClients para registrar um cliente para cada serviço. Os exemplos de código a seguir fornecem diretrizes sobre construtores de aplicativos dos namespaces Microsoft.AspNetCore.Builder e Microsoft.Extensions.Hosting.

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;
}

No código anterior:

  • Os clientes dos Segredos do Key Vault, do Armazenamento de Blobs e do Barramento de Serviço são registrados usando o AddSecretClient, AddBlobServiceClient e AddServiceBusClientWithNamespace, respectivamente. Os argumentos de tipo Uri e string são passados. Para evitar especificar essas URLs explicitamente, confira a seção Armazenar a configuração separadamente do código.
  • DefaultAzureCredential é usado para atender ao requisito do argumento TokenCredential para cada cliente registrado. Quando um dos clientes é criado, DefaultAzureCredential é usado para autenticar.
  • Os subclientes do Barramento de Serviço são registrados para cada fila no serviço usando o subcliente e os tipos de opções correspondentes. Os nomes de fila para os subclientes são recuperados usando um método separado fora do registro de serviço porque o método GetQueuesAsync deve ser executado de forma assíncrona.
  • Um cliente OpenAI do Azure é registrado usando uma fábrica de clientes personalizada por meio do AddClient método, que fornece controle sobre como uma instância de cliente é criada. As fábricas de clientes personalizadas são úteis nos seguintes casos:
    • Você precisa usar outras dependências durante a construção do cliente.
    • Não existe um método de extensão de registro para o cliente de serviço que você deseja registrar.

Usar os clientes registrados

Com os clientes registrados, conforme descrito na seção Registrar clientes e subclientes, agora você pode usá-los. No exemplo a seguir, a injeção de construção é usada para obter o cliente de Armazenamento de Blobs e uma fábrica para obter subclientes do remetente do Barramento de Serviço em um controlador de API do ASP.NET Core:

[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();
    }
}

Armazenar configuração separadamente do código

Na seção Registrar clientes e subclientes, você passou explicitamente as variáveis de tipo Uri para os construtores do cliente. Essa abordagem pode causar problemas ao executar o código em diferentes ambientes durante o desenvolvimento e a produção. A equipe .NET sugere armazenar essas configurações em arquivos JSON dependentes do ambiente . Por exemplo, você pode ter um arquivo appsettings.Development.json contendo as configurações do ambiente de desenvolvimento. Outro arquivo appsettings.Production.json conteria as configurações do ambiente de produção e assim por diante. O formato do arquivo é:

{
  "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"
  }
}

Você pode adicionar todas as propriedades da classe ClientOptions ao arquivo JSON. As configurações no arquivo de configuração JSON podem ser recuperadas usando 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"));
});

No exemplo JSON anterior:

Configurar vários clientes de serviço com nomes diferentes

Imagine que você possui duas contas de armazenamento: uma para informações privadas e outra para informações públicas. Seu aplicativo transfere dados da conta de armazenamento pública para a privada após alguma operação. Você precisa ter dois clientes de serviço de armazenamento. Para diferenciar esses dois clientes, use o método de extensão WithName:

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

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

Usando um controlador de ASP.NET Core como exemplo, acesse o cliente de serviço nomeado usando a interface IAzureClientFactory<TClient>:

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");
    }
}

O cliente de serviço sem nome ainda está disponível da mesma maneira que antes. Os clientes nomeados são aditivos.

Configurar uma nova política de repetição

Em algum momento, talvez você deseje alterar as configurações padrão de um cliente de serviço. Por exemplo, talvez você queira usar configurações de repetição diferentes ou usar uma versão de API de serviço diferente. Você pode definir as configurações de repetição globalmente ou por serviço. Suponha que você tenha o seguinte arquivo appsettings.json em seu projeto ASP.NET Core:

{
  "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"
  }
}

Você pode alterar a política de repetição para atender às suas necessidades da seguinte forma:

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);
        });
});

Você também pode adicionar substituições de política de repetição no arquivo appsettings.json:

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

Confira também