【问题标题】:Web api passing array of integers to action methodWeb api将整数数组传递给动作方法
【发布时间】:2018-06-29 01:48:11
【问题描述】:

我有这个 web api 方法:

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

}

这是我用来调用上述操作方法的 URI:

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

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

知道为什么我在clientsId 上得到0 吗?

【问题讨论】:

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


    【解决方案1】:

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

    [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

    【讨论】:

      【解决方案2】:

      您的问题引起了我的兴趣,因此我想提出一个比 Nkosi 提供的答案更通用的解决方案。虽然 Nkosi 的回答会起作用,但我不喜欢 ModelBinder 语法以及为每种类型定义一个新的 ModelBinder。我以前一直在玩 ParameterBindingAttribute 并且非常喜欢语法,所以我想从这个开始。这允许您定义类似 [FromUri] 或 [FromBody] 的语法。我还希望能够使用不同的“数组”类型,例如 int[] 或 List 或 HashSet 或最好的 IEnumerable。

      第 1 步:创建 HttpParameterBinding

      第 2 步:创建 ParameterBindingAttribute

      第 3 步:将所有内容放在一起

      HttpParameterBinding 允许您解析任何 RouteData 并通过设置 actionContext 的 ActionArguments 字典将它们传递给您的方法。您只需从 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
      }
      

      我真的很喜欢它的转变方式。它对于整数和日期非常有效,但对于小数则失败了,因为路径中的句点确实将其提升了。目前,这很好地解决了您的问题。如果需要十进制数字,则应该能够使用 regex 或 mvc 样式路由调整路由。我使用了与此类似的方法,以便从与 [FromHeader("headerName")] 语法非常配合的标头值中提取复杂类型。

      【讨论】:

        【解决方案3】:

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

        作为最佳实践,复杂数据不应出现在 URI 中。因此,在您的情况下,解决方法是:

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

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

        【讨论】:

        • 此方法不适用于 GET 请求,因为请求正文不应包含任何有关 GET 请求的数据。这种方法非常适合 POST,但是当您将新信息发布到 OP 的端点时会失去 RESTful-ness。
        【解决方案4】:

        尝试提交作为对@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);
        }
        

        【讨论】:

          猜你喜欢
          • 2012-04-16
          • 2013-07-20
          • 2018-12-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多