ASP.NET Core でのカスタム モデル バインド

作成者: Kirk Larkin

モデル バインドにより、コントローラー アクションが HTTP 要求ではなく (メソッド引数として渡される) モデルの型を直接操作できるようになります。 受信要求データとアプリケーション モデルのマッピングは、モデル バインダーによって処理されます。 開発者は、カスタム モデル バインダーを実装することで組み込みのモデル バインド機能を拡張することができます (ただし、通常は自分のプロバイダーを記述する必要はありません)。

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

既定のモデル バインダーの制限事項

既定のモデル バインダーは、一般的な .NET Core データ型の多くをサポートし、ほとんどの開発者のニーズを満たします。 要求からのテキストベースの入力をモデルの型に直接バインドすることが見込まれています。 入力は、バインドする前に変換しなければならないことがあります。 たとえば、モデル データの検索に使用できるキーがある場合などです。 カスタム モデル バインダーを使用すると、キーに基づいてデータをフェッチすることができます。

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

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

モデル バインダーが文字列から変換できる型の一覧については、「単純型」を参照してください。

独自のカスタム モデル バインダーを作成する前に、既存のモデル バインダーがどのように実装されているかを確認するとよいでしょう。 Base64 でエンコードされた文字列をバイト配列に変換できる、ByteArrayModelBinder の使用を検討してください。 バイト配列は多くの場合、ファイルまたはデータベース BLOB のフィールドとして格納されます。

ByteArrayModelBinder の操作

Base64 でエンコードされた文字列を使用してバイナリ データを表すことができます。 たとえば、イメージは文字列としてエンコードできます。 このサンプルには、Base64String.txt 内に Base64 でエンコードされた文字列としてのイメージが含まれています。

ASP.NET Core MVC では Base64 でエンコードされた文字列を取得し、ByteArrayModelBinder を使用してこれをバイト配列に変換できます。 ByteArrayModelBinderProvider により byte[] 引数が ByteArrayModelBinder にマップされます。

public IModelBinder GetBinder(ModelBinderProviderContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (context.Metadata.ModelType == typeof(byte[]))
    {
        var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
        return new ByteArrayModelBinder(loggerFactory);
    }

    return null;
}

独自のカスタム モデル バインダーを作成するときには、独自の IModelBinderProvider 型を実装するか、ModelBinderAttribute を使用することができます。

次の例は、ByteArrayModelBinder を使用して Base64 でエンコードされた文字列を byte[] に変換し、結果をファイルに保存する方法を示しています。

[HttpPost]
public void Post([FromForm] byte[] file, string filename)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, file);
}

コードのコメントを英語以外の言語に翻訳し表示したい場合、こちらの GitHub ディスカッション イシューにてお知らせください。

curl などのツールを使って、base64 でエンコードされた文字列を前の API メソッドに POST することができます。

バインダーが要求データを適切に命名されたプロパティまたは引数にバインドできれば、モデル バインドは成功します。 ビュー モデルを指定して ByteArrayModelBinder を使用する方法を次の例に示します。

[HttpPost("Profile")]
public void SaveProfile([FromForm] ProfileViewModel model)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, model.File);
}

public class ProfileViewModel
{
    public byte[] File { get; set; }
    public string FileName { get; set; }
}

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

このセクションでは、次のようなカスタム モデル バインダーを実装します。

  • 受信した要求データを厳密に型指定されたキーの引数に変換する。
  • Entity Framework Core を使用して関連するエンティティをフェッチする。
  • 関連するエンティティを引数としてアクション メソッドに渡す。

次のサンプルでは、Author モデルの ModelBinder 属性を使用しています。

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;

namespace CustomModelBindingSample.Data
{
    [ModelBinder(BinderType = typeof(AuthorEntityBinder))]
    public class Author
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string GitHub { get; set; }
        public string Twitter { get; set; }
        public string BlogUrl { get; set; }
    }
}

上記のコードでは、Author アクション パラメーターのバインドで使用される必要がある IModelBinder の型が ModelBinder 属性で指定されています。

次の AuthorEntityBinder クラスは、Entity Framework Core と authorId を使用してデータ ソースからエンティティをフェッチすることで Author パラメーターをバインドします。

public class AuthorEntityBinder : IModelBinder
{
    private readonly AuthorContext _context;

    public AuthorEntityBinder(AuthorContext context)
    {
        _context = context;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var value = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        if (!int.TryParse(value, out var id))
        {
            // Non-integer arguments result in model state errors
            bindingContext.ModelState.TryAddModelError(
                modelName, "Author Id must be an integer.");

            return Task.CompletedTask;
        }

        // Model will be null if not found, including for
        // out of range id values (0, -3, etc.)
        var model = _context.Authors.Find(id);
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}

Note

前述の AuthorEntityBinder クラスは、カスタム モデル バインダーを示しています。 このクラスは、参照シナリオのベスト プラクティスを示すものではありません。 参照の場合は、authorId をバインドし、アクション メソッドでデータベースをクエリします。 この方法により、NotFound のケースからモデル バインドのエラーが分離されます。

アクション メソッドでの AuthorEntityBinder の使用方法を次のコードに示します。

[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

ModelBinder 属性を使用すると、既定の規則を使用しないパラメーターに AuthorEntityBinder を適用できます。

[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

この例では、引数の名前が既定の authorId ではないため、ModelBinder 属性を使用してパラメーターに指定されています。 アクション メソッドでエンティティを検索する場合と比較して、コントローラーとアクション メソッドの両方が簡略化されています。 Entity Framework Core を使用して作成者をフェッチするためのロジックは、モデル バインダーに移動しています。 これは、Author モデルにバインドするメソッドがいくつかある場合、大幅な簡略化になる可能性があります。

ModelBinder 属性を (ビューモデルなどの) 個々のモデル プロパティまたはアクション メソッド パラメーターに適用して、その型やアクションのみを対象とする特定のモデル バインダーまたはモデル名を指定することができます。

ModelBinderProvider の実装

属性を適用する代わりに、IModelBinderProvider を実装することができます。 これは、組み込みフレームワーク バインダーの実装方法と同じです。 バインダーが動作する型を指定するときに、バインダーが受け入れる入力ではなく、生成される引数の型を指定します。 次のバインダー プロバイダーは AuthorEntityBinder で動作します。 プロバイダーの MVC のコレクションに追加されるときに、Author または Author の型のパラメーターで ModelBinder 属性を使用する必要はありません。

using CustomModelBindingSample.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

namespace CustomModelBindingSample.Binders
{
    public class AuthorEntityBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(Author))
            {
                return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
            }

            return null;
        }
    }
}

注: 上のコードは BinderTypeModelBinder を返します。 BinderTypeModelBinder はモデル バインダーのファクトリとして機能し、依存関係の挿入 (DI) を提供します。 AuthorEntityBinder は DI に EF Core へのアクセスを求めます。 モデル バインダーが DI からのサービスを必要としている場合は、BinderTypeModelBinder を使用してください。

カスタム モデル バインダー プロバイダーを使用する場合、次のようにしてこれを ConfigureServices に追加します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AuthorContext>(options => options.UseInMemoryDatabase("Authors"));

    services.AddControllers(options =>
    {
        options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
    });
}

モデル バインダーを評価するときに、プロバイダーのコレクションが順序どおりにチェックされます。 入力モデルに一致するバインダーを返す最初のプロバイダーが使用されます。 そのため、コレクションの末尾にプロバイダーを追加すると、カスタム バインダーより前に組み込みのモデル バインダーが呼び出される可能性があります。 この例では、カスタム プロバイダーが Author アクションの引数で常に使用されるように、コレクションの先頭に追加されています。

ポリモーフィック モデル バインド

派生型の異なるモデルへのバインドは、ポリモーフィック モデル バインドと呼ばれます。 ポリモーフィック カスタム モデル バインドは、要求値を特定の派生モデル型にバインドする必要がある場合に必要です。 ポリモーフィック モデル バインド:

  • すべての言語と相互運用できるように設計された REST API では一般的ではありません。
  • バインドされたモデルに関する判断が困難になります。

ただし、アプリにポリモーフィック モデル バインドが必要な場合、実装は次のコードのようになります。

public abstract class Device
{
    public string Kind { get; set; }
}

public class Laptop : Device
{
    public string CPUIndex { get; set; }
}

public class SmartPhone : Device
{
    public string ScreenSize { get; set; }
}

public class DeviceModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(Device))
        {
            return null;
        }

        var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in subclasses)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new DeviceModelBinder(binders);
    }
}

public class DeviceModelBinder : IModelBinder
{
    private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;

    public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
        var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == "Laptop")
        {
            (modelMetadata, modelBinder) = binders[typeof(Laptop)];
        }
        else if (modelTypeValue == "SmartPhone")
        {
            (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

推奨事項とベスト プラクティス

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

  • 状態コードの設定または結果 (たとえば 404 Not Found) のリターンを試行しないでください。 モデル バインドが失敗した場合、アクション メソッド自体のアクション フィルターまたはロジックでエラーが処理される必要があります。
  • アクション メソッドから繰り返しのコードや横断的な問題を排除する場合に最も役立ちます。
  • 通常、文字列をカスタムの型に変換する場合に使用すべきではありません。通常は、TypeConverter の方がオプションとして優れています。

作成者: Steve Smith

モデル バインドにより、コントローラー アクションが HTTP 要求ではなく (メソッド引数として渡される) モデルの型を直接操作できるようになります。 受信要求データとアプリケーション モデルのマッピングは、モデル バインダーによって処理されます。 開発者は、カスタム モデル バインダーを実装することで組み込みのモデル バインド機能を拡張することができます (ただし、通常は自分のプロバイダーを記述する必要はありません)。

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

既定のモデル バインダーの制限事項

既定のモデル バインダーは、一般的な .NET Core データ型の多くをサポートし、ほとんどの開発者のニーズを満たします。 要求からのテキストベースの入力をモデルの型に直接バインドすることが見込まれています。 入力は、バインドする前に変換しなければならないことがあります。 たとえば、モデル データの検索に使用できるキーがある場合などです。 カスタム モデル バインダーを使用すると、キーに基づいてデータをフェッチすることができます。

モデル バインドの確認

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

独自のカスタム モデル バインダーを作成する前に、既存のモデル バインダーがどのように実装されているかを確認するとよいでしょう。 Base64 でエンコードされた文字列をバイト配列に変換できる、ByteArrayModelBinder の使用を検討してください。 バイト配列は多くの場合、ファイルまたはデータベース BLOB のフィールドとして格納されます。

ByteArrayModelBinder の操作

Base64 でエンコードされた文字列を使用してバイナリ データを表すことができます。 たとえば、イメージは文字列としてエンコードできます。 このサンプルには、Base64String.txt 内に Base64 でエンコードされた文字列としてのイメージが含まれています。

ASP.NET Core MVC では Base64 でエンコードされた文字列を取得し、ByteArrayModelBinder を使用してこれをバイト配列に変換できます。 ByteArrayModelBinderProvider により byte[] 引数が ByteArrayModelBinder にマップされます。

public IModelBinder GetBinder(ModelBinderProviderContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (context.Metadata.ModelType == typeof(byte[]))
    {
        return new ByteArrayModelBinder();
    }

    return null;
}

独自のカスタム モデル バインダーを作成するときには、独自の IModelBinderProvider 型を実装するか、ModelBinderAttribute を使用することができます。

次の例は、ByteArrayModelBinder を使用して Base64 でエンコードされた文字列を byte[] に変換し、結果をファイルに保存する方法を示しています。

[HttpPost]
public void Post([FromForm] byte[] file, string filename)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, file);
}

curl などのツールを使って、base64 でエンコードされた文字列を前の API メソッドに POST することができます。

バインダーが要求データを適切に命名されたプロパティまたは引数にバインドできれば、モデル バインドは成功します。 ビュー モデルを指定して ByteArrayModelBinder を使用する方法を次の例に示します。

[HttpPost("Profile")]
public void SaveProfile([FromForm] ProfileViewModel model)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, model.File);
}

public class ProfileViewModel
{
    public byte[] File { get; set; }
    public string FileName { get; set; }
}

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

このセクションでは、次のようなカスタム モデル バインダーを実装します。

  • 受信した要求データを厳密に型指定されたキーの引数に変換する。
  • Entity Framework Core を使用して関連するエンティティをフェッチする。
  • 関連するエンティティを引数としてアクション メソッドに渡す。

次のサンプルでは、Author モデルの ModelBinder 属性を使用しています。

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;

namespace CustomModelBindingSample.Data
{
    [ModelBinder(BinderType = typeof(AuthorEntityBinder))]
    public class Author
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string GitHub { get; set; }
        public string Twitter { get; set; }
        public string BlogUrl { get; set; }
    }
}

上記のコードでは、Author アクション パラメーターのバインドで使用される必要がある IModelBinder の型が ModelBinder 属性で指定されています。

次の AuthorEntityBinder クラスは、Entity Framework Core と authorId を使用してデータ ソースからエンティティをフェッチすることで Author パラメーターをバインドします。

public class AuthorEntityBinder : IModelBinder
{
    private readonly AppDbContext _db;

    public AuthorEntityBinder(AppDbContext db)
    {
        _db = db;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var value = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        if (!int.TryParse(value, out var id))
        {
            // Non-integer arguments result in model state errors
            bindingContext.ModelState.TryAddModelError(
                modelName, "Author Id must be an integer.");

            return Task.CompletedTask;
        }

        // Model will be null if not found, including for 
        // out of range id values (0, -3, etc.)
        var model = _db.Authors.Find(id);
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}

Note

前述の AuthorEntityBinder クラスは、カスタム モデル バインダーを示しています。 このクラスは、参照シナリオのベスト プラクティスを示すものではありません。 参照の場合は、authorId をバインドし、アクション メソッドでデータベースをクエリします。 この方法により、NotFound のケースからモデル バインドのエラーが分離されます。

アクション メソッドでの AuthorEntityBinder の使用方法を次のコードに示します。

[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }
    
    return Ok(author);
}

ModelBinder 属性を使用すると、既定の規則を使用しないパラメーターに AuthorEntityBinder を適用できます。

[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

この例では、引数の名前が既定の authorId ではないため、ModelBinder 属性を使用してパラメーターに指定されています。 アクション メソッドでエンティティを検索する場合と比較して、コントローラーとアクション メソッドの両方が簡略化されています。 Entity Framework Core を使用して作成者をフェッチするためのロジックは、モデル バインダーに移動しています。 これは、Author モデルにバインドするメソッドがいくつかある場合、大幅な簡略化になる可能性があります。

ModelBinder 属性を (ビューモデルなどの) 個々のモデル プロパティまたはアクション メソッド パラメーターに適用して、その型やアクションのみを対象とする特定のモデル バインダーまたはモデル名を指定することができます。

ModelBinderProvider の実装

属性を適用する代わりに、IModelBinderProvider を実装することができます。 これは、組み込みフレームワーク バインダーの実装方法と同じです。 バインダーが動作する型を指定するときに、バインダーが受け入れる入力ではなく、生成される引数の型を指定します。 次のバインダー プロバイダーは AuthorEntityBinder で動作します。 プロバイダーの MVC のコレクションに追加されるときに、Author または Author の型のパラメーターで ModelBinder 属性を使用する必要はありません。

using CustomModelBindingSample.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

namespace CustomModelBindingSample.Binders
{
    public class AuthorEntityBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(Author))
            {
                return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
            }

            return null;
        }
    }
}

注: 上のコードは BinderTypeModelBinder を返します。 BinderTypeModelBinder はモデル バインダーのファクトリとして機能し、依存関係の挿入 (DI) を提供します。 AuthorEntityBinder は DI に EF Core へのアクセスを求めます。 モデル バインダーが DI からのサービスを必要としている場合は、BinderTypeModelBinder を使用してください。

カスタム モデル バインダー プロバイダーを使用する場合、次のようにしてこれを ConfigureServices に追加します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase("App"));

    services.AddMvc(options =>
        {
            // add custom binder to beginning of collection
            options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
        })
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

モデル バインダーを評価するときに、プロバイダーのコレクションが順序どおりにチェックされます。 バインダーを返す最初のプロバイダーが使用されます。 コレクションの末尾にプロバイダーを追加すると、カスタム バインダーより前に組み込みのモデル バインダーが呼び出される可能性があります。 この例では、カスタム プロバイダーが Author アクションの引数で使用されるように、コレクションの先頭に追加されています。

ポリモーフィック モデル バインド

派生型の異なるモデルへのバインドは、ポリモーフィック モデル バインドと呼ばれます。 ポリモーフィック カスタム モデル バインドは、要求値を特定の派生モデル型にバインドする必要がある場合に必要です。 ポリモーフィック モデル バインド:

  • すべての言語と相互運用できるように設計された REST API では一般的ではありません。
  • バインドされたモデルに関する判断が困難になります。

ただし、アプリにポリモーフィック モデル バインドが必要な場合、実装は次のコードのようになります。

public abstract class Device
{
    public string Kind { get; set; }
}

public class Laptop : Device
{
    public string CPUIndex { get; set; }
}

public class SmartPhone : Device
{
    public string ScreenSize { get; set; }
}

public class DeviceModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(Device))
        {
            return null;
        }

        var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in subclasses)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new DeviceModelBinder(binders);
    }
}

public class DeviceModelBinder : IModelBinder
{
    private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;

    public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
        var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == "Laptop")
        {
            (modelMetadata, modelBinder) = binders[typeof(Laptop)];
        }
        else if (modelTypeValue == "SmartPhone")
        {
            (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

推奨事項とベスト プラクティス

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

  • 状態コードの設定または結果 (たとえば 404 Not Found) のリターンを試行しないでください。 モデル バインドが失敗した場合、アクション メソッド自体のアクション フィルターまたはロジックでエラーが処理される必要があります。
  • アクション メソッドから繰り返しのコードや横断的な問題を排除する場合に最も役立ちます。
  • 通常、文字列をカスタムの型に変換する場合に使用すべきではありません。通常は、TypeConverter の方がオプションとして優れています。