Comment injecter ou utiliser IConfiguration dans Azure Function V3 avec injection de dépendance lors de la configuration d'un service

9

Normalement, dans un projet .NET Core, je créais une classe «boostrap» pour configurer mon service avec les commandes d'enregistrement DI. C'est généralement une méthode d'extension d' IServiceCollectionoù je peux appeler une méthode comme .AddCosmosDbServiceet tout ce qui est nécessaire est «autonome» dans la classe statique contenant cette méthode. La clé est que la méthode obtient un IConfigurationde la Startupclasse.

J'ai déjà travaillé avec DI dans Azure Functions, mais je n'ai pas encore rencontré cette exigence spécifique.

J'utilise le IConfigurationpour lier à une classe concrète avec des propriétés correspondant aux paramètres de mon local.settings.jsonainsi qu'aux paramètres de l'application de développement / production lorsque la fonction est déployée dans 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?
    }
}

Évidemment, l'ajout d'un champ privé pour IConfigurationin Startup.csne fonctionnera pas car il doit être rempli avec quelque chose et j'ai également lu que l' utilisation de DI for IConfigurationn'est pas une bonne idée .

J'ai également essayé d'utiliser le modèle d'options décrit ici et mis en œuvre en tant que tel:

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

Bien que cela fonctionne pour injecter un IOptions<CosmosDbClientSettings>dans une classe non statique, j'utilise une classe statique pour conserver mon travail de configuration.

Des suggestions sur la façon dont je peux faire ce travail ou une solution possible? Je préfère garder toute la configuration en un seul endroit (fichier bootstrap).

Jason Shave
la source

Réponses:

5

L' exemple lié est mal conçu (à mon avis). Il encourage le couplage étroit et le mélange d'appels en attente et de blocage asynchrones.

IConfigurationest ajouté à la collection de services par défaut dans le cadre du démarrage, je suggère donc de modifier votre conception pour tirer parti de la résolution différée des dépendances afin de IConfigurationpouvoir les résoudre via la construction à l' IServiceProvideraide d'un délégué d'usine.

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

Notez l'approche adoptée pour contourner la nécessité d'utiliser async voiddans un gestionnaire d'événements non asynchrone.

Référence Async / Await - Meilleures pratiques en programmation asynchrone .

Alors maintenant, le Configurepeut être correctement invoqué.

public class Startup : FunctionsStartup {

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

Voici un exemple que j'ai pu concocter; il établit une connexion à Azure App Configuration pour la configuration centralisée et la gestion des fonctionnalités. On devrait pouvoir utiliser toutes les fonctionnalités DI, telles que IConfigurationet IOptions<T>, tout comme elles le feraient dans un contrôleur ASP.NET Core.

Dépendances 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
la source
Avec cette approche, j'ai un problème que les host.jsonparamètres ne sont pas utilisés, en particulier,routePrefix
Andrii Il y a
1
@Andrii Intéressant, je devrai faire des recherches et éditerai mon article si une solution est trouvée; merci une tonne pour la tête!
Kittoes0124 Il y a