ASP.NET核心MVC混合路由/ FromBody模型绑定&验证

时间:2017-08-03 22:19:45

标签: c# asp.net asp.net-mvc asp.net-core asp.net-core-mvc

我正在使用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)
    });
}

我的action方法参数上的[FromBody]导致模型从发布到端点的JSON有效内容绑定,但是它也会阻止IdRootId属性通过路线参数绑定。

我可以将其分解为单独的模型,一个绑定路线,另一个绑定身体,或者我也可以强制任何客户发送id& rootId作为有效载荷的一部分,但这两种解决方案似乎都比我想要的更复杂,并且不允许我将验证逻辑保存在一个地方。有没有办法让这种情况适用于模型可以正确绑定的地方,我可以保留我的模型和验证逻辑在一起?

5 个答案:

答案 0 :(得分:13)

经过研究,我想出了一个创建新模型绑定器+绑定源+属性的解决方案,它结合了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);
    }
}

答案 1 :(得分:10)

  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. 还有其他高级功能。

答案 2 :(得分:9)

您可以删除输入中的[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)
    });
}

更多信息: Model binding in ASP.NET Core MVC

<强>更新

测试

enter image description here

enter image description here

更新2

@heavyd,你是对的,因为JSON数据需要[FromBody]属性来绑定你的模型。所以我上面所说的将用于表单数据,但不能用于JSON数据。

作为替代方案,您可以创建一个自定义模型绑定器,该绑定器绑定来自URL的IdRootId属性,同时绑定请求主体中的其余属性。

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 here enter image description here

您可以在JSON中添加IdRootId,但我们会在模型工具包中覆盖它们时会忽略它们。

更新3

以上内容允许您使用数据模型注释来验证IdRootId。但我认为可能会混淆其他开发人员,他们会查看您的API代码。我建议只是简化API签名以接受与[FromBody]一起使用的不同模型,并将来自uri的其他两个属性分开。

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

答案 3 :(得分:2)

我没有尝试过这个例子,但它应该像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数据的情况下。

自定义模型绑定器是解决方案,但如果您仍然不想创建那个并且只想管理模型,那么您可以创建两个模型。

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绑定完美配合,因为它的工作方式与其他内容类型不同。

答案 4 :(得分:1)

我最终要做的(翻译成您的案子)是:

  1. 模型
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; }
}
  1. 控制器
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(int rootId, int id, TestModel data)
{
    data.RootId = rootId;
    data.Id = id;
    return Json(new
    {
        data.Id,
        data.RootId,
        data.Name,
        data.Description,
        Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
    });
}

它在控制器方法上可能不是相同的签名。它可能看起来不像仅在签名中包含模型那样优雅。但是,它很容易,因为它不需要下载任何外部软件包,只需要对控制器方法进行少量更改(每个添加的route参数额外一行和声明的参数)。