Como injetar ou usar a IConfiguration na Função V3 do Azure com Injeção de Dependência ao configurar um serviço

9

Normalmente, em um projeto .NET Core, eu criaria uma classe 'boostrap' para configurar meu serviço junto com os comandos de registro DI. Normalmente, esse é um método de extensão no IServiceCollectionqual eu posso chamar um método .AddCosmosDbServicee tudo o que é necessário é 'independente' na classe estática que contém esse método. A chave, porém, é que o método obtém um IConfigurationda Startupclasse.

Eu trabalhei com DI no Azure Functions no passado, mas ainda não encontrei esse requisito específico.

Estou usando o IConfigurationpara vincular a uma classe concreta com propriedades que correspondem às configurações do meu local.settings.jsone do aplicativo de desenvolvimento / produção quando a Função é implantada no Azure.

CosmosDbClientSettings.cs

/// <summary>
/// Holds configuration settings from local.settings.json or application configuration
/// </summary>    
public class CosmosDbClientSettings
{
    public string CosmosDbDatabaseName { get; set; }
    public string CosmosDbCollectionName { get; set; }
    public string CosmosDbAccount { get; set; }
    public string CosmosDbKey { get; set; }
}

BootstrapCosmosDbClient.cs

public static class BootstrapCosmosDbClient
{
    /// <summary>
    /// Adds a singleton reference for the CosmosDbService with settings obtained by injecting IConfiguration
    /// </summary>
    /// <param name="services"></param>
    /// <param name="configuration"></param>
    /// <returns></returns>
    public static async Task<CosmosDbService> AddCosmosDbServiceAsync(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        CosmosDbClientSettings cosmosDbClientSettings = new CosmosDbClientSettings();
        configuration.Bind(nameof(CosmosDbClientSettings), cosmosDbClientSettings);

        CosmosClientBuilder clientBuilder = new CosmosClientBuilder(cosmosDbClientSettings.CosmosDbAccount, cosmosDbClientSettings.CosmosDbKey);
        CosmosClient client = clientBuilder.WithConnectionModeDirect().Build();
        CosmosDbService cosmosDbService = new CosmosDbService(client, cosmosDbClientSettings.CosmosDbDatabaseName, cosmosDbClientSettings.CosmosDbCollectionName);
        DatabaseResponse database = await client.CreateDatabaseIfNotExistsAsync(cosmosDbClientSettings.CosmosDbDatabaseName);
        await database.Database.CreateContainerIfNotExistsAsync(cosmosDbClientSettings.CosmosDbCollectionName, "/id");

        services.AddSingleton<ICosmosDbService>(cosmosDbService);

        return cosmosDbService;
    }
}

Startup.cs

public class Startup : FunctionsStartup
{

    public override async void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddHttpClient();
        await builder.Services.AddCosmosDbServiceAsync(**need IConfiguration reference**); <--where do I get IConfiguration?
    }
}

Obviamente, adicionar um campo privado para IConfigurationin Startup.csnão funcionará, pois precisa ser preenchido com algo e também li que usar DI para IConfigurationnão é uma boa ideia .

Também tentei usar o padrão de opções descrito aqui e implementado da seguinte forma:

builder.Services.AddOptions<CosmosDbClientSettings>()
    .Configure<IConfiguration>((settings, configuration) => configuration.Bind(settings));

Enquanto isso funcionaria para injetar um IOptions<CosmosDbClientSettings>em uma classe não estática, estou usando uma classe estática para manter meu trabalho de configuração.

Alguma sugestão sobre como posso fazer isso funcionar ou uma possível solução alternativa? Eu preferiria manter toda a configuração em um só lugar (arquivo de inicialização).

Jason Shave
fonte

Respostas:

5

O exemplo vinculado é mal projetado (na minha opinião). Ele incentiva o acoplamento rígido e a mistura de chamadas assíncronas em espera e bloqueadas.

IConfigurationé adicionado à coleção de serviços por padrão como parte da inicialização, portanto, sugiro que você altere seu design para aproveitar a resolução adiada de dependências, para que IConfigurationpossa ser resolvido via construído IServiceProviderusando um representante da fábrica.

public static class BootstrapCosmosDbClient {

    private static event EventHandler initializeDatabase = delegate { };

    public static IServiceCollection AddCosmosDbService(this IServiceCollection services) {

        Func<IServiceProvider, ICosmosDbService> factory = (sp) => {
            //resolve configuration
            IConfiguration configuration = sp.GetService<IConfiguration>();
            //and get the configured settings (Microsoft.Extensions.Configuration.Binder.dll)
            CosmosDbClientSettings cosmosDbClientSettings = configuration.Get<CosmosDbClientSettings>();
            string databaseName = cosmosDbClientSettings.CosmosDbDatabaseName;
            string containerName = cosmosDbClientSettings.CosmosDbCollectionName;
            string account = cosmosDbClientSettings.CosmosDbAccount;
            string key = cosmosDbClientSettings.CosmosDbKey;

            CosmosClientBuilder clientBuilder = new CosmosClientBuilder(account, key);
            CosmosClient client = clientBuilder.WithConnectionModeDirect().Build();
            CosmosDbService cosmosDbService = new CosmosDbService(client, databaseName, containerName);

            //async event handler
            EventHandler handler = null;
            handler = async (sender, args) => {
                initializeDatabase -= handler; //unsubscribe
                DatabaseResponse database = await client.CreateDatabaseIfNotExistsAsync(databaseName);
                await database.Database.CreateContainerIfNotExistsAsync(containerName, "/id");
            };
            initializeDatabase += handler; //subscribe
            initializeDatabase(null, EventArgs.Empty); //raise the event to initialize db

            return cosmosDbService;
        };
        services.AddSingleton<ICosmosDbService>(factory);
        return service;
    }
}

Observe a abordagem adotada para contornar a necessidade de usar async voidem um manipulador de eventos não assíncronos.

Referência Async / Await - Práticas recomendadas em programação assíncrona .

Então agora o Configurepode ser chamado corretamente.

public class Startup : FunctionsStartup {

    public override void Configure(IFunctionsHostBuilder builder) =>
        builder.Services
            .AddHttpClient()
            .AddCosmosDbService();
}
Nkosi
fonte
4

Aqui está um exemplo que eu pude criar; ele estabelece uma conexão com a Configuração de Aplicativos do Azure para gerenciamento centralizado de configurações e recursos. Deve-se poder usar todos os recursos de DI, como IConfiguratione IOptions<T>, exatamente como usariam em um controlador ASP.NET Core.

Dependências do NuGet

  • Install-Package Microsoft.Azure.Functions.Extensions
  • Install-Package Microsoft.Extensions.Configuration.AzureAppConfiguration

Startup.cs

[assembly: FunctionsStartup(typeof(Startup))]

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder hostBuilder) {
        var serviceProvider = hostBuilder.Services.BuildServiceProvider();
        var configurationRoot = serviceProvider.GetService<IConfiguration>();
        var configurationBuilder = new ConfigurationBuilder();
        var appConfigEndpoint = configuration["AppConfigEndpoint"];

        if (configurationRoot is IConfigurationRoot) {
            configurationBuilder.AddConfiguration(configurationRoot);
        }

        if (!string.IsNullOrEmpty(appConfigEndpoint)) {
            configurationBuilder.AddAzureAppConfiguration(appConfigOptions => {
                // possible to run this locally if refactored to use ClientSecretCredential or DefaultAzureCredential
                appConfigOptions.Connect(new Uri(appConfigEndpoint), new ManagedIdentityCredential());
            });
        }

        var configuration = configurationBuilder.Build();

        hostBuilder.Services.Replace(ServiceDescriptor.Singleton(typeof(IConfiguration), configuration));

        // Do more stuff with Configuration here...
    }
}

public sealed class HelloFunction
{
    private IConfiguration Configuration { get; }

    public HelloFunction(IConfiguration configuration) {
        Configuration = configuration;
    }

    [FunctionName("HelloFunction")]
    public void Run([TimerTrigger("0 */1 * * * *")]TimerInfo myTimer, ILogger log) {
        log.LogInformation($"Timer Trigger Fired: 'Hello {Configuration["Message"]}!'");
    }
}
Kittoes0124
fonte
Com esta abordagem, tenho um problema em que os host.jsonparâmetros não são usados, em particular,routePrefix
Andrii
11
@Andrii Interessante, vou ter que fazer uma pesquisa e editar meu post se uma solução for encontrada; muito obrigado pela atenção!
Kittoes0124