Как внедрить или использовать IConfiguration в функции Azure V3 с внедрением зависимостей при настройке службы

Обычно в проекте.NET Core я бы создал класс boostrap для настройки моей службы вместе с командами регистрации DI. Обычно это метод расширенияIServiceCollection где я могу вызвать такой метод, как .AddCosmosDbServiceи все необходимое является "автономным" в статическом классе, содержащем этот метод. Ключевым моментом является то, что метод получаетIConfiguration из Startup учебный класс.

Я работал с DI в функциях Azure в прошлом, но еще не сталкивался с этим конкретным требованием.

Я использую IConfiguration для привязки к конкретному классу со свойствами, соответствующими настройкам из обоих моих local.settings.json а также параметры приложения для разработки и производства при развертывании функции в 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?
    }
}

Очевидно добавление частного поля для IConfiguration в Startup.csне будет работать, так как его нужно чем-то заполнить, и я также читал это, используя DI дляIConfigurationне лучшая идея.

Я также пробовал использовать шаблон параметров, описанный здесь, и реализованный как таковой:

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

Хотя это сработает, чтобы ввести IOptions<CosmosDbClientSettings> к нестатическому классу я использую статический класс для хранения моей работы по настройке.

Любые предложения о том, как я могу сделать эту работу или возможное решение? Я бы предпочел хранить всю конфигурацию в одном месте (файл начальной загрузки).

5 ответов

Решение

Связанный пример плохо разработана (на мой взгляд). Он поощряет тесную связь и смешивание вызовов async-await и блокировки.

IConfiguration добавляется в коллекцию служб по умолчанию как часть запуска, поэтому я бы предложил изменить ваш дизайн, чтобы воспользоваться преимуществами отложенного разрешения зависимостей, чтобы IConfiguration можно решить с помощью встроенного IServiceProvider используя фабричный делегат.

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

Обратите внимание на подход, принятый, чтобы обойти необходимость использования async void в неасинхронном обработчике событий.

Ссылка Async/Await - Лучшие практики асинхронного программирования.

Итак, теперь Configure может быть правильно вызван.

public class Startup : FunctionsStartup {

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

Недавно выпущенная версия 1.1.0 Microsoft.Azure.Functions.Extensions позволяет вам делать следующее:

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var configuration = builder.GetContext().Configuration;
        builder.Services.AddCosmosDbService(configuration);
    }
}

К сожалению, он по-прежнему не поддерживает асинхронную конфигурацию, поэтому вам все равно придется заблокировать ожидание завершения задачи или использовать трюк, описанный @Nkosi

В настоящее время рекомендуемый способ

На основе документов здесь https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection

Привязка настроек к пользовательскому классу

Вы можете привязать параметры из параметров функции в Azure, а также файл для локальной разработки следующим образом:

Установите ключ в Portal ( обратите внимание на обозначение в имени ключа)

И, по желанию, в local.settings.json файл:

      {
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "WebhookHandlerSettings:SecretKey": "AYBABTU"
  }
}

Создайте собственный класс для настроек:

      public class WebhookHandlerSettings 
    {
        public string SecretKey { get; set; }
}

Добавьте файл класса Startup со следующим кодом:

      public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
            //bind the settings 
            builder.Services.AddOptions<WebhookHandlerSettings>()
            .Configure<IConfiguration>((settings, configuration) =>
            {
                configuration.GetSection(nameof(WebhookHandlerSettings)).Bind(settings);
            });
            //this is where we use the binded settings (by convention it's an extension method) 
            builder.Services.AddRequestValidation(); 
    }
}

Параметры привязаны к классу, который вы указываете в AddOptions<T>параметр. Вам нужно указать раздел настроек, затем :а затем ключ настроек .
Фреймворк привяжет ключи к свойствам, имена которых совпадают.

Внесите настройки в классы обслуживания

Обычно я помещаю код группы регистраций служб в методы расширения, как показано ниже:

          public static class RequestValidatorRegistration
    {
        public static void AddRequestValidation(this IServiceCollection services)
        {
            services.AddScoped<IWebhookRequestValidator>((s) =>
            {
#if DEBUG
                return new AlwaysPassRequestValidator(s.GetService<ILogger<AlwaysPassRequestValidator>>());
#endif
   //you can pass the built in ILogger<T> (**must be generic**), as well as your IOptions<T>

    return new WebhookRequestValidator(s.GetService<ILogger<WebhookRequestValidator>>(), 
        s.GetService<IOptions<WebhookHandlerSettings>>());

            });
        }
    }

Дополнительный совет - если вы пройдете встроенный логгер, вы не сможете пройти просто ILoggerкак вид услуги. Это должно быть ILogger<T>, иначе не решится.

Наконец, в вашем настраиваемом сервисе у вас есть зависимости, введенные в конструктор:

          public class WebhookRequestValidator : IWebhookRequestValidator
    {
        public WebhookRequestValidator(ILogger<WebhookRequestValidator> log, IOptions<WebhookHandlerSettings> settings)
        {
            this.log = log;
            this.settings = settings.Value;
        }
}

Когда вы передаете зарегистрированные зависимости своим функциональным классам, вам не нужно регистрировать инъекцию в функциональный класс , так как она будет разрешена автоматически.
Просто удалите static ключевое слово из класса функции и добавьте конструктор с зависимостями, которые вы зарегистрировали.

Вот пример, который мне удалось придумать; он устанавливает соединение с конфигурацией приложений Azure для централизованной настройки и управления функциями. Следует иметь возможность использовать все функции DI, такие какIConfiguration а также IOptions<T>так же, как и в контроллере ASP.NET Core.

Зависимости NuGet

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

Startup.cs

[assembly: FunctionsStartup(typeof(SomeApp.Startup))]

namespace SomeApp
{
    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 applicationConfigurationEndpoint = configurationRoot["APPCONFIGURATION_ENDPOINT"];

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

            if (!string.IsNullOrEmpty(applicationConfigurationEndpoint)) {
                configurationBuilder.AddAzureAppConfiguration(appConfigOptions => {
                    var azureCredential = new DefaultAzureCredential();

                    appConfigOptions
                        .Connect(new Uri(applicationConfigurationEndpoint), azureCredential)
                        .ConfigureKeyVault(keyVaultOptions => {
                            keyVaultOptions.SetCredential(azureCredential);
                        });

                    hostBuilder.Services.AddSingleton(appConfigOptions.GetRefresher());
                });
            }

            var configuration = configurationBuilder.Build();

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

Я использую .net core 3.1

      [assembly: FunctionsStartup(typeof(Startup))]
namespace xxxxx.Functions.Base
{
    [ExcludeFromCodeCoverage]
    public class Startup : FunctionsStartup
    {
        private static IConfiguration _configuration = null;

        public override void Configure(IFunctionsHostBuilder builder)
        {
            var serviceProvider = builder.Services.BuildServiceProvider();
            _configuration = serviceProvider.GetRequiredService<IConfiguration>();

            *** Now you can use _configuration["KEY"] in Startup.cs ***
        }
Другие вопросы по тегам