web-dev-qa-db-ja.com

ASP.NET Core MVC混合ルート/ FromBodyモデルのバインドと検証

ASP.NET Core 1.1 MVCを使用してJSON APIを構築しています。次のモデルとアクションメソッドがあるとします。

public class TestModel
{
    public int Id { get; set; }

    [Range(100, 999)]
    public int RootId { get; set; }

    [Required, MaxLength(200)]
    public string Name { get; set; }

    public string Description { get; set; }
}

[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho([FromBody] TestModel data)
{
    return Json(new
    {
        data.Id,
        data.RootId,
        data.Name,
        data.Description,
        Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
    });
}

[FromBody]アクションメソッドパラメータで、エンドポイントにポストされたJSONペイロードからモデルがバインドされますが、IdおよびRootIdプロパティがルートを介してバインドされないようにしますパラメーター。

これを別のモデルに分割することができます。1つはルートから、もう1つはボディから、またはすべてのクライアントにidおよびrootIdをペイロードの一部として送信させることもできますが、これらのソリューションはどちらも、私が望む以上に物事を複雑にしているようであり、検証ロジックを1か所に保持することを許可していません。モデルを適切にバインドでき、モデルと検証ロジックを一緒に保持できるこの状況を機能させる方法はありますか?

15
heavyd

入力の[FromBody]デコレータを削除して、MVCバインディングにプロパティをマッピングさせることができます。

[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(TestModel data)
{
    return Json(new
    {
        data.Id,
        data.RootId,
        data.Name,
        data.Description,
        Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
    });
}

詳細: ASP.NET Core MVCのモデルバインディング

[〜#〜] update [〜#〜]

テスト中

enter image description here

enter image description here

更新2

@heavyd、あなたはJSONデータがモデルをバインドするために[FromBody]属性を必要とするという点で正しいです。したがって、上記で述べたことは、フォームデータでは機能しますが、JSONデータでは機能しません。

別の方法として、URLからIdおよびRootIdプロパティをバインドするカスタムモデルバインダーを作成し、リクエスト本文から残りのプロパティをバインドできます。

public class TestModelBinder : IModelBinder
{
    private BodyModelBinder defaultBinder;

    public TestModelBinder(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory) // : base(formatters, readerFactory)
    {
        defaultBinder = new BodyModelBinder(formatters, readerFactory);
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // callinng the default body binder
        await defaultBinder.BindModelAsync(bindingContext);

        if (bindingContext.Result.IsModelSet)
        {
            var data = bindingContext.Result.Model as TestModel;
            if (data != null)
            {
                var value = bindingContext.ValueProvider.GetValue("Id").FirstValue;
                int intValue = 0;
                if (int.TryParse(value, out intValue))
                {
                    // Override the Id property
                    data.Id = intValue;
                }
                value = bindingContext.ValueProvider.GetValue("RootId").FirstValue;
                if (int.TryParse(value, out intValue))
                {
                    // Override the RootId property
                    data.RootId = intValue;
                }
                bindingContext.Result = ModelBindingResult.Success(data);
            }

        }

    }
}

バインダープロバイダーを作成します。

public class TestModelBinderProvider : IModelBinderProvider
{
    private readonly IList<IInputFormatter> formatters;
    private readonly IHttpRequestStreamReaderFactory readerFactory;

    public TestModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
    {
        this.formatters = formatters;
        this.readerFactory = readerFactory;
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(TestModel))
            return new TestModelBinder(formatters, readerFactory);

        return null;
    }
}

そして、MVCにそれを使用するように指示します。

services.AddMvc()
  .AddMvcOptions(options =>
  {
     IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
     options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(options.InputFormatters, readerFactory));
  });

それからあなたのコントローラーは:

[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(TestModel data)
{...}

テスト中

enter image description hereenter image description here

JSONにIdRootIdを追加できますが、モデルバインダーで上書きしているため無視されます。

更新3

上記により、IdおよびRootIdの検証にデータモデルの注釈を使用できます。しかし、APIコードを見る他の開発者を混乱させる可能性があると思います。 APIシグネチャを単純化して、[FromBody]で使用する別のモデルを受け入れ、uriから派生する他の2つのプロパティを分離することをお勧めします。

[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(int id, int rootId, [FromBody]TestModelNameAndAddress testModelNameAndAddress)

そして、次のように、すべての入力に対してバリデーターを適切に設定できます。

// This would return a list of tuples of property and error message.
var errors = validator.Validate(id, rootId, testModelNameAndAddress); 
if (errors.Count() > 0)
{
    foreach (var error in errors)
    {
        ModelState.AddModelError(error.Property, error.Message);
    }
}
10
Frank Fajardo

調査した後、新しいモデルバインダー+バインディングソース+ BodyModelBinderとComplexTypeModelBinderの機能を組み合わせた属性を作成するソリューションを思い付きました。最初にBodyModelBinderを使用してbodyから読み取り、次にComplexModelBinderが他のフィールドに入力します。ここにコード:

public class BodyAndRouteBindingSource : BindingSource
{
    public static readonly BindingSource BodyAndRoute = new BodyAndRouteBindingSource(
        "BodyAndRoute",
        "BodyAndRoute",
        true,
        true
        );

    public BodyAndRouteBindingSource(string id, string displayName, bool isGreedy, bool isFromRequest) : base(id, displayName, isGreedy, isFromRequest)
    {
    }

    public override bool CanAcceptDataFrom(BindingSource bindingSource)
    {
        return bindingSource == Body || bindingSource == this;
    }
}

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromBodyAndRouteAttribute : Attribute, IBindingSourceMetadata
{
    public BindingSource BindingSource => BodyAndRouteBindingSource.BodyAndRoute;
}

public class BodyAndRouteModelBinder : IModelBinder
{
    private readonly IModelBinder _bodyBinder;
    private readonly IModelBinder _complexBinder;

    public BodyAndRouteModelBinder(IModelBinder bodyBinder, IModelBinder complexBinder)
    {
        _bodyBinder = bodyBinder;
        _complexBinder = complexBinder;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        await _bodyBinder.BindModelAsync(bindingContext);

        if (bindingContext.Result.IsModelSet)
        {
            bindingContext.Model = bindingContext.Result.Model;
        }

        await _complexBinder.BindModelAsync(bindingContext);
    }
}

public class BodyAndRouteModelBinderProvider : IModelBinderProvider
{
    private BodyModelBinderProvider _bodyModelBinderProvider;
    private ComplexTypeModelBinderProvider _complexTypeModelBinderProvider;

    public BodyAndRouteModelBinderProvider(BodyModelBinderProvider bodyModelBinderProvider, ComplexTypeModelBinderProvider complexTypeModelBinderProvider)
    {
        _bodyModelBinderProvider = bodyModelBinderProvider;
        _complexTypeModelBinderProvider = complexTypeModelBinderProvider;
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        var bodyBinder = _bodyModelBinderProvider.GetBinder(context);
        var complexBinder = _complexTypeModelBinderProvider.GetBinder(context);

        if (context.BindingInfo.BindingSource != null
            && context.BindingInfo.BindingSource.CanAcceptDataFrom(BodyAndRouteBindingSource.BodyAndRoute))
        {
            return new BodyAndRouteModelBinder(bodyBinder, complexBinder);
        }
        else
        {
            return null;
        }
    }
}

public static class BodyAndRouteModelBinderProviderSetup
{
    public static void InsertBodyAndRouteBinding(this IList<IModelBinderProvider> providers)
    {
        var bodyProvider = providers.Single(provider => provider.GetType() == typeof(BodyModelBinderProvider)) as BodyModelBinderProvider;
        var complexProvider = providers.Single(provider => provider.GetType() == typeof(ComplexTypeModelBinderProvider)) as ComplexTypeModelBinderProvider;

        var bodyAndRouteProvider = new BodyAndRouteModelBinderProvider(bodyProvider, complexProvider);

        providers.Insert(0, bodyAndRouteProvider);
    }
}
14
Matiszak
  1. インストールパッケージ HybridModelBinding

  2. Statrupに追加:

    services.AddMvc()
        .AddHybridModelBinder();
    
  3. モデル:

    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string FavoriteColor { get; set; }
    }
    
  4. コントローラ:

    [HttpPost]
    [Route("people/{id}")]
    public IActionResult Post([FromHybrid]Person model)
    { }
    
  5. 要求:

    curl -X POST -H "Accept: application/json" -H "Content-Type:application/json" -d '{
        "id": 999,
        "name": "Bill Boga",
        "favoriteColor": "Blue"
    }' "https://localhost/people/123?name=William%20Boga"
    
  6. 結果:

    {
        "Id": 123,
        "Name": "William Boga",
        "FavoriteColor": "Blue"
    }
    
  7. 他にも高度な機能があります。

12
Mentor

私はあなたの例のためにこれを試していませんが、このようなasp.netコアサポートモデルバインディングとして機能するはずです。

このようなモデルを作成できます。

public class TestModel
{
    [FromRoute]
    public int Id { get; set; }

    [FromRoute]
    [Range(100, 999)]
    public int RootId { get; set; }

    [FromBody]
    [Required, MaxLength(200)]
    public string Name { get; set; }

    [FromBody]
    public string Description { get; set; }
}

更新1:ストリームが巻き戻し可能でない場合、上記は機能しません。主にJSONデータを投稿する場合。

カスタムモデルバインダーはソリューションですが、まだ作成しないで、モデルで管理したい場合は、2つのモデルを作成できます。

public class TestModel
    {
        [FromRoute]
        public int Id { get; set; }

        [FromRoute]
        [Range(100, 999)]
        public int RootId { get; set; }        

        [FromBody]
        public ChildModel OtherData { get; set; }        
    }


    public class ChildModel
    {            
        [Required, MaxLength(200)]
        public string Name { get; set; }

        public string Description { get; set; }
    }

注:これは、他のコンテンツタイプとは少し異なる動作をするため、application/jsonバインディングで完全に機能します。

2
dotnetstep