Czy przekazać tablicę liczb całkowitych do ASP.NET Web API?

427

Mam usługę REST interfejsu API sieci Web platformy ASP.NET (wersja 4), w której muszę przekazać tablicę liczb całkowitych.

Oto moja metoda działania:

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

I to jest adres URL, który próbowałem:

/Categories?categoryids=1,2,3,4
Hemanshu Bhojak
źródło
1
Otrzymałem błąd „Nie mogę powiązać wielu parametrów z treścią żądania”, gdy korzystałem z kwerendy typu „/ Categories? Categoryids = 1 & categoryids = 2 & categoryids = 3”. Mam nadzieję, że sprowadza to ludzi, którzy otrzymywali ten sam błąd.
Josh Noe
1
@Josh Czy używałeś [FromUri]? public IEnumerable <Category> GetCategories ([FromUri] int [] categoryids) {...}
Anup Kattel
2
@FrankGorman Nie, nie byłem, co było moim problemem.
Josh Noe

Odpowiedzi:

619

Musisz tylko dodać [FromUri]przed parametrem, wygląda to tak:

GetCategories([FromUri] int[] categoryIds)

I wyślij zapytanie:

/Categories?categoryids=1&categoryids=2&categoryids=3 
Lavel
źródło
18
Co jeśli nie wiem, ile zmiennych mam w tablicy? Co jeśli to jest 1000? Prośba nie powinna tak wyglądać.
Sahar Ch.
7
Daje mi to błąd: „Element z tym samym kluczem został już dodany.”. Akceptuje jednak kategorieids [0] = 1 i kategorieids [1] = 2 itd.
Doktor Jones
19
To powinna być zaakceptowana odpowiedź - @Hemanshu Bhojak: czy nie czas na wybór?
David Rettenbacher
12
Przyczyną tego jest następująca instrukcja witryny API sieci Web ASP.NET, która mówi o wiązaniu parametrów: „Jeśli parametr jest typem„ prostym ”, interfejs API sieci Web próbuje uzyskać wartość z identyfikatora URI. Typy proste obejmują. Typy pierwotne NET (int, bool, double i tak dalej), a także TimeSpan, DateTime, Guid, decimal i string, a także dowolny typ z konwerterem typów, który może konwertować z łańcucha. " int [] nie jest prostym typem.
Tr1stan,
3
To działa dobrze dla mnie. Jeden punkt. W kodzie serwera parametr tablicy musi być na pierwszym miejscu, aby działał, a po nim wszelkie inne parametry. Podczas wprowadzania parametrów w żądaniu zamówienie nie jest ważne.
Sparked
102

Tak jak wskazuje Filip W , może być konieczne skorzystanie z niestandardowego modelu segregatora, takiego jak ten (zmodyfikowanego w celu powiązania z rzeczywistym typem parametru):

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

A potem możesz powiedzieć:

/Categories?categoryids=1,2,3,4i ASP.NET Web API poprawnie powiąza categoryIdstablicę.

Mrchief
źródło
10
Może to naruszać SRP i / lub SoC, ale możesz łatwo sprawić, że to również odziedziczy, ModelBinderAttributewięc można go użyć bezpośrednio zamiast żmudnej składni za pomocą typeof()argumentu. Wszystko co musisz zrobić, to dziedziczą tak: CommaDelimitedArrayModelBinder : ModelBinderAttribute, IModelBindera następnie dostarczyć domyślny konstruktor, który popycha do definicji typu klasy bazowej: public CommaDelimitedArrayModelBinder() : base(typeof(CommaDelimitedArrayModelBinder)) { }.
sliderhouserules
W przeciwnym razie bardzo podoba mi się to rozwiązanie i używam go w moim projekcie, więc ... dzięki. :)
sliderhouserules,
Na marginesie, to rozwiązanie nie działa z rodzajami, takimi System.Collections.Generic.List<long>jak bindingContext.ModelType.GetElementType()tylko System.Arraytypy wsparcia
ViRuSTriNiTy
@ViRuSTriNiTy: To pytanie i odpowiedź mówią konkretnie o tablicach. Jeśli potrzebujesz ogólnego rozwiązania opartego na liście, jest to dość proste do wdrożenia. Jeśli nie jesteś pewien, jak to zrobić, możesz zadać osobne pytanie.
Mrchief
2
@codeMonkey: umieszczenie tablicy w ciele ma sens w przypadku żądania POST, ale co z żądaniami GET? Zazwyczaj nie mają one zawartości w ciele.
stakx - nie przyczynia się już
40

Niedawno sam spotkałem się z tym wymogiem i zdecydowałem się na wdrożenie ActionFiltertego rozwiązania.

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

Stosuję go w ten sposób (zwróć uwagę, że użyłem „id”, a nie „id”, ponieważ tak to jest określone na mojej trasie):

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

Publiczny adres URL to:

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

Być może trzeba to zmienić, aby spełnić określone potrzeby.

Steve Czetty
źródło
1
wpisz int są zakodowane na stałe (int.Parse) w swoim rozwiązaniu. Imho, rozwiązanie Mrchiefa jest lepsze
Razon
27

Na wypadek, gdyby ktoś potrzebował - aby osiągnąć to samo lub podobne (np. Usunąć) POSTzamiast FromUri, użyjFromBody i po stronie klienta (JS / jQuery) param format jak$.param({ '': categoryids }, true)

do#:

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

jQuery:

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

Chodzi o $.param({ '': categoryids }, true)to, że .net będzie oczekiwać, że treść postu będzie zawierać wartość zakodowaną w sposób jak =1&=2&=3bez nazwy parametru i bez nawiasów.

Sofija
źródło
2
Nie musisz uciekać się do POST. Zobacz odpowiedź @Lavel.
André Werlang,
3
Istnieje limit ilości danych, które można wysłać w URI. I standardowo nie powinno to być żądanie GET, ponieważ faktycznie modyfikuje dane.
Worthy7
1
I gdzie dokładnie widziałeś GET? :)
Sofija,
3
@Sofija OP mówi code to retrieve categories from database, więc metoda powinna być metodą GET, a nie POST.
Azymut
22

Łatwy sposób na przesłanie parametrów tablicy do interfejsu API

API

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

Jquery: wyślij obiekt JSON jako parametry żądania

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

Wygeneruje Twój URL żądania ../api/categories/GetCategories?categoryIds=1&categoryIds=2&categoryIds=3&categoryIds=4

Jignesh Variya
źródło
3
czym różni się to od przyjętej odpowiedzi? z wyjątkiem implementowania żądania ajax za pośrednictwem jquery, które nie miało nic wspólnego z oryginalnym postem.
sksallaj
13

Możesz wypróbować ten kod, aby pobrać wartości oddzielone przecinkami / tablicę wartości, aby odzyskać JSON z 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; }
 } 

Wynik :

[
{"CategoryID":"4","CategoryName":"Category_4"}, 
{"CategoryID":"5","CategoryName":"Category_5"}, 
{"CategoryID":"3","CategoryName":"Category_3"} 
]
Naveen Vijay
źródło
12

Rozwiązanie ASP.NET Core 2.0 (gotowe do Swagger)

Wejście

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

Kod

Napisz dostawcę (skąd MVC wie, jakiego spoiwa użyć)

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

Napisz rzeczywisty segregator (dostęp do wszelkiego rodzaju informacji o żądaniu, akcji, modelach, typach, cokolwiek)

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

Zarejestruj go w MVC

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

Przykładowe użycie z dobrze udokumentowanym kontrolerem 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);

EDYCJA: Microsoft zaleca stosowanie TypeConverter dla tych dzieci operacji zamiast tego podejścia. Postępuj zgodnie z poniższymi wskazówkami dotyczącymi plakatów i udokumentuj swój niestandardowy typ za pomocą SchemaFilter.

Victorio Berra
źródło
Myślę, że zalecenie MS, o którym mówisz, jest spełnione przez tę odpowiedź: stackoverflow.com/a/49563970/4367683
Machado
Widziałeś to? github.com/aspnet/Mvc/pull/7967 wygląda na to, że dodali poprawkę, aby rozpocząć parsowanie listy < kolwiek cokolwiek> w ciągu zapytania bez potrzeby specjalnego spoiwa. Również post, który podłączyłeś, nie jest ASPNET Core i nie sądzę, żeby pomógł w mojej sytuacji.
Victorio Berra
Najlepsza, nie zhackowana odpowiedź.
Erik Philips
7

Zamiast używać niestandardowego ModelBinder, możesz również użyć niestandardowego typu z 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);
    }
}

Zaletą jest to, że sprawia, że ​​parametry metody interfejsu API sieci Web są bardzo proste. Nie musisz nawet określać opcji [FromUri].

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

Ten przykład dotyczy Listy ciągów, ale categoryIds.Select(int.Parse)zamiast tego możesz zrobić lub po prostu napisać IntList.

PhillipM
źródło
Nie rozumiem, dlaczego to rozwiązanie nie uzyskało dużej liczby głosów. Jest ładny i czysty i działa ze swaggerem bez dodawania niestandardowych segregatorów i innych rzeczy.
Thieme,
Najlepsza / najczystsza odpowiedź według mnie. Dzięki PhillipM!
Leigh Bowers
7

Pierwotnie korzystałem z rozwiązania, które @Mrchief od lat (działa świetnie). Ale kiedy dodałem Swagger do mojego projektu dla dokumentacji API, moim punktem końcowym NIE było pojawił się.

Zajęło mi to trochę czasu, ale to właśnie wymyśliłem. Działa z Swagger, a sygnatury metod API wyglądają na czystsze:

Na koniec możesz zrobić:

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

    }
}

Utwórz nową klasę: 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() };
}

Utwórz nową klasę: StringToIntArrayConverter.cs

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

Uwagi:

crabCRUSHERclamCOLLECTOR
źródło
1
Na wypadek, gdyby ktokolwiek potrzebował informacji o bibliotekach, z których korzysta. Oto użycie „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));
    }
}

Stosowanie:

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

Poproś o uri

http://localhost/api/v1/files/2a9937c7-8201-59b7-bc8d-11a9178895d0/tags/BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63,BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63
Waninlezu
źródło
@ Elsa Czy mógłbyś wskazać, którego utworu nie rozumiesz? Myślę, że kod jest dość jasny, aby wyjaśnić to samo. Przykro mi, ale trudno mi to wszystko wytłumaczyć po angielsku.
Waninlezu,
@ Steve Czetty oto moja zrekonstruowana wersja, dzięki za pomysł
Waninlezu,
Czy będzie działał /jako separator? Wtedy można mieć: DNS / root / mystuff / ścieżka / do / some / zasobu odwzorowanym napublic string GetMyStuff(params string[] pathBits)
RoboJ1M
5

Jeśli chcesz wyświetlić listę / tablicę liczb całkowitych, najłatwiej to zrobić, akceptuj listę ciągów oddzielonych przecinkami (,) i przekonwertuj ją na listę liczb całkowitych. Nie zapomnij wspomnieć o [FromUri] attriubte. Twój adres URL wygląda następująco:

...? ID = 71 i ID konta = 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
źródło
dlaczego używasz List<string>zamiast po prostu string? będzie miał tylko jeden ciąg znaków, który jest 1,2,3,289,56w twoim przykładzie. Zasugeruję edycję.
Daniël Tulp,
Pracował dla mnie. Byłem zaskoczony, że mój kontroler nie powiązałby się List<Guid>automatycznie. Uwaga w Asp.net Core adnotacja jest [FromQuery]i nie jest potrzebna.
kitsu.eb
2
W przypadku jednowierszowej wersji Linq: int [] accountIdArray = accountId.Split (','). Wybierz (i => int.Parse (i)). ToArray (); Unikałbym haczyka, ponieważ zamaskowałby on kogoś, kto przekazał złe dane.
Steve In CO
3

Ustaw metodę na typ [HttpPost], utwórz model, który ma jeden parametr int [], i opublikuj za pomocą 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
źródło
Zawijasz tablicę w klasę - to będzie działać dobrze (pomimo MVC / WebAPI). OP dotyczył wiązania z tablicą bez klasy opakowania.
Mrchief
1
Pierwotny problem nie mówi nic o robieniu tego bez klasy opakowania, tylko o tym, że chcieli użyć parametrów zapytania dla złożonych obiektów. Jeśli posuniesz się tą ścieżką zbyt daleko, dojdziesz do punktu, w którym potrzebujesz API, aby wybrać naprawdę skomplikowany obiekt js, a parametry zapytania cię nie powiodą. Równie dobrze można nauczyć się robić to w ten sposób, który będzie działał za każdym razem.
codeMonkey
public IEnumerable<Category> GetCategories(int[] categoryIds){- tak, mógłbym interpretować na różne sposoby, jak sądzę. Ale wiele razy nie chcę tworzyć klas opakowań dla tworzenia opakowań. Jeśli masz złożone obiekty, to po prostu zadziała. Obsługa tych prostszych przypadków jest tym, co nie działa po wyjęciu z pudełka, stąd OP.
Mrchief
3
Robienie tego za pośrednictwem POSTjest w rzeczywistości sprzeczne z paradygmatem REST. Zatem taki interfejs API nie byłby interfejsem API REST.
Azymut
1
@Azimuth daj mi paradygmat z jednej strony, co działa z .NET w drugiej
codeMonkey
3

Lub możesz po prostu przekazać ciąg rozdzielanych elementów i umieścić go w tablicy lub liście na końcu odbierającym.

Sirentec
źródło
2

W ten sposób rozwiązałem ten problem.

Użyłem wiadomości post do interfejsu API, aby wysłać listę liczb całkowitych jako dane.

Następnie zwróciłem dane jako niezliczone.

Kod wysyłający jest następujący:

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

Kod odbierający jest następujący:

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

Działa dobrze dla jednego lub wielu rekordów. Wypełnienie to metoda przeładowana za pomocą 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;
}

Pozwala to pobrać dane z tabeli złożonej (lista identyfikatorów), a następnie zwrócić rekordy, które naprawdę Cię interesują z tabeli docelowej.

Możesz zrobić to samo z widokiem, ale daje to nieco większą kontrolę i elastyczność.

Ponadto szczegóły tego, czego szukasz w bazie danych, nie są wyświetlane w ciągu zapytania. Nie musisz również konwertować z pliku csv.

Podczas korzystania z dowolnego narzędzia, takiego jak interfejs WWW API 2.x, należy pamiętać, że funkcje get, put, post, delete, head itp. Mają ogólne zastosowanie, ale nie są ograniczone do tego zastosowania.

Tak więc, chociaż post jest zwykle używany w kontekście tworzenia w interfejsie interfejsu WWW, nie jest on ograniczony do tego zastosowania. Jest to zwykłe połączenie HTML, którego można używać w dowolnym celu dozwolonym przez praktykę HTML.

Ponadto szczegóły tego, co się dzieje, są ukryte przed tymi „wścibskimi oczami”, które słyszymy o tych dniach.

Elastyczność w konwencjach nazewnictwa w interfejsie API 2.x i korzystanie z regularnych połączeń internetowych oznacza, że ​​wysyłasz wywołanie do interfejsu API, które wprowadza w błąd szpiegów, którzy myślą, że naprawdę robisz coś innego. Możesz na przykład użyć „POST”, aby naprawdę odzyskać dane.

Timothy Dooling
źródło
2

Utworzyłem spoiwo modelu niestandardowego, które konwertuje dowolne wartości oddzielone przecinkami (tylko prymitywne, dziesiętne, zmiennoprzecinkowe, łańcuchowe) na odpowiadające im tablice.

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

I jak korzystać z kontrolera:

 public IHttpActionResult Get([ModelBinder(BinderType = typeof(CommaSeparatedToArrayBinder<int>))] int[] ids)
        {
            return Ok(ids);
        }
Sulabh Singla
źródło
Dzięki, przeniosłem go na Netcore 3.1 przy niewielkim wysiłku i działa! Zaakceptowana odpowiedź nie rozwiązuje problemu z koniecznością wielokrotnego podawania nazwy parametru i jest taka sama jak domyślna operacja w netcore 3.1
Bogdan Mart
0

Moje rozwiązanie polegało na utworzeniu atrybutu do sprawdzania poprawności ciągów, ma on szereg dodatkowych typowych funkcji, w tym sprawdzanie poprawności wyrażeń regularnych, których można użyć do sprawdzania tylko liczb, a następnie w razie potrzeby przekonwertowałem na liczby całkowite ...

Oto jak używasz:

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
źródło