Cómo inyectar o usar IConfiguration en Azure Function V3 con Dependency Injection al configurar un servicio

9

Normalmente en un proyecto .NET Core, crearía una clase 'boostrap' para configurar mi servicio junto con los comandos de registro DI. Este suele ser un método de extensión de IServiceCollectiondonde puedo llamar a un método como .AddCosmosDbServicey todo lo necesario es 'autocontenido' en la clase estática que contiene ese método. Sin embargo, la clave es que el método obtiene un IConfigurationde la Startupclase.

He trabajado con DI en Azure Functions en el pasado, pero todavía no me he encontrado con este requisito específico.

Estoy usando el IConfigurationenlace para una clase concreta con propiedades que coinciden con la configuración de mi local.settings.jsony la configuración de la aplicación de desarrollo / producción cuando la función se implementa en 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, agregar un campo privado para IConfigurationin Startup.csno funcionará, ya que debe rellenarse con algo y también he leído que usar DI para IConfigurationno es una buena idea .

También intenté usar el patrón de opciones como se describe aquí y lo implementé como tal:

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

Si bien esto funcionaría para inyectar una IOptions<CosmosDbClientSettings>clase no estática, estoy usando una clase estática para mantener mi trabajo de configuración.

¿Alguna sugerencia sobre cómo puedo hacer que esto funcione o una posible solución? Prefiero mantener toda la configuración en un solo lugar (archivo de arranque).

Jason Shave
fuente

Respuestas:

5

El ejemplo vinculado está mal diseñado (en mi opinión). Fomenta el acoplamiento estrecho y la mezcla de llamadas asincrónicas y bloqueo de llamadas.

IConfigurationse agrega a la colección de servicios de forma predeterminada como parte del inicio, por lo que sugeriría cambiar su diseño para aprovechar la resolución diferida de las dependencias para que IConfigurationpueda resolverse a través de la construcción IServiceProviderutilizando un delegado de 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;
    }
}

Tenga en cuenta el enfoque adoptado para evitar tener que usar async voiden un controlador de eventos no asíncrono.

Referencia asíncrona / espera: mejores prácticas en programación asincrónica .

Entonces ahora Configurese puede invocar correctamente.

public class Startup : FunctionsStartup {

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

Aquí hay un ejemplo que pude preparar; establece una conexión con la Configuración de la aplicación de Azure para la configuración centralizada y la administración de funciones. Uno debería poder usar todas las funciones DI, como IConfigurationy IOptions<T>, tal como lo harían en un controlador ASP.NET Core.

Dependencias de 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
fuente
Con este enfoque, tengo un problema que los host.jsonparámetros no se utilizan, en particular,routePrefix
Andrii hace
1
@Andrii Interesante, tendré que investigar un poco y editaré mi publicación si se encuentra una solución; muchas gracias por el aviso!
Kittoes0124