Passer un tableau d'entiers à l'API Web ASP.NET?

428

J'ai un service REST ASP.NET Web API (version 4) où je dois passer un tableau d'entiers.

Voici ma méthode d'action:

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

Et voici l'URL que j'ai essayé:

/Categories?categoryids=1,2,3,4
Hemanshu Bhojak
la source
1
J'obtenais une erreur «Impossible de lier plusieurs paramètres au contenu de la demande» lors de l'utilisation d'une chaîne de requête comme «/ Categories? Categoryids = 1 & categoryids = 2 & categoryids = 3». J'espère que cela amènera ici des gens qui recevaient la même erreur.
Josh Noe
1
@Josh Avez-vous utilisé [FromUri]? public IEnumerable <Category> GetCategories ([FromUri] int [] categoryids) {...}
Anup Kattel
2
@FrankGorman Non, je ne l'étais pas, ce qui était mon problème.
Josh Noe

Réponses:

619

Vous avez juste besoin d'ajouter [FromUri]avant le paramètre, ressemble à:

GetCategories([FromUri] int[] categoryIds)

Et envoyez la demande:

/Categories?categoryids=1&categoryids=2&categoryids=3 
Lavel
la source
18
Et si je ne sais pas combien de variables j'ai dans le tableau? Et si c'était comme 1000? La demande ne devrait pas être comme ça.
Sahar Ch.
7
Cela me donne une erreur "Un élément avec la même clé a déjà été ajouté.". Il accepte cependant les categoryids [0] = 1 & categoryids [1] = 2 & etc ...
Docteur Jones
19
Cela devrait être la réponse acceptée - @Hemanshu Bhojak: n'est-il pas temps de faire votre choix?
David Rettenbacher
12
Cette raison est due à la déclaration suivante du site Web de l'API Web ASP.NET parlant de la liaison de paramètres: "Si le paramètre est de type" simple ", l'API Web essaie d'obtenir la valeur de l'URI. Les types simples incluent le. Types primitifs NET (int, bool, double, etc.), plus TimeSpan, DateTime, Guid, décimal et chaîne, ainsi que tout type avec un convertisseur de type qui peut convertir à partir d'une chaîne. " un int [] n'est pas un type simple.
Tr1stan
3
Cela fonctionne bien pour moi. Un point. Sur le code du serveur, le paramètre de tableau doit venir en premier pour qu'il fonctionne et tous les autres paramètres, après. Lors de l'alimentation des paramètres de la demande, la commande est sans importance.
Sparked
102

Comme le souligne Filip W , vous devrez peut-être recourir à un classeur de modèle personnalisé comme celui-ci (modifié pour se lier au type réel de paramètre):

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

Et puis vous pouvez dire:

/Categories?categoryids=1,2,3,4et l'API Web ASP.NET lieront correctement votre categoryIdsbaie.

Mrchief
la source
10
Cela peut violer SRP et / ou SoC, mais vous pouvez facilement en faire hériter ModelBinderAttributeafin qu'il puisse être utilisé directement au lieu de la syntaxe laborieuse utilisant l' typeof()argument. Tout ce que vous avez à faire est possèdes comme ceci: CommaDelimitedArrayModelBinder : ModelBinderAttribute, IModelBinderpuis fournir un constructeur par défaut qui pousse la définition de type jusqu'à la classe de base: public CommaDelimitedArrayModelBinder() : base(typeof(CommaDelimitedArrayModelBinder)) { }.
sliderhouserules
Sinon, j'aime vraiment cette solution et je l'utilise dans mon projet, alors ... merci. :)
sliderhouserules
Aa une note de côté, cette solution ne fonctionne pas avec les génériques comme System.Collections.Generic.List<long>comme bindingContext.ModelType.GetElementType()support uniquement les System.Arraytypes
ViRuSTriNiTy
@ViRuSTriNiTy: Cette question et la réponse parlent spécifiquement des tableaux. Si vous avez besoin d'une solution basée sur une liste générique, c'est assez simple à mettre en œuvre. N'hésitez pas à poser une question distincte si vous ne savez pas comment procéder.
Mrchief
2
@codeMonkey: mettre le tableau dans le corps est logique pour une requête POST, mais qu'en est-il des requêtes GET? Ceux-ci n'ont généralement pas de contenu dans le corps.
stakx - ne contribue plus
40

J'ai récemment rencontré cette exigence moi-même et j'ai décidé de mettre en œuvre un ActionFilterpour gérer cela.

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

Je l'applique comme ça (notez que j'ai utilisé 'id', pas 'ids', car c'est ainsi qu'il est spécifié dans mon itinéraire):

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

Et l'URL publique serait:

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

Vous devrez peut-être refactoriser cela pour répondre à vos besoins spécifiques.

Steve Czetty
la source
1
les types int sont codés en dur (int.Parse) dans votre solution. À mon humble avis, la solution de Mrchief est meilleure
razon
27

Dans le cas où quelqu'un aurait besoin de réaliser la même chose ou une chose similaire (comme supprimer) via POSTau lieu de FromUri, utiliser FromBodyet du côté du client (JS / jQuery) param param comme$.param({ '': categoryids }, true)

c #:

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

jQuery:

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

Le problème avec, $.param({ '': categoryids }, true)c'est que .net s'attend à ce que le corps du message contienne une valeur encodée comme =1&=2&=3sans nom de paramètre et sans crochets.

Sofija
la source
2
Pas besoin de recourir à un POST. Voir la réponse @Lavel.
André Werlang
3
La quantité de données que vous pouvez envoyer dans un URI est limitée. Et en standard, cela ne devrait pas être une demande GET car il s'agit en fait de modifier des données.
Worthy7
1
Et où exactement avez-vous vu un GET ici? :)
Sofija
3
@Sofija OP dit code to retrieve categories from database, donc la méthode devrait être une méthode GET, pas POST.
Azimuth
22

Un moyen simple d'envoyer des paramètres de tableau vers une API Web

API

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

Jquery: envoyer un objet JSON en tant que paramètres de demande

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

Il générera votre URL de demande comme ../api/categories/GetCategories?categoryIds=1&categoryIds=2&categoryIds=3&categoryIds=4

Jignesh Variya
la source
3
en quoi est-ce différent de la réponse acceptée? à l'exception de l'implémentation d'une requête ajax via jquery qui n'a rien à voir avec la publication d'origine.
sksallaj
13

Vous pouvez essayer ce code pour prendre des valeurs séparées par des virgules / un tableau de valeurs pour récupérer un JSON à partir 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; }
 } 

Production :

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

Solution ASP.NET Core 2.0 (Swagger Ready)

Contribution

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

Code

Écrivez le fournisseur (comment MVC sait quel classeur utiliser)

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

Écrivez le classeur réel (accédez à toutes sortes d'informations sur la demande, l'action, les modèles, les types, etc.)

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

Enregistrez-le avec MVC

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

Exemple d'utilisation avec un contrôleur bien documenté pour 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 recommande d'utiliser un TypeConverter pour ces enfants d'opérations sur cette approche. Suivez donc les conseils d'affiches ci-dessous et documentez votre type personnalisé avec un SchemaFilter.

Victorio Berra
la source
Je pense que la recommandation MS dont vous parlez est satisfaite par cette réponse: stackoverflow.com/a/49563970/4367683
Machado
As-tu vu ça? github.com/aspnet/Mvc/pull/7967 on dirait qu'ils ont ajouté un correctif pour démarrer l'analyse de List <wwhat> dans la chaîne de requête sans avoir besoin d'un classeur spécial. De plus, le message que vous avez lié n'est pas ASPNET Core et je ne pense pas que cela aide ma situation.
Victorio Berra
La meilleure réponse, non hacky.
Erik Philips
7

Au lieu d'utiliser un ModelBinder personnalisé, vous pouvez également utiliser un type personnalisé avec 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);
    }
}

L'avantage est qu'il rend les paramètres de la méthode API Web très simples. Vous n'avez même pas besoin de spécifier [FromUri].

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

Cet exemple concerne une liste de chaînes, mais vous pouvez faire categoryIds.Select(int.Parse)ou simplement écrire une IntList à la place.

PhillipM
la source
Je ne comprends pas pourquoi cette solution n'a pas obtenu beaucoup de votes. Il est agréable et propre et fonctionne avec swagger sans ajouter de liants personnalisés et d'autres choses.
Thieme
La meilleure réponse / la plus propre à mon avis. Merci PhillipM!
Leigh Bowers,
7

J'ai utilisé à l'origine la solution qui @Mrchief pendant des années (cela fonctionne très bien). Mais quand j'ai ajouté Swagger à mon projet pour la documentation de l'API, mon point final ne s'est PAS présenté.

Cela m'a pris du temps, mais c'est ce que j'ai trouvé. Cela fonctionne avec Swagger et les signatures de votre méthode API sont plus propres:

En fin de compte, vous pouvez faire:

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

    }
}

Créez une nouvelle classe: 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() };
}

Créez une nouvelle classe: StringToIntArrayConverter.cs

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

Remarques:

  • https://stackoverflow.com/a/47123965/862011 m'a pointé dans la bonne direction
  • Swagger ne réussissait pas à choisir mes points de terminaison séparés par des virgules lors de l'utilisation de l'attribut [Route]
CrabCRUSHERclamCOLLECTOR
la source
1
Dans le cas où quelqu'un d'autre aurait besoin d'informations sur les bibliothèques qu'il utilise. Voici l'utilisation de "CommaDelimitedArrayParameterBinder". using System.Collections.Generic; using System.Linq; en utilisant System.Threading; using System.Threading.Tasks; en utilisant System.Web.Http.Controllers; using System.Web.Http.Metadata; using System.Web.Http.ModelBinding; using System.Web.Http.ValueProviders; using 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));
    }
}

Usage:

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

Demander uri

http://localhost/api/v1/files/2a9937c7-8201-59b7-bc8d-11a9178895d0/tags/BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63,BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63
Waninlezu
la source
@Elsa Pourriez-vous s'il vous plaît indiquer quelle pièce vous ne pouvez pas comprendre? Je pense que le code est assez clair pour l'expliquer lui-même. Il m'est difficile d'expliquer tout cela en anglais, désolé.
Waninlezu
@Steve Czetty voici ma version reconstruite, merci pour votre idée
Waninlezu
Cela fonctionnera-t-il avec /le séparateur? Ensuite, vous pourriez avoir: dns / root / mystuff / path / to / some / resource mapped topublic string GetMyStuff(params string[] pathBits)
RoboJ1M
5

Si vous voulez lister / tableau d'entiers, la façon la plus simple de le faire est d'accepter la liste de chaînes séparée par des virgules (,) et de la convertir en liste d'entiers. N'oubliez pas de mentionner [FromUri] attriubte.votre URL ressemble à:

...? 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
la source
pourquoi utilisez-vous List<string>au lieu de juste string? il n'aura qu'une seule chaîne qui est 1,2,3,289,56dans votre exemple. Je proposerai une modification.
Daniël Tulp
A travaillé pour moi. List<Guid>Cependant, j'ai été surpris que mon contrôleur ne se lie pas automatiquement à un . Notez que dans Asp.net Core, l'annotation est [FromQuery], et elle n'est pas nécessaire.
kitsu.eb
2
Pour une version Linq à une ligne: int [] accountIdArray = accountId.Split (','). Sélectionnez (i => int.Parse (i)). ToArray (); J'éviterais la capture car cela masquera quelqu'un qui passe de mauvaises données.
Steve In CO
3

Faites le type de méthode [HttpPost], créez un modèle qui a un paramètre int [] et postez avec 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
la source
Vous encapsulez votre tableau dans une classe - cela fonctionnera bien (malgré MVC / WebAPI). L'OP concernait la liaison à un tableau sans classe wrapper.
Mrchief
1
Le problème d'origine ne dit rien de le faire sans une classe wrapper, juste qu'ils voulaient utiliser des paramètres de requête pour des objets complexes. Si vous suivez ce chemin trop loin, vous arriverez à un point où vous aurez besoin de l'API pour récupérer un objet js vraiment complexe, et les paramètres de requête vous échoueront. Autant apprendre à le faire de la manière qui fonctionnera à chaque fois.
codeMonkey
public IEnumerable<Category> GetCategories(int[] categoryIds){- ouais tu pourrais interpréter de différentes manières je suppose. Mais plusieurs fois, je ne veux pas créer de classes wrapper pour le plaisir de créer des wrappers. Si vous avez des objets complexes, cela fonctionnera. La prise en charge de ces cas plus simples est ce qui ne fonctionne pas dès le départ, d'où l'OP.
Mrchief
3
Faire cela via POSTest en fait contraire au paradigme REST. Une telle API ne serait donc pas une API REST.
Azimuth
1
@Azimuth me donne un paradigme dans une main, ce qui fonctionne avec .NET dans l'autre
codeMonkey
3

Ou vous pouvez simplement passer une chaîne d'éléments délimités et la placer dans un tableau ou une liste à l'extrémité de réception.

Sirentec
la source
2

J'ai abordé cette question de cette façon.

J'ai utilisé un message de poste à l'API pour envoyer la liste des entiers en tant que données.

Ensuite, j'ai renvoyé les données sous forme d'innombrables.

Le code d'envoi est le suivant:

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

Le code de réception est le suivant:

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

Cela fonctionne très bien pour un ou plusieurs enregistrements. Le remplissage est une méthode surchargée utilisant 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;
}

Cela vous permet d'extraire des données d'une table composite (la liste des identifiants), puis de renvoyer les enregistrements qui vous intéressent vraiment dans la table cible.

Vous pouvez faire de même avec une vue, mais cela vous donne un peu plus de contrôle et de flexibilité.

De plus, les détails de ce que vous recherchez dans la base de données ne sont pas affichés dans la chaîne de requête. Vous n'avez pas non plus à convertir à partir d'un fichier csv.

Vous devez garder à l'esprit lorsque vous utilisez un outil comme l'interface web api 2.x, les fonctions get, put, post, delete, head, etc. ont une utilisation générale, mais ne sont pas limitées à cette utilisation.

Ainsi, bien que la publication soit généralement utilisée dans un contexte de création dans l'interface Web API, elle n'est pas limitée à cette utilisation. Il s'agit d'un appel html régulier qui peut être utilisé à toutes fins autorisées par la pratique html.

De plus, les détails de ce qui se passe sont cachés à ces "regards indiscrets" dont on entend tant parler ces jours-ci.

La flexibilité des conventions de dénomination dans l'interface Web Api 2.x et l'utilisation d'appels Web réguliers signifie que vous envoyez un appel à l'API Web qui induit les espions en erreur en pensant que vous faites vraiment autre chose. Vous pouvez utiliser "POST" pour récupérer vraiment des données, par exemple.

Timothy Dooling
la source
2

J'ai créé un classeur de modèle personnalisé qui convertit toutes les valeurs séparées par des virgules (uniquement primitives, décimales, flottantes, chaîne) en leurs tableaux correspondants.

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

Et comment utiliser dans Controller:

 public IHttpActionResult Get([ModelBinder(BinderType = typeof(CommaSeparatedToArrayBinder<int>))] int[] ids)
        {
            return Ok(ids);
        }
Sulabh Singla
la source
Merci, je l'ai porté sur netcore 3.1 avec peu d'effort et ça marche! La réponse acceptée ne résout pas le problème avec la nécessité de spécifier le nom du paramètre plusieurs fois et est la même que l'opération par défaut dans netcore 3.1
Bogdan Mart
0

Ma solution était de créer un attribut pour valider les chaînes, il fait un tas de fonctionnalités communes supplémentaires, y compris la validation regex que vous pouvez utiliser pour vérifier les nombres uniquement, puis plus tard, je convertis en entiers au besoin ...

Voici comment vous utilisez:

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
la source