ASP.NET Core でのモデル バインド

この記事では、モデル バインドとは何か、そのしくみ、その動作のカスタマイズ方法を説明します。

モデル バインドとは何か

コントローラーと Razor Pages では、HTTP 要求からのデータが処理されます。 たとえば、ルート データからはレコード キーが提供され、ポストされたフォーム フィールドからはモデルのプロパティ用の値が提供されます。 これらの各値を取得してそれらを文字列から .NET 型に変換するためのコードを記述するのは、面倒で間違いも起こりやすいでしょう。 モデル バインドを使用すれば、このプロセスを自動化できます。 モデル バインド システムでは次のことが行われます。

  • ルート データ、フォーム フィールド、クエリ文字列などのさまざまなソースからデータを取得します。
  • メソッド パラメーターとパブリック プロパティによりコントローラーと Razor Pages にデータが提供されます。
  • 文字列データを .NET 型に変換します。
  • 複合型のプロパティを更新します。

次のアクション メソッドがあるとします。

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

さらにアプリでは、この URL を使用して要求が受信されます。

https://contoso.com/api/pets/2?DogsOnly=true

ルーティング システムでアクション メソッドが選択されたら、モデル バインドでは次の手順が実行されます。

  • GetById の最初のパラメーター (id という名前の整数型) を検索します。
  • HTTP 要求内で利用可能なソースを調べ、ルート データ内で id = "2" を検索します。
  • 文字列 "2" を整数 2 に変換します。
  • GetById の次のパラメーター (dogsOnly という名前のブール型) を検索します。
  • 該当するソース内を調べ、クエリ文字列内で "DogsOnly=true" を検索します。 名前の照合では大文字と小文字が区別されません。
  • 文字列 "true" をブール型の true に変換します。

次にフレームワークによって GetById メソッドが呼び出され、id パラメーターには 2 が、dogsOnly パラメーターには true が渡されます。

上記の例で、モデル バインディング ターゲットは単純型のメソッド パラメーターになっています。 ターゲットは複合型のプロパティになる場合もあります。 各プロパティが正常にバインドされたら、そのプロパティに対してモデル検証が行われます。 どのようなデータがモデルにバインドされているかを示す記録、バインド エラー、または検証のエラーは、ControllerBase.ModelState または PageModel.ModelState に格納されます。 このプロセスが正常終了したかどうかを確認するために、アプリでは ModelState.IsValid フラグが調べられます。

対象サーバー

モデル バインドでは、次の種類のターゲットの値について検索が試みられます。

  • 要求のルーティング先であるコントローラー アクション メソッドのパラメーター。
  • 要求のルーティング先である Razor Pages ハンドラー メソッドのパラメーター。
  • 属性によって指定されている場合は、コントローラーまたは PageModel クラスのパブリック プロパティ。

[BindProperty] 属性

コントローラーまたは PageModel クラスのパブリック プロパティに適用できます。これによってモデル バインドはそのプロパティをターゲットとするようになります。

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

[BindProperties] 属性

コントローラーまたは PageModel クラスに適用できます。これによってモデル バインドはクラスのすべてのパブリック プロパティをターゲットとするように指示されます。

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

HTTP GET 要求のモデル バインド

既定では、プロパティは HTTP GET 要求にバインドされません。 通常、GET 要求に必要なのはレコード ID パラメーターのみです。 レコード ID は、データベース内の項目の検索に使用されます。 そのため、モデルのインスタンスを保持するプロパティをバインドする必要はありません。 GET 要求からのデータにプロパティがバインドされるようにするシナリオでは、SupportsGet プロパティを true に設定します。

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

変換元

既定では、モデル バインドでは HTTP 要求内の次のソースからキーと値のペアの形式でデータが取得されます。

  1. フォーム フィールド
  2. 要求本文 ([ApiController] 属性を持つコントローラー の場合)。
  3. ルート データ
  4. クエリ文字列パラメーター
  5. アップロード済みのファイル

ターゲット パラメーターまたはプロパティごとに、前述の一覧に示されている順序でソースがスキャンされます。 次のようにいくつかの例外があります。

  • ルート データとクエリ文字列の値は単純型にのみ使用されます。
  • アップロード済みのファイルは、IFormFile または IEnumerable<IFormFile> を実装するターゲットの種類にのみバインドされます。

既定のソースが正しくない場合は、次のいずれかの属性を使用してソースを指定します。

  • [FromQuery] - クエリ文字列から値を取得します。
  • [FromRoute] - ルート データから値を取得します。
  • [FromForm] - ポストされたフォーム フィールドから値を取得します。
  • [FromBody] - 要求本文から値を取得します。
  • [FromHeader] - HTTP ヘッダーから値を取得します。

これらの属性:

  • 次の例のように、モデル クラスではなく、モデル プロパティに個別に追加されます。

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • 必要に応じて、コンストラクター内のモデル名の値を受け取ります。 このオプションは、プロパティ名と要求内の値とが一致しない場合に指定されます。 たとえば、要求内の値は、次の例のようにその名前にハイフンが含まれている場合、ヘッダーである可能性があります。

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

[FromBody] 属性

[FromBody] 属性をパラメーターに適用すると、HTTP 要求の本文からそのプロパティが設定されます。 ASP.NET Core ランタイムでは、本文を読み取る責任が入力フォーマッタに委任されます。 入力フォーマッタについては、この記事で後ほど説明します。

[FromBody] を複合型パラメーターに適用すると、そのプロパティに適用されているバインディング ソース属性はいずれも無視されます。 たとえば、次の Create アクションでは、その pet パラメーターを本文から設定するように指定されています。

public ActionResult<Pet> Create([FromBody] Pet pet)

Pet クラスでは、Breed プロパティをクエリ文字列パラメーターから設定するように指定されています。

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

前の例の場合:

  • [FromQuery] 属性は無視されます。
  • Breed プロパティは、クエリ文字列パラメーターから設定されません。

入力フォーマッタでは本文のみが読み取られ、バインディング ソース属性は認識されません。 本文内で適切な値が見つかった場合は、その値を使用して Breed プロパティが設定されます。

アクション メソッドごとに [FromBody] を複数のパラメーターに適用しないでください。 入力フォーマッタによって要求ストリームが読み取られると、他の [FromBody] パラメーターをバインドするためにそれを再度読み取ることはできません。

その他のソース

ソース データは、"値プロバイダー" によってモデル バインド システムに提供されます。 モデル バインド用に、他のソースからデータを取得するカスタムの値プロバイダーを作成して登録することができます。 たとえば、cookie またはセッション状態からのデータが必要だとします。 新しいソースからデータを取得するには:

  • IValueProvider を実装するクラスを作成します。
  • IValueProviderFactory を実装するクラスを作成します。
  • Program.cs 内のファクトリ クラスを登録します。

このサンプルには、 値プロバイダー s から cookie値を取得するファクトリの例が含まれています。 カスタム値プロバイダー ファクトリを次の場所に Program.cs登録します。

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

上記のコードでは、すべての組み込み値プロバイダーの後にカスタム値プロバイダーが配置されます。 それをリストの最初に持ってくるには、Add ではなく Insert(0, new CookieValueProviderFactory()) を呼び出します。

モデル プロパティ用のソースがない

既定では、モデル プロパティ用の値が見つからない場合、モデル状態エラーは作成されません。 プロパティは次のように null 値または既定値に設定されます。

  • null 許容単純型は null に設定されます。
  • null 非許容値型は default(T) に設定されます。 たとえば、パラメーター int id は 0 に設定されます。
  • 複合型の場合、モデル バインドでは、プロパティを設定せずに既定のコンストラクターを使用して、インスタンスが作成されます。
  • 配列は Array.Empty<T>() に設定されます。例外として、byte[] 配列は null に設定されます。

モデル プロパティ用のフォーム フィールド内で何も見つからないときモデル状態を無効にする必要がある場合は、[BindRequired] 属性を使用します。

この動作は [BindRequired] 、要求本文の ON または XML データではなく JS、ポストされたフォーム データからのモデル バインドに適用されることに注意してください。 要求本文データは、入力フォーマッタによって処理されます。

型変換エラー

ソースは見つかってもそれをターゲットの種類に変換できない場合、無効であることを示すフラグがモデル状態に付けられます。 前のセクションで説明したように、ターゲットのパラメーターまたはプロパティは null または既定値に設定されます。

[ApiController] 属性を持つ API コントローラーでは、モデル状態が無効であると、HTTP 400 の自動応答が生成されます。

Razor ページでは、エラー メッセージを含むページが再表示されます。

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

前のコードによってページが再表示されると、無効な入力はフォーム フィールドに表示されません。 これは、モデル プロパティが null または既定値に設定されているためです。 無効な入力はエラー メッセージに表示されます。 フォーム フィールドに不適切なデータを再表示する場合は、モデル プロパティを文字列にして、データ変換を手動で行うことを検討してください。

型変換エラーが結果的にモデル状態エラーになることを望まない場合も同じ方法をお勧めします。 その場合は、モデル プロパティを文字列にします。

単純型

モデル バインダーでソース文字列の変換先とすることができる単純型には次のものがあります。

複合型

複合型には、バインドする既定のパブリック コンストラクターと書き込み可能なパブリック プロパティが必要です。 モデル バインドが行われると、クラスは既定のパブリック コンストラクターを使用してインスタンス化されます。

複合型の各プロパティについて、モデル バインドは名前パターン prefix.property_nameのソースを検索します。 何も見つからない場合は、プレフィックスなしで property_name だけが探索されます。 クエリを使用する決定は、プロパティごとに行われません。 たとえば、メソッドOnGet(Instructor instructor)にバインドされた 、を含む?Instructor.Id=100&Name=fooクエリでは、結果の型Instructorのオブジェクトに次のものが含まれます。

  • Id100 に設定する。
  • Namenull に設定する。 前の Instructor.Name クエリ パラメーターで使用されたため Instructor.Id 、モデル バインドが想定されています。

パラメーターにバインドする場合、プレフィックスはパラメーター名です。 PageModel パブリック プロパティにバインドする場合、プレフィックスはパブリック プロパティ名です。 一部の属性には、パラメーター名またはプロパティ名の既定の使用をオーバーライドするための Prefix プロパティがあります。

たとえば、複合型が次の Instructor クラスであるとします。

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

プレフィックス = パラメーター名

バインドされるモデルが instructorToUpdate という名前のパラメーターである場合:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

モデル バインドでは、キー instructorToUpdate.ID がないかソースを調べることから始まります。 見つからない場合は、プレフィックスなしで ID が探索されます。

プレフィックス = プロパティ名

バインドされるモデルがコントローラーの Instructor という名前のプロパティか、または PageModel クラスである場合:

[BindProperty]
public Instructor Instructor { get; set; }

モデル バインドでは、キー Instructor.ID がないかソースを調べることから始まります。 見つからない場合は、プレフィックスなしで ID が探索されます。

カスタム プレフィックス

バインドされるモデルが instructorToUpdate という名前のパラメーターであり、かつ Bind 属性でプレフィックスとして Instructor が指定されている場合:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

モデル バインドでは、キー Instructor.ID がないかソースを調べることから始まります。 見つからない場合は、プレフィックスなしで ID が探索されます。

複合型のターゲットの属性

複合型のモデル バインドを制御するために利用できる組み込みの属性がいくつかあります。

警告

ポストされたフォーム データが値のソースである場合、これらの属性はモデル バインドに影響します。 入力フォーマッタには影響 しません 。入力フォーマッタは、ポストされた JSON および XML 要求本文を処理します。 入力フォーマッタについては、この記事で後ほど説明します。

[Bind] 属性

クラスまたはメソッド パラメーターに適用できます。 モデルのどのプロパティをモデル バインドに含めるかを指定します。 [Bind] は、入力フォーマッタには影響 しません

次の例では、任意のハンドラーまたはアクション メソッドが呼び出されると、Instructor モデルの指定されたプロパティのみがバインドされます。

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

次の例では、OnPost メソッドが呼び出されると、Instructor モデルの指定されたプロパティのみがバインドされます。

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

[Bind] 属性を使用すれば、"作成" シナリオにおいて過剰ポスティングから保護することができます。 除外されたプロパティはそのままにしておくのではなく null または既定値に設定されるので、この属性は編集シナリオではうまく機能しません。 過剰ポスティングを防ぐ場合は、[Bind] 属性ではなくビュー モデルをお勧めします。 詳細については、「過剰ポスティングに関するセキュリティの注意事項」を参照してください。

[ModelBinder] 属性

ModelBinderAttribute は、型、プロパティ、またはパラメーターに適用されます。 これにより、特定のインスタンスまたは型をバインドするために使用されるモデル バインダーの種類を指定できます。 次に例を示します。

[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

[ModelBinder] 属性を使用して、モデル バインド時にプロパティまたはパラメーターの名前を変更することもできます。

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

[BindRequired] 属性

モデルのプロパティにのみに適用でき、メソッドのパラメーターには適用できません。 モデルのプロパティに対してバインドを実行できない場合に、モデル バインドがモデル状態エラーを追加できるようにします。 次に例を示します。

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

モデル検証に関するページにある [Required] 属性の説明も参照してください。

[BindNever] 属性

プロパティまたは型に適用できます。 モデル バインドがモデルのプロパティを設定できないようにします。 型に適用すると、モデル バインド システムは、型が定義するすべてのプロパティを除外します。 次に例を示します。

public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}

コレクション

ターゲットが単純型のコレクションである場合、モデル バインドでは parameter_name または property_name との一致が探索されます。 一致が見つからない場合は、サポートされているいずれかの形式がプレフィックスなしで探索されます。 次に例を示します。

  • バインドされるパラメーターが selectedCourses という名前の配列であるとした場合:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • フォームまたはクエリ文字列データは、次のいずれかの形式とすることができます。

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

パラメーターまたは名前付き index プロパティをバインドしたり Index 、コレクション値に隣接している場合はバインドしないでください。 モデル バインドがコレクションのインデックスとして使用 index を試みると、バインドが正しくない可能性があります。 たとえば、次のアクションを考えてみます。

public IActionResult Post(string index, List<Product> products)

前のコードでは、クエリ文字列パラメーターは index メソッド パラメーターに index バインドされ、製品コレクションのバインドにも使用されます。 パラメーターの名前を index 変更するか、モデル バインド属性を使用してバインドを構成すると、この問題は回避されます。

public IActionResult Post(string productIndex, List<Product> products)
selectedCourses[]=1050&selectedCourses[]=2000
  • 上記のすべてのフォーマット例において、モデル バインドでは 2 つの項目から成る配列が selectedCourses パラメーターに渡されます。

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    添え字番号 (... [0] ... [1] ...) を使用するデータ フォーマットでは、確実にそれらがゼロから始まる連続した番号になるようにする必要があります。 添え字の番号付けで欠落している番号がある場合、欠落している番号の後の項目はすべて無視されます。 たとえば、添え字が 0、1 の並びではなく、0、2 の並びで振られている場合、2 番目の項目は無視されます。

辞書

Dictionary ターゲットの場合、モデル バインドでは parameter_name または property_name との一致が探索されます。 一致が見つからない場合は、サポートされているいずれかの形式がプレフィックスなしで探索されます。 次に例を示します。

  • ターゲット パラメーターが selectedCourses という名前の Dictionary<int, string> であるとします:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • ポストされたフォームまたはクエリ文字列データは、次のいずれかの例のようになります。

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • 上記のすべてのフォーマット例において、モデル バインドでは 2 つの項目から成る辞書が selectedCourses パラメーターに渡されます。

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

コンストラクターのバインドとレコードの種類

モデルバインドでは、複合型にパラメーターなしのコンストラクターが含まれている必要があります。 System.Text.JsonNewtonsoft.Json ベースの入力フォーマッタは両方とも、パラメーターなしのコンストラクターを持たないクラスの逆シリアル化をサポートします。

レコードの種類は、ネットワーク経由でデータを簡潔に表す優れた方法です。 ASP.NET Core では、単一のコンストラクターを使用して、モデル バインドとレコード型の検証がサポートされます。

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml:

@model Person

Name: <input asp-for="Name" />
<br />
Age: <input asp-for="Age" />

レコードの種類を検証するとき、ランタイムは、プロパティではなく、特にパラメーターに対してバインドと検証のメタデータを検索します。

フレームワークでは、レコードの種類へのバインディングと検証を行うことができます。

public record Person([Required] string Name, [Range(0, 100)] int Age);

前述の内容を機能させるには、次のような型である必要があります。

  • レコードの種類である。
  • パブリック コンストラクターを 1 つだけ持つ。
  • 同じ名前および型のプロパティを持つパラメーターを含む。 名前は大文字と小文字を区別しないようにする必要がある。

パラメーターなしのコンストラクターを持たない POCO

パラメーターなしのコンストラクターを持たない POCO はバインドできません。

次のコードでは、型にパラメーターなしのコンストラクターが必要であるという例外が発生します。

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

手動で作成されるコンストラクターを含むレコードの種類

プライマリ コンストラクターが動作するように見える、手動で作成されたコンストラクターを含むレコードの種類

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

レコードの種類、検証、メタデータのバインディング

レコードの種類については、パラメーターの検証とバインドのメタデータが使用されます。 プロパティのメタデータはすべて無視されます。

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

検証とメタデータ

検証では、パラメーターのメタデータを使用しますが、プロパティを使用して値を読み取ります。 通常のプライマリ コンストラクターの場合、2 つは同じになります。 ただし、これを打破する方法があります。

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel によって、レコードの種類のパラメーターは更新されません。

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

この場合、MVC により再度 Name のバインドが試行されることはありません。 ただし、Age は更新されます。

モデル バインド ルート データとクエリ文字列のグローバリゼーション動作

ASP.NET Core ルート値プロバイダーとクエリ文字列値プロバイダーでは、次のことが行われます。

  • 値をインバリアント カルチャとして扱います。
  • URL はカルチャに依存しないものと想定します。

これに対し、フォーム データからの値は、カルチャに依存した変換にかけられます。 URL がロケール間で共有可能なように、設計上そのようになっています。

ASP.NET Core ルート値プロバイダーとクエリ文字列値プロバイダーでカルチャ依存の変換が行われるようにするには、次のようにします。

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

特別なデータ型

モデル バインドで処理できる特殊なデータ型がいくつかあります。

IFormFile と IFormFileCollection

HTTP 要求に含まれたアップロード済みファイル。 また、複数のファイルに対して IEnumerable<IFormFile> もサポートされています。

CancellationToken

アクションでは、オプションで CancellationToken をパラメーターとしてバインドすることができます。 これにより、HTTP 要求の基になる接続が中断されたときに、それを通知する RequestAborted がバインドされます。 アクションでは、このパラメーターを使用して、コントローラー アクションの一部として実行される実行時間の長い非同期操作を取り消すことができます。

FormCollection

ポストされたフォーム データからすべての値を取得するために使用します。

入力フォーマッタ

要求本文のデータは、ON、XML、またはその他の JS形式にすることができます。 このデータを解析するために、モデル バインドでは、特定のコンテンツの種類を処理するように構成された "入力フォーマッタ" が使用されます。 既定では、ASP.NET Core には ON データを処理JSするための ON ベースの入力フォーマッタが含まれていますJS。 他のコンテンツの種類については対応する他のフォーマッタを追加することができます。

ASP.NET Core では、Consumes 属性に基づいて入力フォーマッタが選択されます。 属性が存在しない場合は、Content-Type ヘッダーが使用されます。

組み込みの XML 入力フォーマッタを使用するには:

  • Program.cs で、AddXmlSerializerFormatters または AddXmlDataContractSerializerFormatters を呼び出します。

    builder.Services.AddControllers()
        .AddXmlSerializerFormatters();
    
  • 要求本文で XML を必要とするコントローラー クラスまたはアクション メソッドに Consumes 属性を適用します。

    [HttpPost]
    [Consumes("application/xml")]
    public ActionResult<Pet> Create(Pet pet)
    

    詳細については、「XML シリアル化の概要」を参照してください。

入力フォーマッタを使用してモデル バインドをカスタマイズする

入力フォーマッタは、要求本文からデータを読み取るためのすべての役割を担います。 このプロセスをカスタマイズするには、入力フォーマッタによって使用される API を構成します。 このセクションでは、ObjectId という名前のカスタム型を理解するために、System.Text.Json ベースの入力フォーマッタをカスタマイズする方法について説明します。

Id という名前のカスタム ObjectId プロパティが含まれている、次のモデルを考えてみます。

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

System.Text.Json を使用する際のモデル バインド プロセスをカスタマイズするために、JsonConverter<T> から派生するクラスを作成します。

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

カスタム コンバーターを使用するために、型に JsonConverterAttribute 属性を適用します。 次の例では、ObjectId 型は、ObjectIdConverter をそのカスタム コンバーターとして構成されています。

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

詳細については、カスタム コンバーターを記述する方法に関する記事をご覧ください。

指定された型をモデル バインドから除外する

モデル バインドと検証システムの動作は、次によって駆動されます ModelMetadataModelMetadata については、詳細プロバイダーを MvcOptions.ModelMetadataDetailsProviders に追加してカスタマイズできます。 組み込みの詳細プロバイダーは、指定された型に対してモデル バインドまたは検証を無効にする場合に使用できます。

指定された型のすべてのモデルに対してモデル バインドを無効にするには、Program.csExcludeBindingMetadataProvider を追加します。 たとえば、System.Version 型のすべてのモデルに対してモデル バインドを無効にするには、次のようにします。

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

指定された型のプロパティに対して検証を無効にするには、Program.csSuppressChildValidationMetadataProvider を追加します。 たとえば、System.Guid 型のプロパティに対して検証を無効にするには、次のようにします。

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

カスタム モデル バインダー

モデル バインドを拡張するには、カスタム モデル バインダーを記述し、[ModelBinder] 属性を使用してそれを特定のターゲット向けに選択します。 詳細については、「custom model binding」 (カスタム モデル バインド) を参照してください。

手動によるモデル バインド

モデル バインドは、TryUpdateModelAsync メソッドを使用して手動で呼び出すことができます。 このメソッドは ControllerBase クラスと PageModel クラスの両方で定義されています。 メソッドのオーバーロードにより、使用するプレフィックスと値プロバイダーを指定できます。 モデル バインドが失敗した場合は、メソッドから false が返されます。 次に例を示します。

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync では、値プロバイダーを使用して、フォーム本文、クエリ文字列、およびルート データからデータを取得します。 TryUpdateModelAsync は通常、次のようになります。

  • 過剰な投稿を防止するために、Razor Pages と MVC アプリ (コントローラーとビューを使用) で使用されます。
  • フォーム データ、クエリ文字列、およびルート データから使用される場合を除き、Web API では使用されません。 ON を使用 JSする Web API エンドポイントでは 、入力フォーマッタ を使用して、要求本文をオブジェクトに逆シリアル化します。

詳細については、「TryUpdateModelAsync」をご覧ください。

[FromServices] 属性

この属性の名前は、データ ソースを指定するモデル バインド属性のパターンに従います。 ただし、それは、値プロバイダーからのデータ バインドを説明するものではありません。 依存関係挿入コンテナーから型のインスタンスが取得されます。 その目的は、特定のメソッドが呼び出された場合にのみサービスを必要するときにコンストラクターの挿入の代替手段を提供することにあります。

型のインスタンスが依存関係挿入コンテナーに登録されていない場合、アプリはパラメーターをバインドしようとしたときに例外をスローします。 パラメーターを省略可能にするには、次のいずれかの方法を使用します。

  • パラメーターを null 許容にします。
  • パラメーターの既定値を設定します。

null 許容パラメーターの場合は、パラメーターにアクセスする前にパラメーターがないことを null 確認します。

この記事では、モデル バインドとは何か、そのしくみ、その動作のカスタマイズ方法を説明します。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

モデル バインドとは何か

コントローラーと Razor Pages では、HTTP 要求からのデータが処理されます。 たとえば、ルート データからはレコード キーが提供され、ポストされたフォーム フィールドからはモデルのプロパティ用の値が提供されます。 これらの各値を取得してそれらを文字列から .NET 型に変換するためのコードを記述するのは、面倒で間違いも起こりやすいでしょう。 モデル バインドを使用すれば、このプロセスを自動化できます。 モデル バインド システムでは次のことが行われます。

  • ルート データ、フォーム フィールド、クエリ文字列などのさまざまなソースからデータを取得します。
  • メソッド パラメーターとパブリック プロパティによりコントローラーと Razor Pages にデータが提供されます。
  • 文字列データを .NET 型に変換します。
  • 複合型のプロパティを更新します。

次のアクション メソッドがあるとします。

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

さらにアプリでは、この URL を使用して要求が受信されます。

http://contoso.com/api/pets/2?DogsOnly=true

ルーティング システムでアクション メソッドが選択されたら、モデル バインドでは次の手順が実行されます。

  • GetById の最初のパラメーター (id という名前の整数型) を検索します。
  • HTTP 要求内で利用可能なソースを調べ、ルート データ内で id = "2" を検索します。
  • 文字列 "2" を整数 2 に変換します。
  • GetById の次のパラメーター (dogsOnly という名前のブール型) を検索します。
  • 該当するソース内を調べ、クエリ文字列内で "DogsOnly=true" を検索します。 名前の照合では大文字と小文字が区別されません。
  • 文字列 "true" をブール型の true に変換します。

次にフレームワークによって GetById メソッドが呼び出され、id パラメーターには 2 が、dogsOnly パラメーターには true が渡されます。

上記の例で、モデル バインディング ターゲットは単純型のメソッド パラメーターになっています。 ターゲットは複合型のプロパティになる場合もあります。 各プロパティが正常にバインドされたら、そのプロパティに対してモデル検証が行われます。 どのようなデータがモデルにバインドされているかを示す記録、バインド エラー、または検証のエラーは、ControllerBase.ModelState または PageModel.ModelState に格納されます。 このプロセスが正常終了したかどうかを確認するために、アプリでは ModelState.IsValid フラグが調べられます。

対象サーバー

モデル バインドでは、次の種類のターゲットの値について検索が試みられます。

  • 要求のルーティング先であるコントローラー アクション メソッドのパラメーター。
  • 要求のルーティング先である Razor Pages ハンドラー メソッドのパラメーター。
  • 属性によって指定されている場合は、コントローラーまたは PageModel クラスのパブリック プロパティ。

[BindProperty] 属性

コントローラーまたは PageModel クラスのパブリック プロパティに適用できます。これによってモデル バインドはそのプロパティをターゲットとするようになります。

public class EditModel : InstructorsPageModel
{
    [BindProperty]
    public Instructor Instructor { get; set; }

[BindProperties] 属性

ASP.NET Core 2.1 以降で使用できます。 コントローラーまたは PageModel クラスに適用できます。これによってモデル バインドはクラスのすべてのパブリック プロパティをターゲットとするように指示されます。

[BindProperties(SupportsGet = true)]
public class CreateModel : InstructorsPageModel
{
    public Instructor Instructor { get; set; }

HTTP GET 要求のモデル バインド

既定では、プロパティは HTTP GET 要求にバインドされません。 通常、GET 要求に必要なのはレコード ID パラメーターのみです。 レコード ID は、データベース内の項目の検索に使用されます。 そのため、モデルのインスタンスを保持するプロパティをバインドする必要はありません。 GET 要求からのデータにプロパティがバインドされるようにするシナリオでは、SupportsGet プロパティを true に設定します。

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string ApplicationInsightsCookie { get; set; }

変換元

既定では、モデル バインドでは HTTP 要求内の次のソースからキーと値のペアの形式でデータが取得されます。

  1. フォーム フィールド
  2. 要求本文 ([ApiController] 属性を持つコントローラー の場合)。
  3. ルート データ
  4. クエリ文字列パラメーター
  5. アップロード済みのファイル

ターゲット パラメーターまたはプロパティごとに、前述の一覧に示されている順序でソースがスキャンされます。 次のようにいくつかの例外があります。

  • ルート データとクエリ文字列の値は単純型にのみ使用されます。
  • アップロード済みのファイルは、IFormFile または IEnumerable<IFormFile> を実装するターゲットの種類にのみバインドされます。

既定のソースが正しくない場合は、次のいずれかの属性を使用してソースを指定します。

  • [FromQuery] - クエリ文字列から値を取得します。
  • [FromRoute] - ルート データから値を取得します。
  • [FromForm] - ポストされたフォーム フィールドから値を取得します。
  • [FromBody] - 要求本文から値を取得します。
  • [FromHeader] - HTTP ヘッダーから値を取得します。

これらの属性:

  • 次の例のように、(モデル クラスにではなく) モデル プロパティに個別に追加されます。

    public class Instructor
    {
        public int ID { get; set; }
    
        [FromQuery(Name = "Note")]
        public string NoteFromQueryString { get; set; }
    
  • 必要に応じて、コンストラクター内のモデル名の値を受け取ります。 このオプションは、プロパティ名と要求内の値とが一致しない場合に指定されます。 たとえば、要求内の値は、次の例のようにその名前にハイフンが含まれている場合、ヘッダーである可能性があります。

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

[FromBody] 属性

[FromBody] 属性をパラメーターに適用すると、HTTP 要求の本文からそのプロパティが設定されます。 ASP.NET Core ランタイムでは、本文を読み取る責任が入力フォーマッタに委任されます。 入力フォーマッタについては、この記事で後ほど説明します。

[FromBody] を複合型パラメーターに適用すると、そのプロパティに適用されているバインディング ソース属性はいずれも無視されます。 たとえば、次の Create アクションでは、その pet パラメーターを本文から設定するように指定されています。

public ActionResult<Pet> Create([FromBody] Pet pet)

Pet クラスでは、Breed プロパティをクエリ文字列パラメーターから設定するように指定されています。

public class Pet
{
    public string Name { get; set; }

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; }
}

前の例の場合:

  • [FromQuery] 属性は無視されます。
  • Breed プロパティは、クエリ文字列パラメーターから設定されません。

入力フォーマッタでは本文のみが読み取られ、バインディング ソース属性は認識されません。 本文内で適切な値が見つかった場合は、その値を使用して Breed プロパティが設定されます。

アクション メソッドごとに [FromBody] を複数のパラメーターに適用しないでください。 入力フォーマッタによって要求ストリームが読み取られると、他の [FromBody] パラメーターをバインドするためにそれを再度読み取ることはできません。

その他のソース

ソース データは、"値プロバイダー" によってモデル バインド システムに提供されます。 モデル バインド用に、他のソースからデータを取得するカスタムの値プロバイダーを作成して登録することができます。 たとえば、cookie またはセッション状態からのデータが必要だとします。 新しいソースからデータを取得するには:

  • IValueProvider を実装するクラスを作成します。
  • IValueProviderFactory を実装するクラスを作成します。
  • Startup.ConfigureServices 内のファクトリ クラスを登録します。

サンプル アプリには、cookie から値を取得する値プロバイダーファクトリの例が含まれています。 Startup.ConfigureServices 内の登録コードを次に示します。

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

表示したコードでは、すべての組み込み値プロバイダーの後にカスタムの値プロバイダーが配置されています。 それをリストの最初に持ってくるには、Add ではなく Insert(0, new CookieValueProviderFactory()) を呼び出します。

モデル プロパティ用のソースがない

既定では、モデル プロパティ用の値が見つからない場合、モデル状態エラーは作成されません。 プロパティは次のように null 値または既定値に設定されます。

  • null 許容単純型は null に設定されます。
  • null 非許容値型は default(T) に設定されます。 たとえば、パラメーター int id は 0 に設定されます。
  • 複合型の場合、モデル バインドでは、プロパティを設定せずに既定のコンストラクターを使用して、インスタンスが作成されます。
  • 配列は Array.Empty<T>() に設定されます。例外として、byte[] 配列は null に設定されます。

モデル プロパティ用のフォーム フィールド内で何も見つからないときモデル状態を無効にする必要がある場合は、[BindRequired] 属性を使用します。

この [BindRequired] 動作は、要求本文の ON または XML データではなく JS、投稿されたフォーム データからのモデル バインドに適用されることに注意してください。 要求本文データは、入力フォーマッタによって処理されます。

型変換エラー

ソースは見つかってもそれをターゲットの種類に変換できない場合、無効であることを示すフラグがモデル状態に付けられます。 前のセクションで説明したように、ターゲットのパラメーターまたはプロパティは null または既定値に設定されます。

[ApiController] 属性を持つ API コントローラーでは、モデル状態が無効であると、HTTP 400 の自動応答が生成されます。

Razor ページでは、エラー メッセージを含むページが再表示されます。

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _instructorsInMemoryStore.Add(Instructor);
    return RedirectToPage("./Index");
}

クライアント側の検証では、それが行われなかった場合に Razor Pages フォームに送信されてしまう不適切なデータのほとんどがキャッチされます。 この検証により、前の強調表示されたコードをトリガーするのが難しくなります。 サンプル アプリには、[Submit with Invalid Date] ボタンが含まれており、これを使用すると、[Hire Date] フィールドに不適切なデータが入力され、そのフォームが送信されます。 このボタンを使用すると、データ変換エラーが発生したときにページを再表示するためのコードがどのように機能するかを表示できます。

上のコードでページが再表示されると、無効な入力はフォーム フィールドに表示されません。 これは、モデル プロパティが null または既定値に設定されているためです。 無効な入力はエラー メッセージに表示されます。 しかし、フォーム フィールドに不適切なデータを再表示したい場合は、モデル プロパティを文字列にしてデータ変換を手動で行うことを検討してください。

型変換エラーが結果的にモデル状態エラーになることを望まない場合も同じ方法をお勧めします。 その場合は、モデル プロパティを文字列にします。

単純型

モデル バインダーでソース文字列の変換先とすることができる単純型には次のものがあります。

複合型

複合型には、バインドする既定のパブリック コンストラクターと書き込み可能なパブリック プロパティが必要です。 モデル バインドが行われると、クラスは既定のパブリック コンストラクターを使用してインスタンス化されます。

複合型のプロパティごとに、モデル バインドでは名前パターン prefix.property_name がないかソースが調べられます。 何も見つからない場合は、プレフィックスなしで property_name だけが探索されます。

パラメーターにバインドする場合、プレフィックスはパラメーター名です。 PageModel パブリック プロパティにバインドする場合、プレフィックスはパブリック プロパティ名です。 一部の属性には、パラメーター名またはプロパティ名の既定の使用をオーバーライドするための Prefix プロパティがあります。

たとえば、複合型が次の Instructor クラスであるとします。

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

プレフィックス = パラメーター名

バインドされるモデルが instructorToUpdate という名前のパラメーターである場合:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

モデル バインドでは、キー instructorToUpdate.ID がないかソースを調べることから始まります。 見つからない場合は、プレフィックスなしで ID が探索されます。

プレフィックス = プロパティ名

バインドされるモデルがコントローラーの Instructor という名前のプロパティか、または PageModel クラスである場合:

[BindProperty]
public Instructor Instructor { get; set; }

モデル バインドでは、キー Instructor.ID がないかソースを調べることから始まります。 見つからない場合は、プレフィックスなしで ID が探索されます。

カスタム プレフィックス

バインドされるモデルが instructorToUpdate という名前のパラメーターであり、かつ Bind 属性でプレフィックスとして Instructor が指定されている場合:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

モデル バインドでは、キー Instructor.ID がないかソースを調べることから始まります。 見つからない場合は、プレフィックスなしで ID が探索されます。

複合型のターゲットの属性

複合型のモデル バインドを制御するために利用できる組み込みの属性がいくつかあります。

  • [Bind]
  • [BindRequired]
  • [BindNever]

警告

ポストされたフォーム データが値のソースである場合、これらの属性はモデル バインドに影響します。 入力フォーマッタには影響 しません 。入力フォーマッタは、ポストされた JSON および XML 要求本文を処理します。 入力フォーマッタについては、この記事で後ほど説明します。

[Bind] 属性

クラスまたはメソッド パラメーターに適用できます。 モデルのどのプロパティをモデル バインドに含めるかを指定します。 [Bind] は入力フォーマッタには影響 しません

次の例では、任意のハンドラーまたはアクション メソッドが呼び出されると、Instructor モデルの指定されたプロパティのみがバインドされます。

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

次の例では、OnPost メソッドが呼び出されると、Instructor モデルの指定されたプロパティのみがバインドされます。

[HttpPost]
public IActionResult OnPost([Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

[Bind] 属性を使用すれば、"作成" シナリオにおいて過剰ポスティングから保護することができます。 除外されたプロパティはそのままにしておくのではなく null または既定値に設定されるので、この属性は編集シナリオではうまく機能しません。 過剰ポスティングを防ぐ場合は、[Bind] 属性ではなくビュー モデルをお勧めします。 詳細については、「過剰ポスティングに関するセキュリティの注意事項」を参照してください。

[ModelBinder] 属性

ModelBinderAttribute は、型、プロパティ、またはパラメーターに適用されます。 これにより、特定のインスタンスまたは型をバインドするために使用されるモデル バインダーの種類を指定できます。 次に例を示します。

[HttpPost]
public IActionResult OnPost([ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

[ModelBinder] 属性を使用して、モデル バインド時にプロパティまたはパラメーターの名前を変更することもできます。

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    public string Name { get; set; }
}

[BindRequired] 属性

モデルのプロパティにのみに適用でき、メソッドのパラメーターには適用できません。 モデルのプロパティに対してバインドを実行できない場合に、モデル バインドがモデル状態エラーを追加できるようにします。 次に例を示します。

public class InstructorWithCollection
{
    public int ID { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    [Display(Name = "Hire Date")]
    [BindRequired]
    public DateTime HireDate { get; set; }

モデル検証に関するページにある [Required] 属性の説明も参照してください。

[BindNever] 属性

モデルのプロパティにのみに適用でき、メソッドのパラメーターには適用できません。 モデル バインドがモデルのプロパティを設定できないようにします。 次に例を示します。

public class InstructorWithDictionary
{
    [BindNever]
    public int ID { get; set; }

コレクション

ターゲットが単純型のコレクションである場合、モデル バインドでは parameter_name または property_name との一致が探索されます。 一致が見つからない場合は、サポートされているいずれかの形式がプレフィックスなしで探索されます。 次に例を示します。

  • バインドされるパラメーターが selectedCourses という名前の配列であるとした場合:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • フォームまたはクエリ文字列データは、次のいずれかの形式とすることができます。

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    パラメーターまたは名前付き index プロパティをバインドしたり Index 、コレクション値に隣接している場合はバインドしないでください。 モデル バインドがコレクションのインデックスとして使用 index を試みると、バインドが正しくない可能性があります。 たとえば、次のアクションを考えてみます。

    public IActionResult Post(string index, List<Product> products)
    

    前のコードでは、クエリ文字列パラメーターは index メソッド パラメーターに index バインドされ、製品コレクションのバインドにも使用されます。 パラメーターの名前を index 変更するか、モデル バインド属性を使用してバインドを構成すると、この問題は回避されます。

    public IActionResult Post(string productIndex, List<Product> products)
    
  • 次の形式は、フォーム データでのみサポートされます。

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • 上記のすべてのフォーマット例において、モデル バインドでは 2 つの項目から成る配列が selectedCourses パラメーターに渡されます。

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    添え字番号 (... [0] ... [1] ...) を使用するデータ フォーマットでは、確実にそれらがゼロから始まる連続した番号になるようにする必要があります。 添え字の番号付けで欠落している番号がある場合、欠落している番号の後の項目はすべて無視されます。 たとえば、添え字が 0、1 の並びではなく、0、2 の並びで振られている場合、2 番目の項目は無視されます。

辞書

Dictionary ターゲットの場合、モデル バインドでは parameter_name または property_name との一致が探索されます。 一致が見つからない場合は、サポートされているいずれかの形式がプレフィックスなしで探索されます。 次に例を示します。

  • ターゲット パラメーターが selectedCourses という名前の Dictionary<int, string> であるとします:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • ポストされたフォームまたはクエリ文字列データは、次のいずれかの例のようになります。

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • 上記のすべてのフォーマット例において、モデル バインドでは 2 つの項目から成る辞書が selectedCourses パラメーターに渡されます。

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

コンストラクターのバインドとレコードの種類

モデルバインドでは、複合型にパラメーターなしのコンストラクターが含まれている必要があります。 System.Text.JsonNewtonsoft.Json ベースの入力フォーマッタは両方とも、パラメーターなしのコンストラクターを持たないクラスの逆シリアル化をサポートします。

C# 9 では、ネットワーク上のデータを簡潔に表現するための優れた方法であるレコードの種類が導入されています。 ASP.NET Core により、1 つのコンストラクターを使用した、モデル バインドとレコードの種類の検証のサポートが加えられます。

public record Person([Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
   public IActionResult Index() => View();

   [HttpPost]
   public IActionResult Index(Person person)
   {
       ...
   }
}

Person/Index.cshtml:

@model Person

Name: <input asp-for="Name" />
...
Age: <input asp-for="Age" />

レコードの種類を検証するとき、ランタイムは、プロパティではなく、特にパラメーターに対してバインドと検証のメタデータを検索します。

フレームワークでは、レコードの種類へのバインディングと検証を行うことができます。

public record Person([Required] string Name, [Range(0, 100)] int Age);

前述の内容を機能させるには、次のような型である必要があります。

  • レコードの種類である。
  • パブリック コンストラクターを 1 つだけ持つ。
  • 同じ名前および型のプロパティを持つパラメーターを含む。 名前は大文字と小文字を区別しないようにする必要がある。

パラメーターなしのコンストラクターを持たない POCO

パラメーターなしのコンストラクターを持たない POCO はバインドできません。

次のコードでは、型にパラメーターなしのコンストラクターが必要であるという例外が発生します。

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
   public Person(string Name) : this (Name, 0);
}

手動で作成されるコンストラクターを含むレコードの種類

プライマリ コンストラクターが動作するように見える、手動で作成されたコンストラクターを含むレコードの種類

public record Person
{
   public Person([Required] string Name, [Range(0, 100)] int Age) => (this.Name, this.Age) = (Name, Age);

   public string Name { get; set; }
   public int Age { get; set; }
}

レコードの種類、検証、メタデータのバインディング

レコードの種類については、パラメーターの検証とバインドのメタデータが使用されます。 プロパティのメタデータはすべて無視されます。

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

検証とメタデータ

検証では、パラメーターのメタデータを使用しますが、プロパティを使用して値を読み取ります。 通常のプライマリ コンストラクターの場合、2 つは同じになります。 ただし、これを打破する方法があります。

public record Person([Required] string Name)
{
   private readonly string _name;
   public Name { get; init => _name = value ?? string.Empty; } // Now this property is never null. However this object could have been constructed as `new Person(null);`
}

TryUpdateModel によって、レコードの種類のパラメーターは更新されません。

public record Person(string Name)
{
   public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

この場合、MVC により再度 Name のバインドが試行されることはありません。 ただし、Age は更新されます。

モデル バインド ルート データとクエリ文字列のグローバリゼーション動作

ASP.NET Core ルート値プロバイダーとクエリ文字列値プロバイダーでは、次のことが行われます。

  • 値をインバリアント カルチャとして扱います。
  • URL はカルチャに依存しないものと想定します。

これに対し、フォーム データからの値は、カルチャに依存した変換にかけられます。 URL がロケール間で共有可能なように、設計上そのようになっています。

ASP.NET Core ルート値プロバイダーとクエリ文字列値プロバイダーでカルチャ依存の変換が行われるようにするには、次のようにします。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        var index = options.ValueProviderFactories.IndexOf(
            options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>().Single());
        options.ValueProviderFactories[index] = new CulturedQueryStringValueProviderFactory();
    });
}
public class CulturedQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query != null && query.Count > 0)
        {
            var valueProvider = new QueryStringValueProvider(
                BindingSource.Query,
                query,
                CultureInfo.CurrentCulture);

            context.ValueProviders.Add(valueProvider);
        }

        return Task.CompletedTask;
    }
}

特別なデータ型

モデル バインドで処理できる特殊なデータ型がいくつかあります。

IFormFile と IFormFileCollection

HTTP 要求に含まれたアップロード済みファイル。 また、複数のファイルに対して IEnumerable<IFormFile> もサポートされています。

CancellationToken

アクションでは、オプションで CancellationToken をパラメーターとしてバインドすることができます。 これにより、HTTP 要求の基になる接続が中断されたときに、それを通知する RequestAborted がバインドされます。 アクションでは、このパラメーターを使用して、コントローラー アクションの一部として実行される実行時間の長い非同期操作を取り消すことができます。

FormCollection

ポストされたフォーム データからすべての値を取得するために使用します。

入力フォーマッタ

要求本文のデータは、ON、XML、またはその他の JS形式にすることができます。 このデータを解析するために、モデル バインドでは、特定のコンテンツの種類を処理するように構成された "入力フォーマッタ" が使用されます。 既定では、ASP.NET Core には ON データを処理JSするための ON ベースの入力フォーマッタが含まれていますJS。 他のコンテンツの種類については対応する他のフォーマッタを追加することができます。

ASP.NET Core では、Consumes 属性に基づいて入力フォーマッタが選択されます。 属性が存在しない場合は、Content-Type ヘッダーが使用されます。

組み込みの XML 入力フォーマッタを使用するには:

  • Microsoft.AspNetCore.Mvc.Formatters.Xml NuGet パッケージをインストールします。

  • Startup.ConfigureServices で、AddXmlSerializerFormatters または AddXmlDataContractSerializerFormatters を呼び出します。

    services.AddRazorPages()
        .AddMvcOptions(options =>
    {
        options.ValueProviderFactories.Add(new CookieValueProviderFactory());
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(System.Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
    })
    .AddXmlSerializerFormatters();
    
  • 要求本文で XML を必要とするコントローラー クラスまたはアクション メソッドに Consumes 属性を適用します。

    [HttpPost]
    [Consumes("application/xml")]
    public ActionResult<Pet> Create(Pet pet)
    

    詳細については、「XML シリアル化の概要」を参照してください。

入力フォーマッタを使用してモデル バインドをカスタマイズする

入力フォーマッタは、要求本文からデータを読み取るためのすべての役割を担います。 このプロセスをカスタマイズするには、入力フォーマッタによって使用される API を構成します。 このセクションでは、ObjectId という名前のカスタム型を理解するために、System.Text.Json ベースの入力フォーマッタをカスタマイズする方法について説明します。

Id という名前のカスタム ObjectId プロパティが含まれている、次のモデルを考えてみます。

public class ModelWithObjectId
{
    public ObjectId Id { get; set; }
}

System.Text.Json を使用する際のモデル バインド プロセスをカスタマイズするために、JsonConverter<T> から派生するクラスを作成します。

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return new ObjectId(JsonSerializer.Deserialize<int>(ref reader, options));
    }

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
    {
        writer.WriteNumberValue(value.Id);
    }
}

カスタム コンバーターを使用するために、型に JsonConverterAttribute 属性を適用します。 次の例では、ObjectId 型は、ObjectIdConverter をそのカスタム コンバーターとして構成されています。

[JsonConverter(typeof(ObjectIdConverter))]
public struct ObjectId
{
    public ObjectId(int id) =>
        Id = id;

    public int Id { get; }
}

詳細については、カスタム コンバーターを記述する方法に関する記事をご覧ください。

指定された型をモデル バインドから除外する

モデル バインドと検証システムの動作は、次によって駆動されます ModelMetadataModelMetadata については、詳細プロバイダーを MvcOptions.ModelMetadataDetailsProviders に追加してカスタマイズできます。 組み込みの詳細プロバイダーは、指定された型に対してモデル バインドまたは検証を無効にする場合に使用できます。

指定された型のすべてのモデルに対してモデル バインドを無効にするには、Startup.ConfigureServicesExcludeBindingMetadataProvider を追加します。 たとえば、System.Version 型のすべてのモデルに対してモデル バインドを無効にするには、次のようにします。

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

指定された型のプロパティに対して検証を無効にするには、Startup.ConfigureServicesSuppressChildValidationMetadataProvider を追加します。 たとえば、System.Guid 型のプロパティに対して検証を無効にするには、次のようにします。

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

カスタム モデル バインダー

モデル バインドを拡張するには、カスタム モデル バインダーを記述し、[ModelBinder] 属性を使用してそれを特定のターゲット向けに選択します。 詳細については、「custom model binding」 (カスタム モデル バインド) を参照してください。

手動によるモデル バインド

モデル バインドは、TryUpdateModelAsync メソッドを使用して手動で呼び出すことができます。 このメソッドは ControllerBase クラスと PageModel クラスの両方で定義されています。 メソッドのオーバーロードにより、使用するプレフィックスと値プロバイダーを指定できます。 モデル バインドが失敗した場合は、メソッドから false が返されます。 次に例を示します。

if (await TryUpdateModelAsync<InstructorWithCollection>(
    newInstructor,
    "Instructor",
    i => i.FirstMidName, i => i.LastName, i => i.HireDate))
{
    _instructorsInMemoryStore.Add(newInstructor);
    return RedirectToPage("./Index");
}
PopulateAssignedCourseData(newInstructor);
return Page();

TryUpdateModelAsync では、値プロバイダーを使用して、フォーム本文、クエリ文字列、およびルート データからデータを取得します。 TryUpdateModelAsync は通常、次のようになります。

  • 過剰な投稿を防止するために、Razor Pages と MVC アプリ (コントローラーとビューを使用) で使用されます。
  • フォーム データ、クエリ文字列、およびルート データから使用される場合を除き、Web API では使用されません。 ON を使用 JSする Web API エンドポイントでは 、入力フォーマッタ を使用して、要求本文をオブジェクトに逆シリアル化します。

詳細については、「TryUpdateModelAsync」をご覧ください。

[FromServices] 属性

この属性の名前は、データ ソースを指定するモデル バインド属性のパターンに従います。 ただし、それは、値プロバイダーからのデータ バインドを説明するものではありません。 依存関係挿入コンテナーから型のインスタンスが取得されます。 その目的は、特定のメソッドが呼び出された場合にのみサービスを必要するときにコンストラクターの挿入の代替手段を提供することにあります。

その他の技術情報