Come iniettare o usare IConfiguration in Azure Function V3 con Dependency Injection durante la configurazione di un servizio


9

Normalmente in un progetto .NET Core avrei creato una classe 'boostrap' per configurare il mio servizio insieme ai comandi di registrazione DI. Questo è di solito un metodo di estensione in IServiceCollectioncui posso chiamare un metodo simile .AddCosmosDbServicee tutto il necessario è "autonomo" nella classe statica contenente quel metodo. La chiave però è che il metodo ottiene un IConfigurationdalStartup classe.

Ho lavorato con DI in Funzioni di Azure in passato ma non ho ancora incontrato questo requisito specifico.

Sto usando il IConfigurationper legare a una classe concreta con proprietà che corrispondono alle impostazioni di entrambi i mieilocal.settings.json che delle impostazioni di sviluppo / produzione dell'applicazione quando la funzione viene distribuita in 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?
    }
}

Ovviamente aggiungere un campo privato per IConfigurationin Startup.csnon funzionerà in quanto deve essere popolato con qualcosa e ho anche letto che usare DI per IConfigurationnon è una buona idea .

Ho anche provato a utilizzare il modello di opzioni come descritto qui e implementato come tale:

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

Mentre questo funzionerebbe per iniettare una IOptions<CosmosDbClientSettings>classe non statica, sto usando una classe statica per conservare il mio lavoro di configurazione.

Qualche suggerimento su come posso farlo funzionare o una possibile soluzione? Preferirei mantenere tutta la configurazione in un unico posto (file bootstrap).

Risposte:


5

L' esempio collegato è mal progettato (secondo il mio parere). Incoraggia l'accoppiamento stretto e la miscelazione di chiamate async-waitit e di blocco.

IConfigurationviene aggiunto alla raccolta servizi per impostazione predefinita come parte dell'avvio, quindi suggerirei di modificare il progetto per sfruttare la risoluzione differita delle dipendenze in modo che IConfigurationpossa essere risolto tramite la creazione IServiceProviderutilizzando un delegato di fabbrica.

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

Nota l'approccio adottato per aggirare il dover usare async void in un gestore di eventi non asincrono.

Riferimento Async / Await - Best Practices in Asynchronous Programming .

Quindi ora è Configurepossibile richiamare correttamente.

public class Startup : FunctionsStartup {

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

4

Ecco un esempio che sono stato in grado di montare; stabilisce una connessione alla configurazione dell'app di Azure per la configurazione centralizzata e la gestione delle funzionalità. Uno dovrebbe essere in grado di utilizzare tutte le funzionalità DI, come IConfiguratione IOptions<T>, proprio come farebbero in un controller ASP.NET Core.

Dipendenze 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"]}!'");
    }
}

Con questo approccio, ho il problema che i host.jsonparametri non vengono utilizzati, in particolare,routePrefix
Andrii

1
@Andrii Interessante, dovrò fare qualche ricerca e modificherò il mio post se viene trovata una soluzione; grazie mille per l'heads up!
Kittoes0124
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.