Asp.net web api 将整数数组传递给action方法的Web api

Asp.net web api 将整数数组传递给action方法的Web api,asp.net-web-api,asp.net-web-api2,Asp.net Web Api,Asp.net Web Api2,我有以下web api方法: [HttpGet] [RouteWorkPlanList/{clientsId}/{date:datetime}] 公共异步任务工作列表[FromUri]列表客户端SID,[FromUri]日期时间日期 { } 下面是我用来调用上述操作方法的URI: http://localhost/blabla/api/workPlan/WorkPlanList/5,4/2016-06-01 我在曲线括号上设置了断点,看到日期时间值完全通过,而clientsId值为0 知道我为

我有以下web api方法:

[HttpGet] [RouteWorkPlanList/{clientsId}/{date:datetime}] 公共异步任务工作列表[FromUri]列表客户端SID,[FromUri]日期时间日期 { } 下面是我用来调用上述操作方法的URI:

http://localhost/blabla/api/workPlan/WorkPlanList/5,4/2016-06-01
我在曲线括号上设置了断点,看到日期时间值完全通过,而clientsId值为0

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

在clientsId上得到0是因为框架无法将示例中的值4,5绑定到列表。在这种情况下,您将使用自定义模型绑定器,将值解析为所需的类型,并将其绑定到操作参数:

[路线定义标签A/api/工作计划] 公共类工作控制器:ApiController{ [HttpGet] [RouteWorkPlanList/{clientsId}/{date:datetime}] 公共IHttpActionResult工作列表[ModelBindertypeofClientsIdBinder]列表客户端SID,[FromUri]日期时间日期{ var result=new{clientsId,date}; 返回结果; } } 公共类ClientsIdBinder:IModelBinder{ public bool BindModelSystem.Web.Http.Controllers.HttpActionContext actionContext,ModelBindingContext bindingContext{ if!typeofIEnumerable.IsAssignableFrombindingContext.ModelType{ 返回false; } var val=bindingContext.ValueProvider.GetValuebindingContext.ModelName; 如果val==null{ 返回false; } var id=val.RawValue作为字符串; 如果id==null{ 返回false; } var tokens=ids.Splitnew[]{',},StringSplitOptions.RemoveEmptyEntries; 如果令牌.Length>0{ var clientsId=tokens.Selects=>int.Parses; 如果bindingContext.ModelType.IsArray{ bindingContext.Model=clientsId.ToArray; }否则{ bindingContext.Model=clientsId.ToList; } 返回true; } bindingContext.ModelState.AddModelError bindingContext.ModelName,无法转换客户端ID; 返回false; } } 参考:

由于框架无法将示例中的值4,5绑定到列表,因此在clientsId上得到0。在这种情况下,您将使用自定义模型绑定器,将值解析为所需的类型,并将其绑定到操作参数:

[路线定义标签A/api/工作计划] 公共类工作控制器:ApiController{ [HttpGet] [RouteWorkPlanList/{clientsId}/{date:datetime}] 公共IHttpActionResult工作列表[ModelBindertypeofClientsIdBinder]列表客户端SID,[FromUri]日期时间日期{ var result=new{clientsId,date}; 返回结果; } } 公共类ClientsIdBinder:IModelBinder{ public bool BindModelSystem.Web.Http.Controllers.HttpActionContext actionContext,ModelBindingContext bindingContext{ if!typeofIEnumerable.IsAssignableFrombindingContext.ModelType{ 返回false; } var val=bindingContext.ValueProvider.GetValuebindingContext.ModelName; 如果val==null{ 返回false; } var id=val.RawValue作为字符串; 如果id==null{ 返回false; } var tokens=ids.Splitnew[]{',},StringSplitOptions.RemoveEmptyEntries; 如果令牌.Length>0{ var clientsId=tokens.Selects=>int.Parses; 如果bindingContext.ModelType.IsArray{ bindingContext.Model=clientsId.ToArray; }否则{ bindingContext.Model=clientsId.ToList; } 返回true; } bindingContext.ModelState.AddModelError bindingContext.ModelName,无法转换客户端ID; 返回false; } }
参考:

自定义模型绑定是一个选项。但是在请求体中传递值比在URI中传递值更容易

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

创建JSON数组并将其包含在请求正文中

在List clientsId之前写入[FromBody],这将强制框架从请求主体检索数据。模型绑定将自动进行


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

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

创建JSON数组并将其包含在请求正文中

在List clientsId之前写入[FromBody],这将强制框架从请求主体检索数据。模型绑定将自动进行


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

步骤1:创建HttpParameterBinding

步骤2:创建ParameterBindingAttribute

第三步:把它们放在一起

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

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);
    }
}
现在把它放在控制器上并使用它

[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样式的路由调整路由。我使用了与此类似的方法,以便从标题值中提取复杂类型,这在[FromHeaderheaderName]语法中非常有效。

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

步骤1:创建HttpParameterBinding

步骤2:创建ParameterBindingAttribute

第三步:把它们放在一起

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

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);
    }
}
现在把它放在控制器上并使用它

[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样式的路由调整路由。我使用了与此类似的方法,以便从标题值中提取复杂类型,这种方法非常适合[FromHeaderheaderName]语法。

尝试以编辑方式提交@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);
}

尝试将@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);
}

此方法不适用于GET请求,因为请求正文不应包含有关GET请求的任何数据。此方法适用于POST,但在将新信息发布到OP的端点时会丢失RESTful特性。此方法不适用于GET请求,因为请求正文不应包含GET请求的任何数据。这种方法非常适合POST,但是当您将新信息发布到OP的端点时,就会失去RESTful特性。