Как внедрить или использовать 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 ***
}