ツナ缶雑記

ぐうたらSEのブログです。主にマイクロソフト系技術を中心に扱います。たまに技術と関係ないことも書きます。

.NET 8 でのアプリケーション設定の実装方式と設定値の検証方法

本稿のまとめ

  • オプションパターンを利用すると、 appsettings.json からクラスのインスタンスに設定値を読みだせます。
  • IValidateOptions<TOption> の具象クラスを作成して、 Program.cs で設定を追加すると、設定値に対して入力値検証ができます。
  • ネストされた設定や繰り返し項目に対する検証は ValidateObjectMembersAttributeValidateEnumeratedItemsAttribute でできます。

前提となる環境

  • .NET 8
  • Microsoft.Extensions.Options 8.0.2

アプリケーション設定の取り扱い方

.NETアプリケーションでは、appsettings.jsonを利用して設定値を管理することが一般的です。 appsettings.jsonに設定した値は、様々な方法で読み取れますが、個人的にはオプションパターンを活用するのが好きです。

オプションパターンを利用すると、appsettings.jsonの設定値をクラスのインスタンスにマッピングできます。 設定値を読み取るときに型指定されたオブジェクトとして取り扱えるため、タイプミスを削減できます。 またカスタム属性を用いて入力値検証を行うこともできるため、設定の誤りを実行時に検出できます。

DIコンテナー経由で設定値のオブジェクトを取得できるのも良いポイントです。 様々な設定を持った appsettings.json をテストで用意しなくても、テストコードで任意の設定が作り出せます。

オプションパターンの定義方法

まずは設定を読み取るためのクラスを作ります。 以下のように、クラスをネストした構造や、何らかのクラスのコレクションも持てます。

namespace OptionsPattern.Web.Configurations;

public class OptionsPatternSettings
{
    public const string ConfigurationSectionName = nameof(OptionsPatternSettings);

    public required string Setting1 { get; set; }

    public SubSettings SubSettings { get; set; } = new();

    public IList<SettingItem> SettingItems { get; set; } = [];
}

public class SubSettings
{
    public int Level { get; set; } = 0;
}

public class SettingItem
{
    public required string Name { set; get; }

    public required string Value { set; get; }
}

appsettings.json には、設定値を定義します。 プロパティ名と構造をそろえるようにしましょう。 なおトップレベル要素の名前は任意ですが、この例では設定値を読み取るクラスと同名にしてあります。

{
  "OptionsPatternSettings": {
    "Setting1": "Setting1-Value",
    "SettingItems": [
      {
        "Name": "Key1",
        "Value": "Value1"
      },
      {
        "Name": "Key2",
        "Value": "Value2"
      },
      {
        "Name": "Key3",
        "Value": "Value3"
      }
    ],
    "SubSettings": {
      "Level": 50
    }
  }
}

appsettings.json の設定値を読み取るには、 Program.cs に実装を追加します。 これで DI コンテナーに IOptions<OptionsPatternSettings> のオブジェクトが登録されます。

using OptionsPattern.Web.Configurations;

var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddOptions<OptionsPatternSettings>()
    .BindConfiguration(OptionsPatternSettings.ConfigurationSectionName);

設定値の取得方法

設定値は DI コンテナーから IOptions<TOption> のオブジェクトを取り出すだけです。 IOptions<TOption>.Value プロパティを参照すると、 TOption 型のインスタンスを取得できます。

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using OptionsPattern.Web.Configurations;

namespace OptionsPattern.Web.Controllers;

public class HomeController(IOptions<OptionsPatternSettings> options) : Controller
{
    public IActionResult Index()
    {
        this.ViewBag.Options = options.Value;
        return this.View();
    }
}

この例ではプライマリコンストラクターを使用しています。 詳細は以下を参照してください。

learn.microsoft.com

Program.cs の中で IOptions<TOption> のオブジェクトを取得する場合も、 GetRequiredService メソッドを使えば DI コンテナーから取り出せます。

using Microsoft.Extensions.Options;
using OptionsPattern.Web.Configurations;

var builder = WebApplication.CreateBuilder(args);

// 中略

var app = builder.Build();
var options = app.Services.GetRequiredService<IOptions<OptionsPatternSettings>>();
OptionsPatternSettings setting = options.Value;

基本の入力値検証

オプションパターンで作成したクラスは、 System.ComponentModel.DataAnnotations 名前空間に存在するカスタム属性(検証属性)を利用して設定値の検証ができます。

検証ルールの設定

プロパティに対して、入力値検証のルールを検証属性で設定します。 例えば以下の場合は、 Setting1 の設定が必須であることを示しています。

using System.ComponentModel.DataAnnotations;

namespace OptionsPattern.Web.Configurations;

public class OptionsPatternSettings
{
    public const string ConfigurationSectionName = nameof(OptionsPatternSettings);

    [Required]
    public required string Setting1 { get; set; }

    // 省略
}

IValidateOptions 実装クラスの作成

検証属性を設定したら、入力値検証を実行するためのクラスを作成します。 このクラスはソースジェネレーターによって大半が自動生成されるため、開発者が実装するコードは以下の定型句のみです。

using Microsoft.Extensions.Options;

namespace OptionsPattern.Web.Configurations;

[OptionsValidator]
public partial class OptionsPatternSettingsValidator : IValidateOptions<OptionsPatternSettings>
{
}

まずクラスは partial にする必要があります。 そして IValidateOptions<OptionsPatternSettings> インターフェースを継承し、 OptionsValidator 属性を付与します。

Program.cs の実装

作成したこれらのクラスは、 Program.cs で DI コンテナーに登録します。 また appsettings.json から設定を読み込むようにします。

using OptionsPattern.Web.Configurations;

var builder = WebApplication.CreateBuilder(args);

var optionBuilder = builder.Services
    .AddOptions<OptionsPatternSettings>()
    .BindConfiguration(OptionsPatternSettings.ConfigurationSectionName)
    .ValidateDataAnnotations();

AddOptions<TOption> メソッドを呼び出すと、オプションパターンで作成したクラスが DI コンテナーに登録されます。 BindConfiguration メソッドを呼び出すと、 appsettings.json から設定値を読み取り、先ほど追加した TOption 型のオブジェクトにデータをバインドします。 BindConfiguration メソッドの引数には、 appsettings.json のトップレベル要素の名前を指定します。 ValidateDataAnnotations メソッドを呼び出すと、 IValidateOptions<TOption> のオブジェクトが DI コンテナーに登録されます。

入力値検証の実行タイミング

通常時

コントローラーなどで IOptions<TOption> のオブジェクトを取得する場合は、以下のように実装します。 入力値検証は、 IOptions<TOption>.Value の値を取得しようとしたときに行われます。 appsettings.json の値に誤りがあると、 OptionsValidationException が発生します。

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using OptionsPattern.Web.Configurations;

namespace OptionsPattern.Web.Controllers;

public class HomeController(IOptions<OptionsPatternSettings> options) : Controller
{
    public IActionResult Index()
    {
        this.ViewBag.Options = options.Value; // OptionsValidationException
        return this.View();
    }
}

Program.cs 内でオプションの値を取得する場合も同様に、 IOptions<TOption>.Value を参照したタイミングで OptionsValidationException が発生します。

using Microsoft.Extensions.Options;
using OptionsPattern.Web.Configurations;

var builder = WebApplication.CreateBuilder(args);

var optionBuilder = builder.Services
    .AddOptions<OptionsPatternSettings>()
    .BindConfiguration(OptionsPatternSettings.ConfigurationSectionName)
    .ValidateDataAnnotations();

var app = builder.Build();

var options = app.Services.GetRequiredService<IOptions<OptionsPatternSettings>>();
var setting = options.Value; // OptionsValidationException

ValidateOnStart を有効にしたとき

Program.cs 内でオプションの値を使用しない場合、実際に入力値検証が行われるのは、オプションの値を参照するときまで遅延します。 しかし、多くの場合で設定値の誤りには素早く気づきたいものです。 そういった場合は、入力値検証のタイミングを Web アプリケーション起動時に前倒しするよう設定します。 この機能を有効にするには、 Program.cs でオプションパターンで作成したクラスを DI コンテナーに登録する箇所で、 ValidateOnStart メソッドを呼び出します。

var optionBuilder = builder.Services
    .AddOptions<OptionsPatternSettings>()
    .BindConfiguration(OptionsPatternSettings.ConfigurationSectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart(); // これを追加

このように設定しておくと、 WebApplication.Run メソッドを実行したときに検証が行われます。 appsettings.json の設定に誤りがあると、 OptionsValidationException が発生します。

var builder = WebApplication.CreateBuilder(args);

// 中略

var app = builder.Build();

// 中略

app.Run(); // OptionsValidationException

ネストされたオプションクラスに対して入力値検証を有効にする

ネストされたオプションクラスに対して入力値検証を有効にするには、 ValidateObjectMembers 属性と ValidateEnumeratedItems 属性を使用します。 ネストするクラスが単純なクラスの場合は、そのプロパティに ValidateObjectMembers 属性をつけます。 リストの場合は ValidateEnumeratedItems 属性をつけます。 ネストされているクラスは、検証属性を付与します。

先ほど登場した OptionsPatternSettings のクラス群に、これらの属性を付与する例は以下の通りです。

using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;

namespace OptionsPattern.Web.Configurations;

public class OptionsPatternSettings
{
    public const string ConfigurationSectionName = nameof(OptionsPatternSettings);

    [Required]
    public required string Setting1 { get; set; }

    [ValidateObjectMembers]
    public SubSettings SubSettings { get; set; } = new();

    [ValidateEnumeratedItems]
    public IList<SettingItem> SettingItems { get; set; } = [];
}

public class SubSettings
{
    [Required]
    [Range(0, 100)]
    public int Level { get; set; } = 0;
}

public class SettingItem
{
    [Required]
    public required string Name { set; get; }

    [Required]
    public required string Value { set; get; }
}

ネストされたコレクションに対して入力値検証を有効にする

入力値検証を有効にするには IValidateOptions<TOption> を実装する必要がありました。 しかし、 ValidateObjectMembers 属性と ValidateEnumeratedItems 属性をつけたプロパティのクラスには、 IValidateOptions<TOption> の実装クラスの追加は不要です。 この例でいえば、 IValidateOptions<SubSettings>IValidateOptions<SettingItem> の実装クラスは追加定義しなくてかまいません。 これらのクラスに対する IValidateOptions<TOption> の実装クラスは、 IValidateOptions<OptionsPatternSettings> のソースジェネレーターが生成してくれます。 IValidateOptions<SubSettings>IValidateOptions<SettingItem> を明示的に作成しても、それらが使われることはありません。

サンプルコード

本稿で紹介したサンプルコードは、以下からダウンロードできます。

github.com