¿Pasar una matriz de enteros a la API web ASP.NET?

427

Tengo un servicio REST ASP.NET Web API (versión 4) donde necesito pasar una matriz de enteros.

Aquí está mi método de acción:

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

Y esta es la URL que he probado:

/Categories?categoryids=1,2,3,4
Hemanshu Bhojak
fuente
1
Recibía un error "No se pueden vincular varios parámetros al contenido de la solicitud" al usar una cadena de consulta como "/ Categorías? Categoryids = 1 & categoryids = 2 & categoryids = 3". Espero que esto traiga a las personas que estaban recibiendo este mismo error.
Josh Noe
1
@Josh ¿Usaste [FromUri] sin embargo? public IEnumerable <Category> GetCategories ([FromUri] int [] categoryids) {...}
Anup Kattel
2
@FrankGorman No, no lo era, que era mi problema.
Josh Noe

Respuestas:

619

Solo necesita agregar [FromUri]antes del parámetro, se ve así:

GetCategories([FromUri] int[] categoryIds)

Y enviar solicitud:

/Categories?categoryids=1&categoryids=2&categoryids=3 
Lavel
fuente
18
¿Qué pasa si no sé cuántas variables tengo en la matriz? ¿Qué pasa si es como 1000? La solicitud no debería ser así.
Sahar Ch.
77
Esto me da el error "Ya se ha agregado un elemento con la misma clave". Sin embargo, acepta categoryids [0] = 1 & categoryids [1] = 2 & etc ...
Doctor Jones
19
Esta debería ser la respuesta aceptada: @Hemanshu Bhojak: ¿no es hora de elegir?
David Rettenbacher
12
Esta razón se debe a la siguiente declaración del sitio web ASP.NET Web API que habla sobre el enlace de parámetros: "Si el parámetro es de tipo" simple ", la API web intenta obtener el valor del URI. Los tipos simples incluyen. NET tipos primitivos (int, bool, double, etc.), más TimeSpan, DateTime, Guid, decimal y string, más cualquier tipo con un convertidor de tipos que pueda convertir desde una cadena ". un int [] no es un tipo simple.
Tr1stan
3
Esto funciona bien para mi. Un punto. En el código del servidor, el parámetro de matriz tiene que venir primero para que funcione y luego cualquier otro parámetro. Al introducir los parámetros en la solicitud, el pedido no es importante.
Sparked
102

Como señala Filip W , es posible que deba recurrir a un modelo de carpeta personalizado como este (modificado para unirse al tipo real de parámetro):

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

Y luego puedes decir:

/Categories?categoryids=1,2,3,4y la API web ASP.NET enlazará correctamente su categoryIdsmatriz.

Mrchief
fuente
10
Esto puede violar SRP y / o SoC, pero puede hacer que esto también herede fácilmente ModelBinderAttributepara que pueda usarse directamente en lugar de la sintaxis laboriosa que usa el typeof()argumento. Todo lo que tiene que hacer es heredan de este modo: CommaDelimitedArrayModelBinder : ModelBinderAttribute, IModelBindery entonces proporcionarle un constructor por defecto que empuja hacia abajo la definición del tipo de la clase base: public CommaDelimitedArrayModelBinder() : base(typeof(CommaDelimitedArrayModelBinder)) { }.
sliderhouserules
De lo contrario, me gusta mucho esta solución y la estoy usando en mi proyecto, así que ... gracias. :)
sliderhouserules
Como nota al margen, esta solución no funciona con genéricos System.Collections.Generic.List<long>como bindingContext.ModelType.GetElementType()solo System.Arraytipos de soporte
ViRuSTriNiTy
@ViRuSTriNiTy: esta pregunta y la respuesta hablan específicamente de matrices. Si necesita una solución basada en una lista genérica, es bastante trivial de implementar. Siéntase libre de plantear una pregunta por separado si no está seguro de cómo hacerlo.
Mrchief
2
@codeMonkey: poner la matriz en el cuerpo tiene sentido para una solicitud POST, pero ¿qué pasa con las solicitudes GET? Estos generalmente no tienen contenido en el cuerpo.
stakx - ya no contribuye el
40

Recientemente me encontré con este requisito, y decidí implementar un ActionFilterpara manejar esto.

public class ArrayInputAttribute : ActionFilterAttribute
{
    private readonly string _parameterName;

    public ArrayInputAttribute(string parameterName)
    {
        _parameterName = parameterName;
        Separator = ',';
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (actionContext.ActionArguments.ContainsKey(_parameterName))
        {
            string parameters = string.Empty;
            if (actionContext.ControllerContext.RouteData.Values.ContainsKey(_parameterName))
                parameters = (string) actionContext.ControllerContext.RouteData.Values[_parameterName];
            else if (actionContext.ControllerContext.Request.RequestUri.ParseQueryString()[_parameterName] != null)
                parameters = actionContext.ControllerContext.Request.RequestUri.ParseQueryString()[_parameterName];

            actionContext.ActionArguments[_parameterName] = parameters.Split(Separator).Select(int.Parse).ToArray();
        }
    }

    public char Separator { get; set; }
}

Lo estoy aplicando así (tenga en cuenta que usé 'id', no 'ids', ya que así es como se especifica en mi ruta):

[ArrayInput("id", Separator = ';')]
public IEnumerable<Measure> Get(int[] id)
{
    return id.Select(i => GetData(i));
}

Y la url pública sería:

/api/Data/1;2;3;4

Puede que tenga que refactorizar esto para satisfacer sus necesidades específicas.

Steve Czetty
fuente
1
Los tipos int están codificados (int.Parse) en su solución. En mi opinión, la solución de @ Mrchief es mejor
razon
27

En caso de que alguien necesite, para lograr lo mismo o algo similar (como eliminar) a través POSTde FromUri, en lugar de , use FromBodyy en el lado del cliente (JS / jQuery) el parámetro de formato como$.param({ '': categoryids }, true)

C#:

public IHttpActionResult Remove([FromBody] int[] categoryIds)

jQuery:

$.ajax({
        type: 'POST',
        data: $.param({ '': categoryids }, true),
        url: url,
//...
});

La $.param({ '': categoryids }, true)cuestión es que .net esperará que el cuerpo de la publicación contenga un valor codificado como =1&=2&=3sin nombre de parámetro y sin corchetes.

Sofija
fuente
2
No es necesario recurrir a un POST. Ver respuesta @Lavel.
André Werlang
3
Hay un límite en la cantidad de datos que puede enviar en un URI. Y, por norma, esto no debería ser una solicitud GET, ya que en realidad está modificando datos.
Digno7
1
¿Y dónde exactamente viste un GET aquí? :)
Sofija
3
@Sofija OP dice code to retrieve categories from database, por lo tanto, el método debe ser un método GET, no POST.
Azimuth
22

Manera fácil de enviar parámetros de matriz a la API web

API

public IEnumerable<Category> GetCategories([FromUri]int[] categoryIds){
 // code to retrieve categories from database
}

Jquery: enviar objeto JSON como parámetros de solicitud

$.get('api/categories/GetCategories',{categoryIds:[1,2,3,4]}).done(function(response){
console.log(response);
//success response
});

Generará su URL de solicitud como ../api/categories/GetCategories?categoryIds=1&categoryIds=2&categoryIds=3&categoryIds=4

Jignesh Variya
fuente
3
¿Cómo es esto diferente de la respuesta aceptada? con la excepción de implementar una solicitud ajax a través de jquery que no tenía nada que ver con la publicación original.
sksallaj
13

Puede probar este código para tomar valores separados por comas / una matriz de valores para recuperar un JSON de webAPI

 public class CategoryController : ApiController
 {
     public List<Category> Get(String categoryIDs)
     {
         List<Category> categoryRepo = new List<Category>();

         String[] idRepo = categoryIDs.Split(',');

         foreach (var id in idRepo)
         {
             categoryRepo.Add(new Category()
             {
                 CategoryID = id,
                 CategoryName = String.Format("Category_{0}", id)
             });
         }
         return categoryRepo;
     }
 }

 public class Category
 {
     public String CategoryID { get; set; }
     public String CategoryName { get; set; }
 } 

Salida:

[
{"CategoryID":"4","CategoryName":"Category_4"}, 
{"CategoryID":"5","CategoryName":"Category_5"}, 
{"CategoryID":"3","CategoryName":"Category_3"} 
]
Naveen Vijay
fuente
12

Solución ASP.NET Core 2.0 (Listo para Swagger)

Entrada

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

Código

Escriba al proveedor (cómo MVC sabe qué carpeta usar)

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

Escriba el cuaderno real (acceda a todo tipo de información sobre la solicitud, acción, modelos, tipos, lo que sea)

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

Regístralo con MVC

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

Ejemplo de uso con un controlador bien documentado para 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);

EDITAR: Microsoft recomienda usar un TypeConverter para estos niños de operaciones sobre este enfoque. Por lo tanto, siga los siguientes consejos de carteles y documente su tipo personalizado con un SchemaFilter.

Victorio Berra
fuente
Creo que la respuesta de MS de la que estás hablando está satisfecha con esta respuesta: stackoverflow.com/a/49563970/4367683
Machado
¿Viste esto? github.com/aspnet/Mvc/pull/7967 parece que agregaron una solución para comenzar a analizar List <whatever> en la cadena de consulta sin necesidad de un cuaderno especial. Además, la publicación que ha vinculado no es ASPNET Core y no creo que ayude con mi situación.
Victorio Berra
La mejor respuesta, no hacky.
Erik Philips
7

En lugar de usar un ModelBinder personalizado, también puede usar un tipo personalizado con un 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);
    }
}

La ventaja es que hace que los parámetros del método de API web sean muy simples. Ni siquiera necesita especificar [FromUri].

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

Este ejemplo es para una Lista de cadenas, pero podría hacer categoryIds.Select(int.Parse)o simplemente escribir una IntList en su lugar.

PhillipM
fuente
No entiendo por qué esta solución no obtuvo muchos votos. Es agradable y limpio y funciona con swagger sin agregar carpetas y cosas personalizadas.
Thieme
La mejor / más limpia respuesta en mi opinión. Gracias PhillipM!
Leigh Bowers
7

Originalmente usé la solución que @Mrchief durante años (funciona muy bien). Pero cuando agregué Swagger a mi proyecto para la documentación de la API, mi punto final NO aparecía.

Me tomó un tiempo, pero esto es lo que se me ocurrió. Funciona con Swagger, y sus firmas de método API se ven más limpias:

Al final puedes hacer:

    // 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 ..

    }
}

Cree una nueva clase: 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() };
}

Cree una nueva clase: StringToIntArrayConverter.cs

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

Notas:

cangrejo CRUSHERclamCOLLECTOR
fuente
1
En caso de que alguien más necesite información sobre las bibliotecas que utiliza. Aquí está el uso de "CommaDelimitedArrayParameterBinder". usando System.Collections.Generic; usando System.Linq; usando System.Threading; usando System.Threading.Tasks; usando System.Web.Http.Controllers; usando System.Web.Http.Metadata; usando System.Web.Http.ModelBinding; usando System.Web.Http.ValueProviders; utilizando System.Web.Http.ValueProviders.Providers;
SteckDEV
6
public class ArrayInputAttribute : ActionFilterAttribute
{
    private readonly string[] _ParameterNames;
    /// <summary>
    /// 
    /// </summary>
    public string Separator { get; set; }
    /// <summary>
    /// cons
    /// </summary>
    /// <param name="parameterName"></param>
    public ArrayInputAttribute(params string[] parameterName)
    {
        _ParameterNames = parameterName;
        Separator = ",";
    }

    /// <summary>
    /// 
    /// </summary>
    public void ProcessArrayInput(HttpActionContext actionContext, string parameterName)
    {
        if (actionContext.ActionArguments.ContainsKey(parameterName))
        {
            var parameterDescriptor = actionContext.ActionDescriptor.GetParameters().FirstOrDefault(p => p.ParameterName == parameterName);
            if (parameterDescriptor != null && parameterDescriptor.ParameterType.IsArray)
            {
                var type = parameterDescriptor.ParameterType.GetElementType();
                var parameters = String.Empty;
                if (actionContext.ControllerContext.RouteData.Values.ContainsKey(parameterName))
                {
                    parameters = (string)actionContext.ControllerContext.RouteData.Values[parameterName];
                }
                else
                {
                    var queryString = actionContext.ControllerContext.Request.RequestUri.ParseQueryString();
                    if (queryString[parameterName] != null)
                    {
                        parameters = queryString[parameterName];
                    }
                }

                var values = parameters.Split(new[] { Separator }, StringSplitOptions.RemoveEmptyEntries)
                    .Select(TypeDescriptor.GetConverter(type).ConvertFromString).ToArray();
                var typedValues = Array.CreateInstance(type, values.Length);
                values.CopyTo(typedValues, 0);
                actionContext.ActionArguments[parameterName] = typedValues;
            }
        }
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        _ParameterNames.ForEach(parameterName => ProcessArrayInput(actionContext, parameterName));
    }
}

Uso:

    [HttpDelete]
    [ArrayInput("tagIDs")]
    [Route("api/v1/files/{fileID}/tags/{tagIDs}")]
    public HttpResponseMessage RemoveFileTags(Guid fileID, Guid[] tagIDs)
    {
        _FileRepository.RemoveFileTags(fileID, tagIDs);
        return Request.CreateResponse(HttpStatusCode.OK);
    }

Solicitar uri

http://localhost/api/v1/files/2a9937c7-8201-59b7-bc8d-11a9178895d0/tags/BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63,BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63
Waninlezu
fuente
@Elsa ¿Podría indicar qué pieza no puede entender? Creo que el código es bastante claro para explicarse por sí mismo. Es difícil para mí explicar todo esto en inglés, lo siento.
Waninlezu
@ Steve Czetty aquí está mi versión reconstruida, gracias por su idea
Waninlezu
¿ /Funcionará con el separador? Entonces podría tener: dns / root / mystuff / path / to / some / resource mapeado enpublic string GetMyStuff(params string[] pathBits)
RoboJ1M
5

Si desea hacer una lista / matriz de enteros, la forma más fácil de hacerlo es aceptar la lista de cadenas separada por comas (,) y convertirla en una lista de enteros. No se olvide de mencionar [FromUri] attriubte.su URL se ve así:

...? ID = 71 & accountID = 1,2,3,289,56

public HttpResponseMessage test([FromUri]int ID, [FromUri]string accountID)
{
    List<int> accountIdList = new List<int>();
    string[] arrAccountId = accountId.Split(new char[] { ',' });
    for (var i = 0; i < arrAccountId.Length; i++)
    {
        try
        {
           accountIdList.Add(Int32.Parse(arrAccountId[i]));
        }
        catch (Exception)
        {
        }
    }
}
Vaibhav
fuente
¿Por qué lo usas en List<string>lugar de solo string? solo tendrá una cadena que está 1,2,3,289,56en su ejemplo. Sugeriré una edición.
Daniël Tulp
Trabajó para mi. Sin List<Guid>embargo, me sorprendió que mi controlador no se uniera automáticamente. Tenga en cuenta en Asp.net Core que la anotación es [FromQuery], y no es necesaria.
kitsu.eb
2
Para una versión de Linq de una línea: int [] accountIdArray = accountId.Split (','). Seleccione (i => int.Parse (i)). ToArray (); Evitaría la captura ya que enmascarará a alguien que pasa datos incorrectos.
Steve In CO
3

Haga que el método escriba [HttpPost], cree un modelo que tenga un parámetro int [] y publique con json:

/* Model */
public class CategoryRequestModel 
{
    public int[] Categories { get; set; }
}

/* WebApi */
[HttpPost]
public HttpResponseMessage GetCategories(CategoryRequestModel model)
{
    HttpResponseMessage resp = null;

    try
    {
        var categories = //your code to get categories

        resp = Request.CreateResponse(HttpStatusCode.OK, categories);

    }
    catch(Exception ex)
    {
        resp = Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex);
    }

    return resp;
}

/* jQuery */
var ajaxSettings = {
    type: 'POST',
    url: '/Categories',
    data: JSON.serialize({Categories: [1,2,3,4]}),
    contentType: 'application/json',
    success: function(data, textStatus, jqXHR)
    {
        //get categories from data
    }
};

$.ajax(ajaxSettings);
codeMonkey
fuente
Está envolviendo su matriz en una clase, eso funcionará bien (a pesar de MVC / WebAPI). El OP se trataba de vincular a la matriz sin una clase de contenedor.
Mrchief
1
El problema original no dice nada acerca de hacerlo sin una clase contenedora, solo que querían usar parámetros de consulta para objetos complejos. Si sigue ese camino demasiado lejos, llegará a un punto en el que necesita la API para recoger un objeto js realmente complejo, y los parámetros de consulta le fallarán. También podría aprender a hacerlo de la manera que funcionará siempre.
codeMonkey
public IEnumerable<Category> GetCategories(int[] categoryIds){- Sí, podrías interpretar de diferentes maneras, supongo. Pero muchas veces, no quiero crear clases de envoltorios por crear envoltorios. Si tiene objetos complejos, eso simplemente funcionará. Apoyar estos casos más simples es lo que no funciona de fábrica, de ahí el OP.
Mrchief
3
Hacer esto vía POSTestá realmente en contra del paradigma REST. Por lo tanto, dicha API no sería una API REST.
Azimuth
1
@Azimuth me da un paradigma en una mano, lo que funciona con .NET en la otra
codeMonkey
3

O simplemente puede pasar una cadena de elementos delimitados y ponerlos en una matriz o lista en el extremo receptor.

Sirentec
fuente
2

Abordé este problema de esta manera.

Utilicé un mensaje de publicación en la API para enviar la lista de enteros como datos.

Luego devolví los datos como un número inevitable.

El código de envío es el siguiente:

public override IEnumerable<Contact> Fill(IEnumerable<int> ids)
{
    IEnumerable<Contact> result = null;
    if (ids!=null&&ids.Count()>0)
    {
        try
        {
            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri("http://localhost:49520/");
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                String _endPoint = "api/" + typeof(Contact).Name + "/ListArray";

                HttpResponseMessage response = client.PostAsJsonAsync<IEnumerable<int>>(_endPoint, ids).Result;
                response.EnsureSuccessStatusCode();
                if (response.IsSuccessStatusCode)
                {
                    result = JsonConvert.DeserializeObject<IEnumerable<Contact>>(response.Content.ReadAsStringAsync().Result);
                }

            }

        }
        catch (Exception)
        {

        }
    }
    return result;
}

El código de recepción es el siguiente:

// POST api/<controller>
[HttpPost]
[ActionName("ListArray")]
public IEnumerable<Contact> Post([FromBody]IEnumerable<int> ids)
{
    IEnumerable<Contact> result = null;
    if (ids != null && ids.Count() > 0)
    {
        return contactRepository.Fill(ids);
    }
    return result;
}

Funciona bien para un registro o muchos registros. El relleno es un método sobrecargado que usa DapperExtensions:

public override IEnumerable<Contact> Fill(IEnumerable<int> ids)
{
    IEnumerable<Contact> result = null;
    if (ids != null && ids.Count() > 0)
    {
        using (IDbConnection dbConnection = ConnectionProvider.OpenConnection())
        {
            dbConnection.Open();
            var predicate = Predicates.Field<Contact>(f => f.id, Operator.Eq, ids);
            result = dbConnection.GetList<Contact>(predicate);
            dbConnection.Close();
        }
    }
    return result;
}

Esto le permite obtener datos de una tabla compuesta (la lista de id) y luego devolver los registros que realmente le interesan de la tabla de destino.

Puede hacer lo mismo con una vista, pero esto le da un poco más de control y flexibilidad.

Además, los detalles de lo que está buscando en la base de datos no se muestran en la cadena de consulta. Tampoco tiene que convertir desde un archivo csv.

Debe tener en cuenta al usar cualquier herramienta como la interfaz web api 2.x, es que las funciones get, put, post, delete, head, etc. tienen un uso general, pero no están restringidas a ese uso.

Por lo tanto, aunque la publicación se usa generalmente en un contexto de creación en la interfaz de la API web, no se limita a ese uso. Es una llamada html normal que se puede usar para cualquier propósito permitido por la práctica html.

Además, los detalles de lo que está sucediendo están ocultos para esos "ojos curiosos" de los que tanto escuchamos en estos días.

La flexibilidad en las convenciones de nomenclatura en la interfaz de la API web 2.x y el uso de llamadas web regulares significa que envía una llamada a la API web que engaña a los intrusos para que piensen que realmente está haciendo otra cosa. Puede usar "POST" para realmente recuperar datos, por ejemplo.

Timothy Dooling
fuente
2

He creado un archivador de modelo personalizado que convierte cualquier valor separado por comas (solo primitivo, decimal, flotante, cadena) en sus matrices correspondientes.

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

Y cómo usarlo en el controlador:

 public IHttpActionResult Get([ModelBinder(BinderType = typeof(CommaSeparatedToArrayBinder<int>))] int[] ids)
        {
            return Ok(ids);
        }
Sulabh Singla
fuente
¡Gracias, lo he portado a netcore 3.1 con poco esfuerzo y funciona! La respuesta aceptada no resuelve el problema con la necesidad de especificar el nombre del parámetro muchas veces y es lo mismo que la operación predeterminada en netcore 3.1
Bogdan Mart
0

Mi solución fue crear un atributo para validar cadenas, hace un montón de características adicionales comunes, incluida la validación de expresiones regulares que puede usar para verificar solo los números y luego convertirlos a enteros según sea necesario ...

Así es como usas:

public class MustBeListAndContainAttribute : ValidationAttribute
{
    private Regex regex = null;
    public bool RemoveDuplicates { get; }
    public string Separator { get; }
    public int MinimumItems { get; }
    public int MaximumItems { get; }

    public MustBeListAndContainAttribute(string regexEachItem,
        int minimumItems = 1,
        int maximumItems = 0,
        string separator = ",",
        bool removeDuplicates = false) : base()
    {
        this.MinimumItems = minimumItems;
        this.MaximumItems = maximumItems;
        this.Separator = separator;
        this.RemoveDuplicates = removeDuplicates;

        if (!string.IsNullOrEmpty(regexEachItem))
            regex = new Regex(regexEachItem, RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase);
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var listOfdValues = (value as List<string>)?[0];

        if (string.IsNullOrWhiteSpace(listOfdValues))
        {
            if (MinimumItems > 0)
                return new ValidationResult(this.ErrorMessage);
            else
                return null;
        };

        var list = new List<string>();

        list.AddRange(listOfdValues.Split(new[] { Separator }, System.StringSplitOptions.RemoveEmptyEntries));

        if (RemoveDuplicates) list = list.Distinct().ToList();

        var prop = validationContext.ObjectType.GetProperty(validationContext.MemberName);
        prop.SetValue(validationContext.ObjectInstance, list);
        value = list;

        if (regex != null)
            if (list.Any(c => string.IsNullOrWhiteSpace(c) || !regex.IsMatch(c)))
                return new ValidationResult(this.ErrorMessage);

        return null;
    }
}
Alan Cardoso
fuente