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

Note

これは、この記事の最新バージョンではありません。 最新のリリースについては、この記事の ASP.NET Core 8.0 バージョンを参照してください。

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

モデル バインドとは何か

コントローラーと 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; }

モデル バインドの単純型と複合型

モデル バインドは、操作の対象とする型に特定の定義を使用します。 単純型は、TypeConverter または TryParse メソッドを使用して、1 つの文字列から変換されます。 複合型は、複数の入力値から変換されます。 フレームワークは、TypeConverter または TryParse の存在の有無によって違いを判断します。 外部リソースや複数の入力を必要としない string から SomeType への変換には、型コンバーターを作成するか、TryParse を使用することをお勧めします。

ソース

既定では、モデル バインドでは 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 内のファクトリ クラスを登録します。

サンプルには、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] 動作は、要求本文内の JSON または XML データに対してではなく、ポストされたフォーム データからのモデル バインドに適用されることに注意してください。 要求本文データは、入力フォーマッタによって処理されます。

型変換エラー

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

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

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

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

    // ...

    return RedirectToPage("./Index");
}

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

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

単純型

単純型と複合型の説明については、「モデル バインドの単純型と複合型」をご覧ください。

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

IParsable<T>.TryParse を使用したバインド

IParsable<TSelf>.TryParse API では、バインディング コントローラー アクションのパラメーター値がサポートされています。

public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);

次の DateRange クラスは、日付範囲のバインドをサポートするために IParsable<TSelf> を実装します。

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

上記のコードでは次の操作が行われます。

  • 2 つの日付を表す文字列を DateRange オブジェクトに変換します
  • モデル バインダーは IParsable<TSelf>.TryParse メソッドを使用して DateRange をバインドします。

次のコントローラー アクションは、DateRange クラスを使用して日付の範囲をバインドします。

// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

次の Locale クラスは、CultureInfo へのバインドをサポートするために IParsable<TSelf> を実装します。

public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}

次のコントローラー アクションは、Locale クラスを使用して CultureInfo 文字列をバインドします。

// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}

次のコントローラー アクションは、DateRangeLocale クラスを使用して日付の範囲を CultureInfo にバインドします。

// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

GitHub の API サンプル アプリには、API コントローラーの前のサンプルが示されています。

TryParse を使用したバインド

TryParse API では、バインディング コントローラー アクションのパラメーター値がサポートされています。

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

パラメーター バインドには、IParsable<T>.TryParse の方法が推奨されます。これは、TryParse とは異なり、リフレクションに依存しないためです。

次の DateRangeTP クラスは、TryParse を実装します。

public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

次のコントローラー アクションは、DateRangeTP クラスを使用して日付の範囲をバインドします。

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

複合型

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

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

  • Id100 に設定する。
  • Namenull に設定する。 前のクエリ パラメーターで Instructor.Id が使用されたため、モデル バインドは Instructor.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 が探索されます。

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

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

警告

ポストされたフォーム データが値のソースである場合、これらの属性はモデル バインドに影響します。 ポストされた 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<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 では、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" />
<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

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

入力フォーマッタ

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

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 ベースの入力フォーマッタをカスタマイズする方法について説明します。

カスタム 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);

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

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

モデル バインドおよび検証システムの動作は、ModelMetadata によって駆動されます。 ModelMetadata については、詳細プロバイダーを 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 では使用されません。 JSON を使用する 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 を使用して要求が受信されます。

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; }

モデル バインドの単純型と複合型

モデル バインドは、操作の対象とする型に特定の定義を使用します。 単純型は、TypeConverter または TryParse メソッドを使用して、1 つの文字列から変換されます。 複合型は、複数の入力値から変換されます。 フレームワークは、TypeConverter または TryParse の存在の有無によって違いを判断します。 外部リソースや複数の入力を必要としない string から SomeType への変換には、型コンバーターを作成するか、TryParse を使用することをお勧めします。

ソース

既定では、モデル バインドでは 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 内のファクトリ クラスを登録します。

サンプルには、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] 動作は、要求本文内の JSON または XML データに対してではなく、ポストされたフォーム データからのモデル バインドに適用されることに注意してください。 要求本文データは、入力フォーマッタによって処理されます。

型変換エラー

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

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

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

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

    // ...

    return RedirectToPage("./Index");
}

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

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

単純型

単純型と複合型の説明については、「モデル バインドの単純型と複合型」をご覧ください。

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

IParsable<T>.TryParse を使用したバインド

IParsable<TSelf>.TryParse API では、バインディング コントローラー アクションのパラメーター値がサポートされています。

public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);

次の DateRange クラスは、日付範囲のバインドをサポートするために IParsable<TSelf> を実装します。

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

上記のコードでは次の操作が行われます。

  • 2 つの日付を表す文字列を DateRange オブジェクトに変換します
  • モデル バインダーは IParsable<TSelf>.TryParse メソッドを使用して DateRange をバインドします。

次のコントローラー アクションは、DateRange クラスを使用して日付の範囲をバインドします。

// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

次の Locale クラスは、CultureInfo へのバインドをサポートするために IParsable<TSelf> を実装します。

public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}

次のコントローラー アクションは、Locale クラスを使用して CultureInfo 文字列をバインドします。

// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}

次のコントローラー アクションは、DateRangeLocale クラスを使用して日付の範囲を CultureInfo にバインドします。

// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

GitHub の API サンプル アプリには、API コントローラーの前のサンプルが示されています。

TryParse を使用したバインド

TryParse API では、バインディング コントローラー アクションのパラメーター値がサポートされています。

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

パラメーター バインドには、IParsable<T>.TryParse の方法が推奨されます。これは、TryParse とは異なり、リフレクションに依存しないためです。

次の DateRangeTP クラスは、TryParse を実装します。

public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

次のコントローラー アクションは、DateRangeTP クラスを使用して日付の範囲をバインドします。

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

複合型

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

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

  • Id100 に設定する。
  • Namenull に設定する。 前のクエリ パラメーターで Instructor.Id が使用されたため、モデル バインドは Instructor.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 が探索されます。

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

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

警告

ポストされたフォーム データが値のソースである場合、これらの属性はモデル バインドに影響します。 ポストされた 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 では、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" />
<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

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

入力フォーマッタ

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

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 ベースの入力フォーマッタをカスタマイズする方法について説明します。

カスタム 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);

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

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

モデル バインドおよび検証システムの動作は、ModelMetadata によって駆動されます。 ModelMetadata については、詳細プロバイダーを 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 では使用されません。 JSON を使用する 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 を使用して要求が受信されます。

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 内のファクトリ クラスを登録します。

サンプルには、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] 動作は、要求本文内の JSON または XML データに対してではなく、ポストされたフォーム データからのモデル バインドに適用されることに注意してください。 要求本文データは、入力フォーマッタによって処理されます。

型変換エラー

ソースは見つかってもそれをターゲットの種類に変換できない場合、無効であることを示すフラグがモデル状態に付けられます。 前のセクションで説明したように、ターゲットのパラメーターまたはプロパティは 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.Id が使用されたため、モデル バインドは Instructor.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 が探索されます。

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

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

警告

ポストされたフォーム データが値のソースである場合、これらの属性はモデル バインドに影響します。 ポストされた 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 では、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" />
<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

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

入力フォーマッタ

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

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 ベースの入力フォーマッタをカスタマイズする方法について説明します。

カスタム 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);

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

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

モデル バインドおよび検証システムの動作は、ModelMetadata によって駆動されます。 ModelMetadata については、詳細プロバイダーを 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 では使用されません。 JSON を使用する 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] 動作は、要求本文内の JSON または XML データに対してではなく、ポストされたフォーム データからのモデル バインドに適用されることに注意してください。 要求本文データは、入力フォーマッタによって処理されます。

型変換エラー

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

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

入力フォーマッタ

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

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; }
}

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

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

モデル バインドおよび検証システムの動作は、ModelMetadata によって駆動されます。 ModelMetadata については、詳細プロバイダーを 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 では使用されません。 JSON を使用する Web API エンドポイントでは、入力フォーマッタを使用して要求本文がオブジェクトに逆シリアル化されます。

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

[FromServices] 属性

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

その他のリソース