配置服务时如何通过依赖注入在 Azure Function V3 中注入或使用 IConfiguration
How to inject or use IConfiguration in Azure Function V3 with Dependency Injection when configuring a service
通常在 .NET Core 项目中,我会创建一个 'boostrap' class 来配置我的服务以及 DI 注册命令。这通常是 IServiceCollection
的扩展方法,我可以在其中调用 .AddCosmosDbService
之类的方法,所需的一切都是包含该方法的静态 class 中的 'self-contained'。关键是该方法从 Startup
class.
中获取 IConfiguration
我过去曾在 Azure Functions 中使用过 DI,但尚未遇到此特定要求。
当函数部署在 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?
}
}
显然在 Startup.cs
中为 IConfiguration
添加一个私有字段是行不通的,因为它需要填充一些东西,我也读过 using DI for IConfiguration
isn't a good idea.
我也试过使用 here 描述的选项模式并按如下方式实现:
builder.Services.AddOptions<CosmosDbClientSettings>()
.Configure<IConfiguration>((settings, configuration) => configuration.Bind(settings));
虽然这可以将 IOptions<CosmosDbClientSettings>
注入非静态 class,但我使用静态 class 来保存我的配置工作。
关于如何使这项工作或可能的解决方法有什么建议吗?我更愿意将所有配置保存在一个地方(bootstrap 文件)。
linked example 设计不佳(我认为)。它鼓励紧密耦合以及异步等待和阻塞调用的混合。
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 - Best Practices in Asynchronous Programming.
所以现在 Configure
可以正常调用了。
public class Startup : FunctionsStartup {
public override void Configure(IFunctionsHostBuilder builder) =>
builder.Services
.AddHttpClient()
.AddCosmosDbService();
}
这是我能够快速创建的示例;它与 Azure App Configuration 建立连接以进行集中配置和功能管理。应该能够使用所有 DI 功能,例如 IConfiguration
和 IOptions<T>
,就像在 ASP.NET 核心控制器中一样。
NuGet 依赖关系
Install-Package Microsoft.Azure.Functions.Extensions
Install-Package Microsoft.Extensions.Configuration.AzureAppConfiguration
Install-Package Microsoft.Extensions.Configuration.UserSecrets
Startup.cs
[assembly: FunctionsStartup(typeof(SomeApp.Startup))]
namespace SomeApp
{
public class Startup : FunctionsStartup
{
public IConfigurationRefresher ConfigurationRefresher { get; private set; }
public override void Configure(IFunctionsHostBuilder hostBuilder) {
if (ConfigurationRefresher is not null) {
hostBuilder.Services.AddSingleton(ConfigurationRefresher);
}
}
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder configurationBuilder) {
var hostBuilderContext = configurationBuilder.GetContext();
var isDevelopment = ("Development" == hostBuilderContext.EnvironmentName);
if (isDevelopment) {
configurationBuilder.ConfigurationBuilder
.AddJsonFile(Path.Combine(hostBuilderContext.ApplicationRootPath, $"appsettings.{hostBuilderContext.EnvironmentName}.json"), optional: true, reloadOnChange: false)
.AddUserSecrets<Startup>(optional: true, reloadOnChange: false);
}
var configuration = configurationBuilder.ConfigurationBuilder.Build();
var applicationConfigurationEndpoint = configuration["APPLICATIONCONFIGURATION_ENDPOINT"];
if (!string.IsNullOrEmpty(applicationConfigurationEndpoint)) {
configurationBuilder.ConfigurationBuilder.AddAzureAppConfiguration(appConfigOptions => {
var azureCredential = new DefaultAzureCredential(includeInteractiveCredentials: false);
appConfigOptions
.Connect(new Uri(applicationConfigurationEndpoint), azureCredential)
.ConfigureKeyVault(keyVaultOptions => {
keyVaultOptions.SetCredential(azureCredential);
})
.ConfigureRefresh(refreshOptions => {
refreshOptions.Register(key: "Application:ConfigurationVersion", label: LabelFilter.Null, refreshAll: true);
refreshOptions.SetCacheExpiration(TimeSpan.FromMinutes(3));
});
ConfigurationRefresher = appConfigOptions.GetRefresher();
});
}
}
}
}
从 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 的回答中描述的技巧。
我正在使用 .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 ***
}
目前推荐的方式
基于此处的文档 https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection
将设置绑定到自定义 class
您可以绑定 Azure 中功能设置的设置以及本地开发的 local.settings.json
文件,如下所示:
在 Portal 中设置密钥(注意密钥名称中的 :
符号)
并且可以选择在 local.settings.json
文件中:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"WebhookHandlerSettings:SecretKey": "AYBABTU"
}
}
自定义 class 设置:
public class WebhookHandlerSettings
{
public string SecretKey { get; set; }
}
使用以下代码添加启动 class 文件:
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>
参数中指定的 class。
您需要指定设置的 部分,然后是 :
和设置键 .
该框架会将键绑定到名称匹配的属性。
将设置注入服务 classes
通常我将服务注册组代码放入扩展方法中,如下所示:
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;
}
}
当你将注册的依赖项传递给你的函数classes时,你不需要将注入注册到函数class中,因为它会自动解决。
只需从函数 class 中删除 static
关键字,并添加一个具有您注册的依赖项的构造函数。
通常在 .NET Core 项目中,我会创建一个 'boostrap' class 来配置我的服务以及 DI 注册命令。这通常是 IServiceCollection
的扩展方法,我可以在其中调用 .AddCosmosDbService
之类的方法,所需的一切都是包含该方法的静态 class 中的 'self-contained'。关键是该方法从 Startup
class.
IConfiguration
我过去曾在 Azure Functions 中使用过 DI,但尚未遇到此特定要求。
当函数部署在 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?
}
}
显然在 Startup.cs
中为 IConfiguration
添加一个私有字段是行不通的,因为它需要填充一些东西,我也读过 using DI for IConfiguration
isn't a good idea.
我也试过使用 here 描述的选项模式并按如下方式实现:
builder.Services.AddOptions<CosmosDbClientSettings>()
.Configure<IConfiguration>((settings, configuration) => configuration.Bind(settings));
虽然这可以将 IOptions<CosmosDbClientSettings>
注入非静态 class,但我使用静态 class 来保存我的配置工作。
关于如何使这项工作或可能的解决方法有什么建议吗?我更愿意将所有配置保存在一个地方(bootstrap 文件)。
linked example 设计不佳(我认为)。它鼓励紧密耦合以及异步等待和阻塞调用的混合。
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 - Best Practices in Asynchronous Programming.
所以现在 Configure
可以正常调用了。
public class Startup : FunctionsStartup {
public override void Configure(IFunctionsHostBuilder builder) =>
builder.Services
.AddHttpClient()
.AddCosmosDbService();
}
这是我能够快速创建的示例;它与 Azure App Configuration 建立连接以进行集中配置和功能管理。应该能够使用所有 DI 功能,例如 IConfiguration
和 IOptions<T>
,就像在 ASP.NET 核心控制器中一样。
NuGet 依赖关系
Install-Package Microsoft.Azure.Functions.Extensions
Install-Package Microsoft.Extensions.Configuration.AzureAppConfiguration
Install-Package Microsoft.Extensions.Configuration.UserSecrets
Startup.cs
[assembly: FunctionsStartup(typeof(SomeApp.Startup))]
namespace SomeApp
{
public class Startup : FunctionsStartup
{
public IConfigurationRefresher ConfigurationRefresher { get; private set; }
public override void Configure(IFunctionsHostBuilder hostBuilder) {
if (ConfigurationRefresher is not null) {
hostBuilder.Services.AddSingleton(ConfigurationRefresher);
}
}
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder configurationBuilder) {
var hostBuilderContext = configurationBuilder.GetContext();
var isDevelopment = ("Development" == hostBuilderContext.EnvironmentName);
if (isDevelopment) {
configurationBuilder.ConfigurationBuilder
.AddJsonFile(Path.Combine(hostBuilderContext.ApplicationRootPath, $"appsettings.{hostBuilderContext.EnvironmentName}.json"), optional: true, reloadOnChange: false)
.AddUserSecrets<Startup>(optional: true, reloadOnChange: false);
}
var configuration = configurationBuilder.ConfigurationBuilder.Build();
var applicationConfigurationEndpoint = configuration["APPLICATIONCONFIGURATION_ENDPOINT"];
if (!string.IsNullOrEmpty(applicationConfigurationEndpoint)) {
configurationBuilder.ConfigurationBuilder.AddAzureAppConfiguration(appConfigOptions => {
var azureCredential = new DefaultAzureCredential(includeInteractiveCredentials: false);
appConfigOptions
.Connect(new Uri(applicationConfigurationEndpoint), azureCredential)
.ConfigureKeyVault(keyVaultOptions => {
keyVaultOptions.SetCredential(azureCredential);
})
.ConfigureRefresh(refreshOptions => {
refreshOptions.Register(key: "Application:ConfigurationVersion", label: LabelFilter.Null, refreshAll: true);
refreshOptions.SetCacheExpiration(TimeSpan.FromMinutes(3));
});
ConfigurationRefresher = appConfigOptions.GetRefresher();
});
}
}
}
}
从 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 的回答中描述的技巧。
我正在使用 .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 ***
}
目前推荐的方式
基于此处的文档 https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection
将设置绑定到自定义 class
您可以绑定 Azure 中功能设置的设置以及本地开发的 local.settings.json
文件,如下所示:
在 Portal 中设置密钥(注意密钥名称中的 :
符号)
并且可以选择在 local.settings.json
文件中:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"WebhookHandlerSettings:SecretKey": "AYBABTU"
}
}
自定义 class 设置:
public class WebhookHandlerSettings
{
public string SecretKey { get; set; }
}
使用以下代码添加启动 class 文件:
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>
参数中指定的 class。
您需要指定设置的 部分,然后是 :
和设置键 .
该框架会将键绑定到名称匹配的属性。
将设置注入服务 classes
通常我将服务注册组代码放入扩展方法中,如下所示:
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;
}
}
当你将注册的依赖项传递给你的函数classes时,你不需要将注入注册到函数class中,因为它会自动解决。
只需从函数 class 中删除 static
关键字,并添加一个具有您注册的依赖项的构造函数。