Web api将整数数组传递给动作方法

时间:2016-06-11 20:29:47

标签: asp.net-web-api asp.net-web-api2

我有这个web api方法:

[HttpGet]
[Route("WorkPlanList/{clientsId}/{date:datetime}")]
public async Task<IHttpActionResult> WorkPlanList([FromUri]List<int> clientsId, [FromUri]DateTime date)
{

}

这是我用来调用上面的action方法的URI:

http://localhost/blabla/api/workPlan/WorkPlanList/5,4/2016-06-01

我在弯曲括号上设置了断点,并在clientsId值为0时看到日期时间值完美传递。

我知道为什么0 clientsId上有private static String reverse(String str) { int i = 0; int j = str.length()-1; char []c = str.toCharArray(); while(i <= j){ char t = str.charAt(i); c[i] = str.charAt(j); c[j]=t; i++; j--; } return new String(c); }

4 个答案:

答案 0 :(得分:4)

您在0上获得clientsId,因为该框架无法将示例中的值4,5绑定到List<int>。在这种情况下,您使用自定义模型绑定器将该值解析为您想要的类型并将其绑定到您的操作参数:

[RoutePrefix("blabla/api/workplan")]
public class WorkPlanController : ApiController {

    [HttpGet]
    [Route("WorkPlanList/{clientsId}/{date:datetime}")]
    public IHttpActionResult WorkPlanList([ModelBinder(typeof(ClientsIdBinder))]List<int> clientsId, [FromUri]DateTime date) {

        var result = new { clientsId, date };

        return (Ok(result));
    }
}

public class ClientsIdBinder : IModelBinder {

    public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext) {
        if (!typeof(IEnumerable<int>).IsAssignableFrom(bindingContext.ModelType)) {
            return false;
        }

        var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (val == null) {
            return false;
        }

        var ids = val.RawValue as string;
        if (ids == null) {
            return false;
        }

        var tokens = ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
        if (tokens.Length > 0) {
            var clientsId = tokens.Select(s => int.Parse(s));
            if (bindingContext.ModelType.IsArray) {
                bindingContext.Model = clientsId.ToArray();
            } else {
                bindingContext.Model = clientsId.ToList();
             }
            return true;
        }

        bindingContext.ModelState.AddModelError(
            bindingContext.ModelName, "Cannot convert client ids");
        return false;
    }
}

参考:Parameter Binding in ASP.NET Web API

答案 1 :(得分:3)

自定义模型绑定是一种选择。但更容易传递请求体中的值而不是URI中的值。

作为最佳实践,URI中不应存在复杂数据。因此,您的案例中的解决方法是:

  1. 创建一个JSON数组并将其包含在请求正文中。

  2. [FromBody]之前写List<int> clientsId,这将强制框架从请求正文中检索数据。模型绑定将自动发生。

答案 2 :(得分:3)

你的问题引起了我的兴趣,所以我想提出一个比Nkosi提供的答案更通用的解决方案。虽然Nkosi的答案可行,但我不是ModelBinder语法的粉丝,也不是为每种类型定义一个新的ModelBinder。我之前一直在使用ParameterBindingAttribute并且非常喜欢语法,所以我想从这开始。这允许您定义[FromUri]或[FromBody]之类的语法。我也希望能够使用不同的&#34;数组&#34;诸如int []或List或HashSet之类的类型,或者最好的IEnumerable。

第1步:创建HttpParameterBinding

第2步:创建ParameterBindingAttribute

第3步:全部放在一起

HttpParameterBinding允许您解析任何RouteData并通过设置actionContext的ActionArguments字典将它们传递给您的methed。您只需从HttpParameterBinding继承并覆盖ExecuteBindingAsync方法。如果需要,可以在这里抛出异常,但是也可以让它流过,如果它无法解析RouteData,则该方法将收到null。对于此示例,我正在创建一个由RouteData构成的数组的JSON字符串。由于我们知道Json.NET在解析数据类型方面非常出色,因此使用它似乎很自然。这将解析RouteData的CSV值。这对于整数或日期最有效。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata;
using Newtonsoft.Json;

public class CsvParameterBinding : HttpParameterBinding
{
    public CsvParameterBinding(HttpParameterDescriptor descriptor) : base(descriptor)
    {
    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        var paramName = this.Descriptor.ParameterName;

        var rawParamemterValue = actionContext.ControllerContext.RouteData.Values[paramName].ToString();

        var rawValues = rawParamemterValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

        //To convert the raw value int a true JSON array we need to make sure everything is quoted.
        var jsonString = $"[\"{string.Join("\",\"", rawValues)}\"]";

        try
        {
            var obj = JsonConvert.DeserializeObject(jsonString, this.Descriptor.ParameterType);

            actionContext.ActionArguments[paramName] = obj;
        }
        catch
        {
            //There was an error casting, the jsonString must be invalid.
            //Don't set anything and the action will just receive null.
        }

        return Task.FromResult<object>(null);
    }
}

ParameterBindingAttribute允许我们在方法签名中使用声明绑定权限的干净语法。我决定使用[FromUriCsv]作为语法,因此该类被恰当地命名。覆盖唯一的东西是GetBinding方法,我们在其中连接我们刚刚创建的CsvParameterBinding类。

using System.Web.Http;
using System.Web.Http.Controllers;

public class FromUriCsvAttribute : ParameterBindingAttribute
{
    public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
    {
        return new CsvParameterBinding(parameter);
    }
}

现在将它放在控制器上并使用它。

[Route("WorkPlanList/{clientsId}/{date:datetime}")]
public async Task<IHttpActionResult> WorkPlanList([FromUriCsv] List<int> clientsId, [FromUri] DateTime date)
{
    //matches WorkPlanList/2,3,4/7-3-2016
}

[Route("WorkPlanList/{clientsId}")]
public async Task<IHttpActionResult> WorkPlanList([FromUriCsv] HashSet<int> clientsId)
{
    //matches WorkPlanList/2,3,4,5,2
    //clientsId will only contain 2,3,4,5 since it's a HashSet the extra 2 won't be included.
}

[Route("WorkPlanList/{clientsId}/{dates}")]
public async Task<IHttpActionResult> WorkPlanList([FromUriCsv] IEnumerable<int> clientsId, [FromUriCsv] IEnumerable<DateTime> dates)
{
    //matches WorkPlanList/2,3,4/5-2-16,6-17-16,7-3-2016
}

我真的很喜欢这是怎么回事。它对于整数和日期非常有效,但是由于路径中的周期确实很小,因此小数点失败了。现在,这很好地解决了你的问题。如果需要十进制数,则应该能够使用正则表达式或mvc样式路由来调整路由。我已经使用了类似的方法,以便从标头值中提取复杂类型,这对于[FromHeader("headerName")]语法非常有效。

答案 3 :(得分:0)

尝试提交作为对@ManOVision答案的修改,但可能更适合单独显示。

在执行他的答案时,我发现它不支持可选的绑定参数。我进行了一些更新以支持该操作,如下所示。

我未通过参数时收到的错误是:

{
    "Message": "The request is invalid.",
    "MessageDetail": "The parameters dictionary does not contain an entry for parameter 'skus' of type 'System.String[]' for method 'System.Web.Http.IHttpActionResult Get(System.String[], System.String, System.String, System.String, System.String, Boolean)' in 'eGAPI.Controllers.GiftCardsController'. The dictionary must contain an entry for each parameter, including parameters that have null values."
}

实现:

[Route("{skus}")]
public IHttpActionResult Get([FromUriCsv] string[] skus = null)

更新的代码:

public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
{
    var paramName = Descriptor.ParameterName;

    try
    {
        if (actionContext.ControllerContext.RouteData.Values.ContainsKey(paramName))
        {
            var rawParamemterValue = actionContext.ControllerContext.RouteData.Values[paramName]?.ToString();

            if (!string.IsNullOrEmpty(rawParamemterValue))
            {
                var rawValues = rawParamemterValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

                actionContext.ActionArguments[paramName] = JsonConvert.DeserializeObject($"[\"{string.Join("\",\"", rawValues)}\"]", Descriptor.ParameterType);
            }
            else
            {
                actionContext.ActionArguments[paramName] = null;
            }
        }
        else
        {
            actionContext.ActionArguments[paramName] = null;
        }
    }
    catch (Exception)
    {
        actionContext.ActionArguments[paramName] = null;
    }

    return Task.FromResult<object>(null);
}