配置服务时如何通过依赖注入在 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 功能,例如 IConfigurationIOptions<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.0Microsoft.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 关键字,并添加一个具有您注册的依赖项的构造函数。