はじめに
動機
たとえば、Azure FunctionsでRedisを使うとき、DI(DependencyInjection)を使いたい。
DIを使うには設定を読み込んでインスタンス作ってDIサービスに登録する必要がある。
そこまではいいんだけど、設定が欠落していても起動時にわからず、実際にAPI叩くまでわからん。
すごく困る。
ということで、アプリを起動した時点である程度設定エラーが検出できて、DIする方法を見つけてきて実装してみました。
やりたいこと
- AzureFunctionsでDependencyInjectionを使う
- DependencyInjectionで注入するオブジェクトの構築に設定情報を使う
- 設定パラメタをappsettings.jsonからPOCOへ読み込む
- 設定パラメタを保持するPOCOにアノテーションをつけてバリデーションを行う
- 設定パラメタを保持するPOCOのバリデーションは再帰的に行いたい
ここで上がってるIssueとほぼ同じ事がやりたかった。
IssueにリンクしてるPullRequestがマージされたっぽいので、そのうち公式サポートされるのかも。
環境
- Azure Functions v3
- .NET Core 3.1
実際の手順
- プロジェクトにNuGetパッケージを入れる
- Microsoft.Azure.Functions.Extensions 1.1.0
- Microsoft.Extensions.Options.DataAnnotations 3.1.18
- ReHackt.Extensions.Options.Validation 3.1.9
- 設定パラメタを保持するPOCOクラス「MyConfig」を作る
- 必須項目にバリデーション用のアノテーションをつける
- appsettings.jsonファイルを作る
- 出力フォルダにコピーされるように設定する。
- FunctionsStartupを継承したStartupクラスを作る
- appsettings.jsonからパラメタを読み込むように設定する
- 読み込んだ設定パラメタを「MyConfig」にバインドしてバリデーションする
- 「MyConfig」をAzureFunctionsで利用する
プロジェクトにNuGetパッケージを入れる
パッケージ名 | バージョン | 用途 |
---|---|---|
Microsoft.Azure.Functions.Extensions | 1.1.0 | DependencyInjectionに必要 |
Microsoft.Extensions.Options.DataAnnotations | 3.1.18 | 設定パラメタPOCOのアノテーションとバリデーションに必要 |
ReHackt.Extensions.Options.Validation | 3.1.9 | 設定パラメタPOCOの再帰的なバリデーションに必要 |
設定情報を保持するPOCOクラス「MyConfig」を作る
MyConfig.cs
using System.ComponentModel.DataAnnotations;
namespace DISample1.FunctionApp.Config
{
public class MyConfig
{
[Required]
public string Message { get; set; }
[Required]
public MyRedisOptions MyRedisOptions { get; set; }
}
public class MyRedisOptions
{
[Required]
public string Host { get; set; }
[Range(1024,60000)]
public int Port { get; set; }
[Required]
public string AuthPassword { get; set; }
}
}
appsettings.jsonを作る
appsettings.json
{
"MyConfig":{
"Message": "This is appsetting.json Message",
"MyRedisOptions": {
"Host":"localhost",
"Port": "6379",
"AuthPassword": "password"
}
}
}
appsettings.jsonを出力フォルダへコピーされるようにする
.csprojファイルを編集して、以下のようにする。
csprojファイル
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
FunctionsStartupを継承したStartupクラスを作る
Startup.cs
using System.IO;
using DISample1.FunctionApp;
using DISample1.FunctionApp.Config;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
[assembly: FunctionsStartup(typeof(Startup))]
namespace DISample1.FunctionApp
{
public class Startup : FunctionsStartup
{
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
FunctionsHostBuilderContext context = builder.GetContext();
builder.ConfigurationBuilder
.AddJsonFile(Path.Combine(context.ApplicationRootPath, "appsettings.json"), optional: true, reloadOnChange: false)
.AddJsonFile(Path.Combine(context.ApplicationRootPath, $"appsettings.{context.EnvironmentName}.json"), optional: true, reloadOnChange: false)
.AddEnvironmentVariables();
}
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddOptions<MyConfig>()
.Bind(builder.GetContext().Configuration.GetSection("MyConfig"))
.ValidateDataAnnotationsRecursively()
.ValidateEagerly();
// ここでバリデーションを発生させる
var options = builder.Services.BuildServiceProvider().GetRequiredService<IOptions<MyConfig>>();
var aaa = options.Value;
}
}
}
appsettings.jsonからパラメタを読み込むように設定する
これはStartup.csのConfigureAppConfigurationメソッドでやっている。
公式ドキュメント:構成ソースのカスタマイズ
Startup.csのConfigureAppConfigurationメソッド
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
FunctionsHostBuilderContext context = builder.GetContext();
builder.ConfigurationBuilder
.AddJsonFile(Path.Combine(context.ApplicationRootPath, "appsettings.json"), optional: true, reloadOnChange: false)
.AddJsonFile(Path.Combine(context.ApplicationRootPath, $"appsettings.{context.EnvironmentName}.json"), optional: true, reloadOnChange: false)
.AddEnvironmentVariables();
}
読み込んだ設定パラメタを「MyConfig」にバインドしてバリデーションする
これはStartup.csのConfigureメソッドでやっている。
Startup.csのConfigureメソッド
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddOptions<MyConfig>()
.Bind(builder.GetContext().Configuration.GetSection("MyConfig"))
.ValidateDataAnnotationsRecursively()
.ValidateEagerly();
// ここでバリデーションを発生させる
var options = builder.Services.BuildServiceProvider().GetRequiredService<IOptions<MyConfig>>();
var aaa = options.Value;
}
「MyConfig」をAzureFunctionsで利用する
コンストラクタでDependencyInjectionしている。
ShowConfig.cs
using System.Threading.Tasks;
using DISample1.FunctionApp.Config;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace DISample1.FunctionApp
{
public class ShowConfig
{
private readonly MyConfig _myConfig;
public ShowConfig(IOptions<MyConfig> options)
{
_myConfig = options.Value;
}
[FunctionName("ShowConfig")]
public async Task<IActionResult> RunAsync(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req, ILogger log)
{
return (ActionResult)new OkObjectResult($"Hello, {_myConfig.Message}\nRedis:{_myConfig.MyRedisOptions.Host}:{_myConfig.MyRedisOptions.Port}");
}
}
}
バリデーションに失敗したときのコンソール出力
プロセス自体は止まらないけど、
とりあえずローカル環境で動かす分にはエラーがわかるので、これでOKとする
バリデーション失敗時のコンソール出力
[2021-08-28T14:16:29.058Z] A host error has occurred during startup operation '29971fab-979d-4125-8c12-2c314b89a35a'.
[2021-08-28T14:16:29.058Z] Microsoft.Extensions.Options: DataAnnotation validation failed for members Message with the error 'The Message field is required.'.
[2021-08-28T14:16:29.058Z] DataAnnotation validation failed for members MyRedisOptions.Host with the error 'The Host field is required.'.
[2021-08-28T14:16:29.058Z] DataAnnotation validation failed for members MyRedisOptions.Port with the error 'The field Port must be between 1024 and 60000.'.
Value cannot be null. (Parameter 'provider')
Press any to continue....[2021-08-28T14:16:30.100Z] A host error has occurred during startup operation '3e379b87-8928-4662-9b52-9f6922766a2f'.
[2021-08-28T14:16:30.100Z] Microsoft.Extensions.Options: DataAnnotation validation failed for members Message with the error 'The Message field is required.'.
[2021-08-28T14:16:30.100Z] DataAnnotation validation failed for members MyRedisOptions.Host with the error 'The Host field is required.'.
[2021-08-28T14:16:30.100Z] DataAnnotation validation failed for members MyRedisOptions.Port with the error 'The field Port must be between 1024 and 60000.'.
[2021-08-28T14:16:32.129Z] A host error has occurred during startup operation '4ef938b9-5434-4373-8d92-deb93067ce42'.
[2021-08-28T14:16:32.129Z] Microsoft.Extensions.Options: DataAnnotation validation failed for members Message with the error 'The Message field is required.'.
[2021-08-28T14:16:32.129Z] DataAnnotation validation failed for members MyRedisOptions.Host with the error 'The Host field is required.'.
[2021-08-28T14:16:32.129Z] DataAnnotation validation failed for members MyRedisOptions.Port with the error 'The field Port must be between 1024 and 60000.'.
参考資料
- DependencyInjection関係
- appsettins.jsonを読み込む
- アノテーションとバリデーション関係
- 公式ドキュメント:オプションの検証
- しばやん雑記:Azure Functions に Options のバリデーションを追加する
- Qiita:ASP.NET Core の IStartupFilter でアプリの起動時に設定を検証する
- Andrew Lock | .NET Escapades:Adding validation to strongly typed configuration objects in ASP.NET Core
- KeesTalksTech:Validate strongly typed options when using config sections
- stackoverflow:Validation of ASP.NET Core options during startup