Pass an array of integers to ASP.NET Web API?

前端 未结 17 1728
孤独总比滥情好
孤独总比滥情好 2020-11-22 04:40

I have an ASP.NET Web API (version 4) REST service where I need to pass an array of integers.

Here is my action method:



        
相关标签:
17条回答
  • 2020-11-22 05:08

    As Filip W points out, you might have to resort to a custom model binder like this (modified to bind to actual type of param):

    public IEnumerable<Category> GetCategories([ModelBinder(typeof(CommaDelimitedArrayModelBinder))]long[] categoryIds) 
    {
        // do your thing
    }
    
    public class CommaDelimitedArrayModelBinder : IModelBinder
    {
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            var key = bindingContext.ModelName;
            var val = bindingContext.ValueProvider.GetValue(key);
            if (val != null)
            {
                var s = val.AttemptedValue;
                if (s != null)
                {
                    var elementType = bindingContext.ModelType.GetElementType();
                    var converter = TypeDescriptor.GetConverter(elementType);
                    var values = Array.ConvertAll(s.Split(new[] { ","},StringSplitOptions.RemoveEmptyEntries),
                        x => { return converter.ConvertFromString(x != null ? x.Trim() : x); });
    
                    var typedValues = Array.CreateInstance(elementType, values.Length);
    
                    values.CopyTo(typedValues, 0);
    
                    bindingContext.Model = typedValues;
                }
                else
                {
                    // change this line to null if you prefer nulls to empty arrays 
                    bindingContext.Model = Array.CreateInstance(bindingContext.ModelType.GetElementType(), 0);
                }
                return true;
            }
            return false;
        }
    }
    

    And then you can say:

    /Categories?categoryids=1,2,3,4 and ASP.NET Web API will correctly bind your categoryIds array.

    0 讨论(0)
  • 2020-11-22 05:11

    ASP.NET Core 2.0 Solution (Swagger Ready)

    Input

    DELETE /api/items/1,2
    DELETE /api/items/1
    

    Code

    Write the provider (how MVC knows what binder to use)

    public class CustomBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
    
            if (context.Metadata.ModelType == typeof(int[]) || context.Metadata.ModelType == typeof(List<int>))
            {
                return new BinderTypeModelBinder(typeof(CommaDelimitedArrayParameterBinder));
            }
    
            return null;
        }
    }
    

    Write the actual binder (access all sorts of info about the request, action, models, types, whatever)

    public class CommaDelimitedArrayParameterBinder : IModelBinder
    {
    
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
    
            var value = bindingContext.ActionContext.RouteData.Values[bindingContext.FieldName] as string;
    
            // Check if the argument value is null or empty
            if (string.IsNullOrEmpty(value))
            {
                return Task.CompletedTask;
            }
    
            var ints = value?.Split(',').Select(int.Parse).ToArray();
    
            bindingContext.Result = ModelBindingResult.Success(ints);
    
            if(bindingContext.ModelType == typeof(List<int>))
            {
                bindingContext.Result = ModelBindingResult.Success(ints.ToList());
            }
    
            return Task.CompletedTask;
        }
    }
    

    Register it with MVC

    services.AddMvc(options =>
    {
        // add custom binder to beginning of collection
        options.ModelBinderProviders.Insert(0, new CustomBinderProvider());
    });
    

    Sample usage with a well documented controller for Swagger

    /// <summary>
    /// Deletes a list of items.
    /// </summary>
    /// <param name="itemIds">The list of unique identifiers for the  items.</param>
    /// <returns>The deleted item.</returns>
    /// <response code="201">The item was successfully deleted.</response>
    /// <response code="400">The item is invalid.</response>
    [HttpDelete("{itemIds}", Name = ItemControllerRoute.DeleteItems)]
    [ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)]
    public async Task Delete(List<int> itemIds)
    => await _itemAppService.RemoveRangeAsync(itemIds);
    

    EDIT: Microsoft recommends using a TypeConverter for these kids of operations over this approach. So follow the below posters advice and document your custom type with a SchemaFilter.

    0 讨论(0)
  • 2020-11-22 05:15

    I originally used the solution that @Mrchief for years (it works great). But when when I added Swagger to my project for API documentation my end point was NOT showing up.

    It took me a while, but this is what I came up with. It works with Swagger, and your API method signatures look cleaner:

    In the end you can do:

        // GET: /api/values/1,2,3,4 
    
        [Route("api/values/{ids}")]
        public IHttpActionResult GetIds(int[] ids)
        {
            return Ok(ids);
        }
    

    WebApiConfig.cs

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Allow WebApi to Use a Custom Parameter Binding
            config.ParameterBindingRules.Add(descriptor => descriptor.ParameterType == typeof(int[]) && descriptor.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get)
                                                               ? new CommaDelimitedArrayParameterBinder(descriptor)
                                                               : null);
    
            // Allow ApiExplorer to understand this type (Swagger uses ApiExplorer under the hood)
            TypeDescriptor.AddAttributes(typeof(int[]), new TypeConverterAttribute(typeof(StringToIntArrayConverter)));
    
            // Any existing Code ..
    
        }
    }
    

    Create a new class: CommaDelimitedArrayParameterBinder.cs

    public class CommaDelimitedArrayParameterBinder : HttpParameterBinding, IValueProviderParameterBinding
    {
        public CommaDelimitedArrayParameterBinder(HttpParameterDescriptor desc)
            : base(desc)
        {
        }
    
        /// <summary>
        /// Handles Binding (Converts a comma delimited string into an array of integers)
        /// </summary>
        public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
                                                 HttpActionContext actionContext,
                                                 CancellationToken cancellationToken)
        {
            var queryString = actionContext.ControllerContext.RouteData.Values[Descriptor.ParameterName] as string;
    
            var ints = queryString?.Split(',').Select(int.Parse).ToArray();
    
            SetValue(actionContext, ints);
    
            return Task.CompletedTask;
        }
    
        public IEnumerable<ValueProviderFactory> ValueProviderFactories { get; } = new[] { new QueryStringValueProviderFactory() };
    }
    

    Create a new class: StringToIntArrayConverter.cs

    public class StringToIntArrayConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
        }
    }
    

    Notes:

    • https://stackoverflow.com/a/47123965/862011 pointed me in the right direction
    • Swagger was only failing to pick my comma delimited end points when using the [Route] attribute
    0 讨论(0)
  • 2020-11-22 05:16

    I have created a custom model binder which converts any comma separated values (only primitive, decimal, float, string) to their corresponding arrays.

    public class CommaSeparatedToArrayBinder<T> : IModelBinder
        {
            public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
            {
                Type type = typeof(T);
                if (type.IsPrimitive || type == typeof(Decimal) || type == typeof(String) || type == typeof(float))
                {
                    ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
                    if (val == null) return false;
    
                    string key = val.RawValue as string;
                    if (key == null) { bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Wrong value type"); return false; }
    
                    string[] values = key.Split(',');
                    IEnumerable<T> result = this.ConvertToDesiredList(values).ToArray();
                    bindingContext.Model = result;
                    return true;
                }
    
                bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Only primitive, decimal, string and float data types are allowed...");
                return false;
            }
    
            private IEnumerable<T> ConvertToDesiredArray(string[] values)
            {
                foreach (string value in values)
                {
                    var val = (T)Convert.ChangeType(value, typeof(T));
                    yield return val;
                }
            }
        }
    

    And how to use in Controller:

     public IHttpActionResult Get([ModelBinder(BinderType = typeof(CommaSeparatedToArrayBinder<int>))] int[] ids)
            {
                return Ok(ids);
            }
    
    0 讨论(0)
  • 2020-11-22 05:18

    I just added the Query key (Refit lib) in the property for the request.

    [Query(CollectionFormat.Multi)]

    public class ExampleRequest
    {
           
            [FromQuery(Name = "name")]
            public string Name { get; set; }               
           
            [AliasAs("category")]
            [Query(CollectionFormat.Multi)]
            public List<string> Categories { get; set; }
    }
    
    0 讨论(0)
  • 2020-11-22 05:20

    Instead of using a custom ModelBinder, you can also use a custom type with a TypeConverter.

    [TypeConverter(typeof(StrListConverter))]
    public class StrList : List<string>
    {
        public StrList(IEnumerable<string> collection) : base(collection) {}
    }
    
    public class StrListConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
        }
    
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value == null)
                return null;
    
            if (value is string s)
            {
                if (string.IsNullOrEmpty(s))
                    return null;
                return new StrList(s.Split(','));
            }
            return base.ConvertFrom(context, culture, value);
        }
    }
    

    The advantage is that it makes the Web API method's parameters very simple. You dont't even need to specify [FromUri].

    public IEnumerable<Category> GetCategories(StrList categoryIds) {
      // code to retrieve categories from database
    }
    

    This example is for a List of strings, but you could do categoryIds.Select(int.Parse) or simply write an IntList instead.

    0 讨论(0)
提交回复
热议问题