しばやん雑記

Azure Serverless とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Functions に Options のバリデーションを追加する

ASP.NET Core で導入された Options パターンを使ったアプリケーション設定ですが、IConfiguration と DI を導入すれば使えるので最近は Azure Functions の Trigger に関係ない部分で使っています。

Azure Functions では IConfiguration を簡単に取れないので、自前で用意する必要はありますが ASP.NET Core とほぼ同じ感覚で使えます。何回かエントリを書いているので参照してください。

Trigger に関係する設定は Function Runtime というか Scale Controller が読み取り可能な場所に書かないといけないので、IConfiguration を使って JSON などから読み込んでも無視されます。今のアーキテクチャでは解決は不可能かもしれません。

そして今回の本題ですが Options にはバリデーションを追加することが出来ます。使い慣れた Data Annotations をプロパティに付けるだけなので簡単ですし、複雑なバリデーションも実装出来るようです。

ASP.NET Core では ValidateDataAnnotations を呼び出すだけで、属性ベースでの Options のバリデーションが有効になります。タイミングは IOptions<T>.Value を参照し、インスタンスが作成された直後です。

Azure Functions の場合は Microsoft.AspNetCore.App が暗黙的に参照されているわけではないので、ドキュメントにある通り追加の NuGet パッケージをインストールする必要があります。

実際に Azure Functions で Options パターンを使いつつ、Options 値のバリデーションを有効にするには、以下のような Startup クラスを用意すれば良いです。

バリデーションを行う場合は Configure<TOptions> が使えないのが少し手間です。

public class Startup : FunctionsStartup
{
    public Startup()
    {
        var config = new ConfigurationBuilder()
            .AddEnvironmentVariables();

        Configuration = config.Build();
    }

    public IConfiguration Configuration { get; }

    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.Replace(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));

        builder.Services.AddOptions<SampleOptions>()
               .Bind(Configuration.GetSection("Sample"))
               .ValidateDataAnnotations();
    }
}

public class SampleOptions
{
    [Required]
    public string Value { get; set; }
}

public class Function1
{
    public Function1(IOptions<SampleOptions> options)
    {
        _options = options.Value;
    }

    private readonly SampleOptions _options;

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req,
        ILogger log)
    {
        return new OkResult();
    }
}

IOptionsFactory<> をわざわざ標準の実装に入れ替えているのは、Azure Functions が提供している実装が雑でバリデーションが動作しないものになっているからです。

入れ替えたとしても違いは Options の値をログに書き出すかどうかなので、正直無くても良いです。入れ替えずに実行すると、以下のように SampleOptions.Valuenull でも問題なく通ってしまいます。

f:id:shiba-yan:20200619231849p:plain

標準の実装に入れ替えることで IOptions<T>.Value を参照した瞬間に、バリデーションが正しく行われて OptionsValidationException が投げられていることが確認できます。

f:id:shiba-yan:20200619231900p:plain

この件については Issue を一応上げていますが、これまでの傾向から行くと修正される可能性は低そうです。なので標準の実装に入れ替えてしまうのが一番スマートでしょう。

ちょいちょい接続文字列を設定するのを忘れて実行時エラーというのを経験しているので、最低でも必須の値には付けておくのが良いと感じました。特に OSS で配布している場合は必須だと実感しました。