0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Azure Functionsで設定ファイルのバリデーションをしつつDIする

Posted at

はじめに

動機

たとえば、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

実際の手順

  1. プロジェクトにNuGetパッケージを入れる
  2. Microsoft.Azure.Functions.Extensions 1.1.0
  3. Microsoft.Extensions.Options.DataAnnotations 3.1.18
  4. ReHackt.Extensions.Options.Validation 3.1.9
  5. 設定パラメタを保持するPOCOクラス「MyConfig」を作る
  6. 必須項目にバリデーション用のアノテーションをつける
  7. appsettings.jsonファイルを作る
  8. 出力フォルダにコピーされるように設定する。
  9. FunctionsStartupを継承したStartupクラスを作る
  10. appsettings.jsonからパラメタを読み込むように設定する
  11. 読み込んだ設定パラメタを「MyConfig」にバインドしてバリデーションする
  12. 「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.'.

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?