Ein Array von Ganzzahlen an die ASP.NET-Web-API übergeben?


425

Ich habe einen REST-Service der ASP.NET-Web-API (Version 4), bei dem ich ein Array von Ganzzahlen übergeben muss.

Hier ist meine Aktionsmethode:

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

Und das ist die URL, die ich versucht habe:

/Categories?categoryids=1,2,3,4

1
Bei Verwendung eines Abfragerings wie "/ Categories? Categoriesids = 1 & categoryids = 2 & categoryids = 3" wurde der Fehler "Mehrere Parameter können nicht an den Inhalt der Anforderung gebunden werden" angezeigt. Hoffe, das bringt Leute hierher, die den gleichen Fehler bekommen haben.
Josh Noe

1
@ Josh Hast du [FromUri] benutzt? public IEnumerable <Category> GetCategories ([FromUri] int [] categoryids) {...}
Anup Kattel

2
@FrankGorman Nein, das war ich nicht, was mein Problem war.
Josh Noe

Antworten:


618

Sie müssen nur [FromUri]vor dem Parameter hinzufügen , sieht aus wie:

GetCategories([FromUri] int[] categoryIds)

Und Anfrage senden:

/Categories?categoryids=1&categoryids=2&categoryids=3 

18
Was ist, wenn ich nicht weiß, wie viele Variablen ich im Array habe? Was ist, wenn es wie 1000 ist? Die Anfrage sollte nicht so sein.
Sahar Ch.

7
Dies gibt mir die Fehlermeldung "Ein Element mit demselben Schlüssel wurde bereits hinzugefügt." Es akzeptiert jedoch Kategorie-IDs [0] = 1 & Kategorie-IDs [1] = 2 & etc ...
Doctor Jones

19
Dies sollte die akzeptierte Antwort sein - @Hemanshu Bhojak: Ist es nicht an der Zeit, Ihre Wahl zu treffen?
David Rettenbacher

12
Dieser Grund dafür liegt in der folgenden Anweisung der ASP.NET-Web-API-Website, die sich mit der Parameterbindung befasst: "Wenn der Parameter ein" einfacher "Typ ist, versucht die Web-API, den Wert vom URI abzurufen. Zu den einfachen Typen gehören die. Primitive NET-Typen (int, bool, double usw.) sowie TimeSpan, DateTime, Guid, Dezimalzahl und Zeichenfolge sowie alle Typen mit einem Typkonverter, der aus einer Zeichenfolge konvertiert werden kann. " Ein int [] ist kein einfacher Typ.
Tr1stan

3
Das funktioniert gut für mich. Ein Punkt. Auf dem Servercode muss der Array-Parameter an erster Stelle stehen, damit er funktioniert, und danach alle anderen Parameter. Bei der Eingabe der Parameter in der Anfrage ist die Reihenfolge unwichtig.
Funken

102

Wie Filip W hervorhebt, müssen Sie möglicherweise auf einen benutzerdefinierten Modellordner wie diesen zurückgreifen (geändert, um an den tatsächlichen Parametertyp zu binden):

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

Und dann kann man sagen:

/Categories?categoryids=1,2,3,4und die ASP.NET-Web-API bindet Ihr categoryIdsArray korrekt .


10
Dies kann gegen SRP und / oder SoC verstoßen, aber Sie können dies auch leicht erben lassen, ModelBinderAttributesodass es direkt anstelle der mühsamen Syntax verwendet werden kann, die das typeof()Argument verwendet. Alles, was Sie tun müssen, ist wie folgt zu erben: CommaDelimitedArrayModelBinder : ModelBinderAttribute, IModelBinderund dann einen Standardkonstruktor bereitzustellen, der die Typdefinition auf die Basisklasse herunterschiebt : public CommaDelimitedArrayModelBinder() : base(typeof(CommaDelimitedArrayModelBinder)) { }.
Sliderhouserules

Ansonsten gefällt mir diese Lösung sehr gut und ich verwende sie in meinem Projekt, also ... danke. :)
Sliderhouserules

Aa a side note, ist diese Lösung nicht mit Generika wie arbeitet System.Collections.Generic.List<long>als bindingContext.ModelType.GetElementType()nur Unterstützung System.ArrayTypen
ViRuSTriNiTy

@ViRuSTriNiTy: Diese Frage und die Antwort beziehen sich speziell auf Arrays. Wenn Sie eine generische listenbasierte Lösung benötigen, ist die Implementierung ziemlich trivial. Sie können gerne eine separate Frage stellen, wenn Sie sich nicht sicher sind, wie Sie vorgehen sollen.
Mrchief

2
@codeMonkey: Das Einfügen des Arrays in den Body ist für eine POST-Anforderung sinnvoll, aber was ist mit GET-Anforderungen? Diese haben normalerweise keinen Inhalt im Körper.
stakx - nicht mehr beitragen

40

Ich bin kürzlich selbst auf diese Anforderung gestoßen und habe beschlossen, eine ActionFilterzu implementieren , um dies zu handhaben.

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

Ich wende es so an (beachte, dass ich 'id' verwendet habe, nicht 'ids', da es so in meiner Route angegeben ist):

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

Und die öffentliche URL wäre:

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

Möglicherweise müssen Sie dies umgestalten, um Ihre spezifischen Anforderungen zu erfüllen.


1
Typ int sind in Ihrer Lösung fest codiert (int.Parse). Imho, @ Mrchiefs Lösung ist besser
Rason

27

Für den Fall, dass jemand das Gleiche oder Ähnliches (wie Löschen) über POSTanstelle von FromUri, verwenden Sie FromBodyund auf clientseitiger (JS / jQuery) Formatparameter als erreichen müsste$.param({ '': categoryids }, true)

c #:

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

jQuery:

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

Die Sache mit $.param({ '': categoryids }, true)ist, dass es .net erwartet, dass der Post-Body einen urlencodierten Wert enthält, wie =1&=2&=3ohne Parameternamen und ohne Klammern.


2
Sie müssen nicht auf einen POST zurückgreifen. Siehe @Lavel Antwort.
André Werlang

3
Die Anzahl der Daten, die Sie in einem URI senden können, ist begrenzt. Und standardmäßig sollte dies keine GET-Anforderung sein, da tatsächlich Daten geändert werden.
Worthy7

1
Und wo genau hast du hier ein GET gesehen? :)
Sofija

3
@Sofija OP sagt code to retrieve categories from database, daher sollte die Methode eine GET-Methode sein, nicht POST.
Azimut

22

Einfache Möglichkeit, Array-Parameter an die Web-API zu senden

API

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

Jquery: JSON-Objekt als Anforderungsparameter senden

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

Es wird Ihre Anforderungs-URL wie generiert ../api/categories/GetCategories?categoryIds=1&categoryIds=2&categoryIds=3&categoryIds=4


3
Wie unterscheidet sich das von der akzeptierten Antwort? mit Ausnahme der Implementierung einer Ajax-Anfrage über jquery, die nichts mit dem ursprünglichen Beitrag zu tun hatte.
Sksallaj

13

Sie können diesen Code versuchen, um durch Kommas getrennte Werte / ein Array von Werten zu verwenden, um einen JSON von webAPI zurückzugewinnen

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

Ausgabe :

[
{"CategoryID":"4","CategoryName":"Category_4"}, 
{"CategoryID":"5","CategoryName":"Category_5"}, 
{"CategoryID":"3","CategoryName":"Category_3"} 
]

12

ASP.NET Core 2.0-Lösung (Swagger Ready)

Eingang

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

Code

Schreiben Sie den Anbieter (woher MVC weiß, welcher Ordner verwendet werden soll)

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

Schreiben Sie den eigentlichen Ordner (greifen Sie auf alle möglichen Informationen zu Anfrage, Aktion, Modellen, Typen usw. zu).

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

Registrieren Sie es bei MVC

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

Beispielnutzung mit einem gut dokumentierten Controller für 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);

BEARBEITEN: Microsoft empfiehlt, für diese untergeordneten Elemente einen TypeConverter für diesen Ansatz zu verwenden. Befolgen Sie daher die folgenden Hinweise für Poster und dokumentieren Sie Ihren benutzerdefinierten Typ mit einem SchemaFilter.


Ich denke, die MS-Empfehlung, über die Sie sprechen, wird durch diese Antwort erfüllt: stackoverflow.com/a/49563970/4367683
Machado

Hast du das gesehen? github.com/aspnet/Mvc/pull/7967 Es sieht so aus, als hätten sie einen Fix hinzugefügt, um die Analyse von List <whatever> in der Abfragezeichenfolge zu starten, ohne dass ein spezieller Ordner erforderlich ist. Außerdem ist der von Ihnen verlinkte Beitrag nicht ASPNET Core und ich glaube nicht, dass er in meiner Situation hilft.
Victorio Berra

Die beste, nicht hackige Antwort.
Erik Philips

7

Ich habe ursprünglich jahrelang die Lösung @Mrchief verwendet (sie funktioniert hervorragend). Aber als ich Swagger zu meinem Projekt für API-Dokumentation hinzufügte , war mein Endpunkt NICHT angezeigt.

Es hat eine Weile gedauert, aber das habe ich mir ausgedacht. Es funktioniert mit Swagger und Ihre API-Methodensignaturen sehen sauberer aus:

Am Ende können Sie tun:

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

    }
}

Erstellen Sie eine neue Klasse: 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() };
}

Erstellen Sie eine neue Klasse: StringToIntArrayConverter.cs

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

Anmerkungen:


1
Für den Fall, dass jemand anderes Informationen zu den verwendeten Bibliotheken benötigt. Hier ist die Verwendung für "CommaDelimitedArrayParameterBinder". using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using 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));
    }
}

Verwendungszweck:

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

Uri anfordern

http://localhost/api/v1/files/2a9937c7-8201-59b7-bc8d-11a9178895d0/tags/BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63,BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63

@ Elsa Könnten Sie bitte darauf hinweisen, welches Stück Sie nicht verstehen können? Ich denke, der Code ist ziemlich klar, um sich selbst zu erklären. Es fällt mir schwer, das alles auf Englisch zu erklären, sorry.
Waninlezu

@ Steve Czetty hier ist meine rekonstruierte Version, danke für Ihre Idee
Waninlezu

Wird es /als Trenner funktionieren? Dann könnten Sie haben: DNS / Root / Mystuff / Pfad / zu / einige / Ressource zugeordnetpublic string GetMyStuff(params string[] pathBits)
RoboJ1M

6

Anstatt einen benutzerdefinierten ModelBinder zu verwenden, können Sie auch einen benutzerdefinierten Typ mit einem TypeConverter verwenden.

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

Der Vorteil besteht darin, dass die Parameter der Web-API-Methode sehr einfach sind. Sie müssen nicht einmal [FromUri] angeben.

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

Dieses Beispiel bezieht sich auf eine Liste von Zeichenfolgen, die Sie jedoch ausführen können categoryIds.Select(int.Parse) bezieht auch eine IntList erstellen oder einfach schreiben.


Verstehe nicht, warum diese Lösung nicht viele Stimmen bekommen hat. Es ist schön und sauber und funktioniert mit Prahlerei, ohne benutzerdefinierte Ordner und andere Dinge hinzuzufügen.
Thieme

Die beste / sauberste Antwort meiner Meinung nach. Danke PhillipM!
Leigh Bowers

5

Wenn Sie eine Liste / ein Array von Ganzzahlen auflisten möchten, akzeptieren Sie am einfachsten die durch Kommas (,) getrennte Liste der Zeichenfolgen und konvertieren Sie sie in eine Liste von Ganzzahlen. Vergessen Sie nicht, [FromUri] attriubte zu erwähnen. Ihre URL sieht folgendermaßen aus:

...? 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)
        {
        }
    }
}

warum benutzt du List<string>statt nur string? Es wird nur eine Zeichenfolge enthalten sein, die sich 1,2,3,289,56in Ihrem Beispiel befindet. Ich werde eine Bearbeitung vorschlagen.
Daniël Tulp

Hat für mich gearbeitet. Ich war überrascht, dass mein Controller sich nicht List<Guid>automatisch an einen binden würde . Beachten Sie, dass in Asp.net Core die Anmerkung vorhanden ist [FromQuery]und nicht benötigt wird.
Kitsu.eb

2
Für eine einzeilige Linq-Version: int [] accountIdArray = accountId.Split (','). Wählen Sie (i => int.Parse (i)). ToArray (); Ich würde den Haken vermeiden, da er jemanden maskiert, der schlechte Daten weitergibt.
Steve In CO

3

Machen Sie den Methodentyp [HttpPost], erstellen Sie ein Modell mit einem int [] -Parameter und posten Sie mit 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);

Sie verpacken Ihr Array in eine Klasse - das funktioniert einwandfrei (trotz MVC / WebAPI). Beim OP ging es darum, ohne Wrapper-Klasse an ein Array zu binden.
Mrchief

1
Das ursprüngliche Problem sagt nichts darüber aus, es ohne eine Wrapper-Klasse zu tun, nur dass sie Abfrageparameter für komplexe Objekte verwenden wollten. Wenn Sie diesen Pfad zu weit gehen, gelangen Sie zu einem Punkt, an dem Sie die API benötigen, um ein wirklich komplexes js-Objekt aufzunehmen, und Abfrageparameter schlagen fehl. Könnte auch lernen, es so zu machen, wie es jedes Mal funktioniert.
CodeMonkey

public IEnumerable<Category> GetCategories(int[] categoryIds){- Ja, Sie könnten auf verschiedene Arten interpretieren, nehme ich an. Aber oft möchte ich keine Wrapper-Klassen erstellen, um Wrapper zu erstellen. Wenn Sie komplexe Objekte haben, funktioniert das einfach. Die Unterstützung dieser einfacheren Fälle funktioniert nicht sofort, daher das OP.
Mrchief

3
Dies über zu tun POSTist eigentlich gegen das REST-Paradigma. Somit wäre eine solche API keine REST-API.
Azimut

1
@ Azimuth geben mir ein Paradigma in der einen Hand, was mit .NET in der anderen
funktioniert

3

Oder Sie übergeben einfach eine Zeichenfolge mit begrenzten Elementen und fügen sie auf der Empfangsseite in ein Array oder eine Liste ein.


2

Ich habe dieses Problem auf diese Weise angesprochen.

Ich habe eine Post-Nachricht an die API verwendet, um die Liste der Ganzzahlen als Daten zu senden.

Dann habe ich die Daten als ienumerable zurückgegeben.

Der Sendecode lautet wie folgt:

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

Der Empfangscode lautet wie folgt:

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

Es funktioniert gut für einen Datensatz oder mehrere Datensätze. Die Füllung ist eine überladene Methode mit 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;
}

Auf diese Weise können Sie Daten aus einer zusammengesetzten Tabelle (der ID-Liste) abrufen und dann die Datensätze, an denen Sie wirklich interessiert sind, aus der Zieltabelle zurückgeben.

Sie könnten dasselbe mit einer Ansicht tun, aber dies gibt Ihnen ein wenig mehr Kontrolle und Flexibilität.

Außerdem werden die Details dessen, was Sie in der Datenbank suchen, nicht in der Abfragezeichenfolge angezeigt. Sie müssen auch nicht aus einer CSV-Datei konvertieren.

Wenn Sie ein Tool wie die Web-API 2.x-Oberfläche verwenden, müssen Sie berücksichtigen, dass die Funktionen zum Abrufen, Einfügen, Veröffentlichen, Löschen, Kopf usw. eine allgemeine Verwendung haben, jedoch nicht auf diese Verwendung beschränkt sind.

Während Post im Allgemeinen in einem Erstellungskontext in der Web-API-Oberfläche verwendet wird, ist es nicht auf diese Verwendung beschränkt. Es ist ein regulärer HTML-Aufruf, der für jeden von der HTML-Praxis zugelassenen Zweck verwendet werden kann.

Darüber hinaus sind die Details dessen, was vor sich geht, vor diesen "neugierigen Augen" verborgen, von denen wir heutzutage so viel hören.

Die Flexibilität bei der Benennung von Konventionen in der Web-API 2.x-Oberfläche und die Verwendung von regulären Web-Anrufen bedeutet, dass Sie einen Anruf an die Web-API senden, der Snoopers in die Irre führt, zu glauben, dass Sie wirklich etwas anderes tun. Mit "POST" können Sie beispielsweise wirklich Daten abrufen.


2

Ich habe einen benutzerdefinierten Modellordner erstellt, der alle durch Kommas getrennten Werte (nur Grundelemente, Dezimalzahlen, Gleitkommazahlen, Zeichenfolgen) in die entsprechenden Arrays konvertiert.

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

Und wie man in Controller verwendet:

 public IHttpActionResult Get([ModelBinder(BinderType = typeof(CommaSeparatedToArrayBinder<int>))] int[] ids)
        {
            return Ok(ids);
        }

Danke, ich habe es mit wenig Aufwand auf Netcore 3.1 portiert und es funktioniert! Akzeptierte Antworten lösen das Problem nicht, da der Parametername viele Male angegeben werden muss. Dies entspricht der Standardoperation in Netcore 3.1
Bogdan Mart

0

Meine Lösung bestand darin, ein Attribut zum Validieren von Zeichenfolgen zu erstellen. Es enthält eine Reihe zusätzlicher allgemeiner Funktionen, einschließlich der Regex-Validierung, mit der Sie nur nach Zahlen suchen können. Später konvertiere ich nach Bedarf in Ganzzahlen.

So verwenden Sie:

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;
    }
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.