配置服务时如何通过依赖注入在 Azure Function V3 中注入或使用 IConfiguration

Posted

技术标签:

【中文标题】配置服务时如何通过依赖注入在 Azure Function V3 中注入或使用 IConfiguration【英文标题】:How to inject or use IConfiguration in Azure Function V3 with Dependency Injection when configuring a service 【发布时间】:2020-04-15 20:22:45 【问题描述】:

通常在 .NET Core 项目中,我会创建一个“boostrap”类来配置我的服务以及 DI 注册命令。这通常是IServiceCollection 的扩展方法,我可以在其中调用.AddCosmosDbService 之类的方法,并且在包含该方法的静态类中,所有必要的东西都是“自包含的”。但关键是该方法从 Startup 类中获取了一个 IConfiguration

我过去曾在 Azure Functions 中使用 DI,但尚未遇到此特定要求。

当函数部署在 Azure 中时,我正在使用 IConfiguration 绑定到具有与我的 local.settings.json 以及开发/生产应用程序设置匹配的属性设置的具体类。

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&lt;CosmosDbClientSettings&gt; 注入非静态类,但我使用静态类来保存我的配置工作。

关于如何使这项工作或可能的解决方法有什么建议吗?我希望将所有配置保存在一个地方(引导文件)。

【问题讨论】:

看看@Casper 的回答。这不是最合适的吗? 【参考方案1】:

从Microsoft.Azure.Functions.Extensions 的1.1.0 版开始,您可以执行以下操作:

public class Startup : FunctionsStartup

    public override void Configure(IFunctionsHostBuilder builder)
    
        var configuration = builder.GetContext().Configuration;
        builder.Services.AddCosmosDbService(configuration);
    

不幸的是,它仍然不支持异步配置,因此您仍然必须阻止等待任务完成或使用@Nkosi 回答中描述的技巧。

【讨论】:

GetContext() 方法是在另一个项目或程序集中定义的扩展方法吗? 否,但它是在包的 1.1.0 版本中引入的,因此如果使用旧版本,则必须升级。【参考方案2】:

linked example 设计不佳(在我看来)。它鼓励紧密耦合以及 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 - Best Practices in Asynchronous Programming。

所以现在Configure 可以正常调用了。

public class Startup : FunctionsStartup 

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

【讨论】:

工厂理念完美!现在我不再需要手动构建 ServiceProvider 了!顺便说一句,我在AddDbContext((sp,options)=&gt;) 中使用它。【参考方案3】:

这是一个我能够制作的例子;它建立到Azure App Configuration 的连接以进行集中配置和功能管理。应该能够使用所有 DI 功能,例如 IConfigurationIOptions&lt;T&gt;,就像在 ASP.NET Core 控制器中一样。

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();
                );
            
        
    

【讨论】:

使用这种方法,我有一个问题是host.json参数没有被使用,特别是routePrefix @Andrii 有趣的是,我必须做一些研究,如果找到解决方案,我会编辑我的帖子;非常感谢您的提醒! @Andrii 尝试最新的编辑,让我知道您的问题是否仍然存在;抱歉花了这么长时间。 ??? 很好用! @Kittoes0124 我想知道我的问题是否与库版本有关...您介意分享您正在使用的版本号吗?【参考方案4】:

我正在使用 .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 ***
        

【讨论】:

这不是推荐的做法了... 没关系。我只是想确保来这里寻找问题答案的开发人员知道这个答案不是推荐的答案。 @Casper 那么推荐的是什么? @PawelCioch 请参阅上面的答案。 @Casper 我明白了,我对 GetContext() 有与 F. 提到的相同的错误,将尝试更新包,希望在部署到 Azure 后不会出现任何问题 :) 【参考方案5】:

目前推荐的方式

基于此处的文档https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection

将设置绑定到自定义类

您可以从 Azure 中的 Function settings 以及 local.settings.json 文件中绑定设置以进行本地开发,如下所示:

在 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&lt;T&gt; 参数中指定的类。 您需要指定 设置部分,然后是 :,然后是设置键。 框架会将键绑定到名称匹配的属性。

将设置注入服务类

通常我将服务注册组代码放入扩展方法中,如下所示:

    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&lt;T&gt;,否则无法解析。

最后,在您的自定义服务中,您将依赖项注入到构造函数中:

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

当您将注册的依赖项传递给您的函数类时,您不需要将注入注册到函数类中,因为它会自动解析。 只需从函数类中删除 static 关键字,然后添加一个包含您注册的依赖项的构造函数。

【讨论】:

当您可以简单地执行 services.AddScoped&lt;IWebhookRequestValidator, WebhookRequestValidator&gt;() 时,为什么要使用工厂方法来注册 IWebhookValidator(或者无论语法是什么,我使用的是 SimpleInjector)? @Casper - 好问题 - 我选择使用工厂方法,因为我的完整代码涉及为#DEBUG 模式提供不同的验证器实现 - 请参阅更新的代码:) 这也可以通过使用正常注册的 else 语句将整个注册包装在调试块中来处理? @IanKemp,好吧,我不同意。我的回答对这种模式提供了更全面的解释(尽管众所周知)。请记住,*** 不仅是为了回答 OP 的问题(因为他肯定不再关心),而且还回答任何其他谷歌用户的问题,他们正在寻找“如何在 Azure Functions 中进行 DI”并找到这个页。并且我相信我的回答是有用且正确的。问候:)

以上是关于配置服务时如何通过依赖注入在 Azure Function V3 中注入或使用 IConfiguration的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Azure TableController 中注入公共服务依赖项

Spring 依赖注入原理

Azure webjobs - Unity - 如何将范围内的依赖项注入其他类

在 asp.net Core 中配置服务注册时的依赖注入访问 (3+)

如何通过azure设备配置服务从azure功能向iot设备发送自定义错误消息?

如何从嵌套类通过依赖注入服务进行访问?