web-dev-qa-db-fra.com

Passer plusieurs objets complexes à une méthode API Web post/put

Certains peuvent-ils m'aider à savoir comment passer plusieurs objets d'une application de console C # au contrôleur API Web, comme indiqué ci-dessous?

using (var httpClient = new System.Net.Http.HttpClient())
{
    httpClient.BaseAddress = new Uri(ConfigurationManager.AppSettings["Url"]);
    httpClient.DefaultRequestHeaders.Accept.Clear();
    httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));   

    var response = httpClient.PutAsync("api/process/StartProcessiong", objectA, objectB);
}

Ma méthode d'API Web est la suivante:

public void StartProcessiong([FromBody]Content content, [FromBody]Config config)
{

}
33
SKumar

Dans la version actuelle de l'API Web, l'utilisation de plusieurs objets complexes (tels que vos objets complexes Content et Config) dans la signature de la méthode de l'API Web est non autorisé. Je parie bien que config (votre deuxième paramètre) revient toujours sous la forme NULL. En effet, un seul objet complexe peut être analysé à partir du corps pour une requête. Pour des raisons de performances, l'accès et l'analyse du corps de la requête de l'API Web sont autorisés uniquement une fois. Ainsi, après l'analyse et l'analyse du corps de la demande pour le paramètre "content", toutes les analyses de corps suivantes se termineront par "NULL". Donc en gros:

  • Un seul élément peut être attribué avec [FromBody].
  • Tout nombre d'éléments peut être attribué avec [FromUri].

Vous trouverez ci-dessous un extrait utile de l'excellent article de blog de Mike Stall (oldie but goldie!). Vous voudrez faire attention à item 4:

Voici les règles de base pour déterminer si un paramètre est lu avec une liaison de modèle ou un formateur:

  1. Si le paramètre n'a pas d'attribut, la décision est prise uniquement sur le type .NET du paramètre. Les "types simples" utilisent la liaison de modèle. Les types complexes utilisent les formateurs. Un "type simple" comprend: primitives , TimeSpan, DateTime, Guid, Decimal, String, ou quelque chose avec un TypeConverter qui convertit à partir de chaînes.
  2. Vous pouvez utiliser un attribut [FromBody] pour spécifier qu'un paramètre doit provenir du corps.
  3. Vous pouvez utiliser un attribut [ModelBinder] sur le paramètre ou le type de paramètre pour spécifier qu'un paramètre doit être lié au modèle. Cet attribut vous permet également de configurer le classeur de modèle. [FromUri] est une instance dérivée de [ModelBinder] qui configure spécifiquement un classeur de modèle pour qu'il ne regarde que dans l'URI.
  4. Le corps ne peut être lu qu'une fois. Donc, si vous avez 2 types complexes dans la signature, au moins un d’eux doit avoir un attribut [ModelBinder].

Pour ces règles, l’un des objectifs clés de la conception était statique et prévisible.

Une différence essentielle entre MVC et Web API réside dans le fait que MVC met le contenu en mémoire tampon (par exemple, le corps de la demande). Cela signifie que la liaison de paramètres de MVC peut rechercher à plusieurs reprises dans le corps pour rechercher des morceaux de paramètres. Alors que dans l'API Web, le corps de la demande (une HttpContent) peut être un flux en lecture seule, infini, non mis en mémoire tampon, ni rembobinable.

Vous pouvez lire vous-même le reste de cet article incroyablement utile. Pour résumer, ce que vous essayez de faire n’est pas actuellement possible de cette façon (vous devez faire preuve de créativité. ). Ce qui suit n'est pas une solution, mais une solution de contournement et une seule possibilité. il y a d'autres moyens.

Solution/Contournement

(Disclaimer: Je ne l'ai pas utilisé moi-même, je suis juste au courant de la théorie!)}

Une "solution" possible consiste à utiliser l'objet JObject . Cet objet fournit un type concret spécialement conçu pour travailler avec JSON.

Vous devez simplement ajuster la signature pour n'accepter qu'un seul objet complexe du corps, la JObject, appelons-le stuff. Ensuite, vous devez analyser manuellement les propriétés de l'objet JSON et utiliser des génériques pour hydrater les types de béton.

Par exemple, voici un exemple quick'n'dirty pour vous donner une idée:

public void StartProcessiong([FromBody]JObject stuff)
{
  // Extract your concrete objects from the json object.
  var content = stuff["content"].ToObject<Content>();
  var config = stuff["config"].ToObject<Config>();

  . . . // Now do your thing!
}

J'ai dit qu'il y avait d'autres moyens, par exemple, vous pouvez simplement envelopper vos deux objets dans un super-objet de votre propre création et le transmettre à votre méthode d'action. Ou vous pouvez simplement éliminer le besoin de deux paramètres complexes dans le corps de la demande en fournissant l'un d'entre eux dans l'URI. Ou ... eh bien, vous obtenez le point.

Permettez-moi de répéter que je n’ai jamais essayé moi-même, même si tout devrait fonctionner en théorie.

48
djikay

Comme @djikay l'a mentionné, vous ne pouvez pas transmettre plusieurs paramètres FromBody.

L’une de mes solutions consiste à définir une CompositeObject,

public class CompositeObject
{
    public Content Content { get; set; }
    public Config Config { get; set; }
}

et que votre WebAPI prenne ceci CompositeObject comme paramètre.

public void StartProcessiong([FromBody] CompositeObject composite)
{ ... }
18
Maggie Ying

Vous pouvez essayer de publier du contenu en plusieurs parties à partir du client, comme suit: 

 using (var httpClient = new HttpClient())
{
    var uri = new Uri("http://example.com/api/controller"));

    using (var formData = new MultipartFormDataContent())
    {
        //add content to form data
        formData.Add(new StringContent(JsonConvert.SerializeObject(content)), "Content");

        //add config to form data
        formData.Add(new StringContent(JsonConvert.SerializeObject(config)), "Config");

        var response = httpClient.PostAsync(uri, formData);
        response.Wait();

        if (!response.Result.IsSuccessStatusCode)
        {
            //error handling code goes here
        }
    }
}

Du côté du serveur, vous pouvez lire le contenu comme ceci:

public async Task<HttpResponseMessage> Post()
{
    //make sure the post we have contains multi-part data
    if (!Request.Content.IsMimeMultipartContent())
    {
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }

    //read data
    var provider = new MultipartMemoryStreamProvider();
    await Request.Content.ReadAsMultipartAsync(provider);

    //declare backup file summary and file data vars
    var content = new Content();
    var config = new Config();

    //iterate over contents to get Content and Config
    foreach (var requestContents in provider.Contents)
    {
        if (requestContents.Headers.ContentDisposition.Name == "Content")
        {
            content = JsonConvert.DeserializeObject<Content>(requestContents.ReadAsStringAsync().Result);
        }
        else if (requestContents.Headers.ContentDisposition.Name == "Config")
        {
            config = JsonConvert.DeserializeObject<Config>(requestContents.ReadAsStringAsync().Result);
        }
    }

    //do something here with the content and config and set success flag
    var success = true;

    //indicate to caller if this was successful
    HttpResponseMessage result = Request.CreateResponse(success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError, success);
    return result;

}

}

8
Brian Wenhold

Je sais que c’est une vieille question, mais j’avais le même problème et voici ce que j’ai trouvé et j’espère être utile à quelqu'un. Cela permettra de passer les paramètres formatés JSON individuellement dans l'URL de requête (GET), sous la forme d'un objet JSON unique après? (GET) ou dans un seul objet corps JSON (POST). Mon objectif était une fonctionnalité de style RPC.

Création d'un attribut personnalisé et d'une liaison de paramètre, héritant de HttpParameterBinding: 

public class JSONParamBindingAttribute : Attribute
{

}

public class JSONParamBinding : HttpParameterBinding
{

    private static JsonSerializer _serializer = JsonSerializer.Create(new JsonSerializerSettings()
    {
        DateTimeZoneHandling = DateTimeZoneHandling.Utc
    });


    public JSONParamBinding(HttpParameterDescriptor descriptor)
        : base(descriptor)
    {
    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
                                                HttpActionContext actionContext,
                                                CancellationToken cancellationToken)
    {
        JObject jobj = GetJSONParameters(actionContext.Request);

        object value = null;

        JToken jTokenVal = null;
        if (!jobj.TryGetValue(Descriptor.ParameterName, out jTokenVal))
        {
            if (Descriptor.IsOptional)
                value = Descriptor.DefaultValue;
            else
                throw new MissingFieldException("Missing parameter : " + Descriptor.ParameterName);
        }
        else
        {
            try
            {
                value = jTokenVal.ToObject(Descriptor.ParameterType, _serializer);
            }
            catch (Newtonsoft.Json.JsonException e)
            {
                throw new HttpParseException(String.Join("", "Unable to parse parameter: ", Descriptor.ParameterName, ". Type: ", Descriptor.ParameterType.ToString()));
            }
        }

        // Set the binding result here
        SetValue(actionContext, value);

        // now, we can return a completed task with no result
        TaskCompletionSource<AsyncVoid> tcs = new TaskCompletionSource<AsyncVoid>();
        tcs.SetResult(default(AsyncVoid));
        return tcs.Task;
    }

    public static HttpParameterBinding HookupParameterBinding(HttpParameterDescriptor descriptor)
    {
        if (descriptor.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<JSONParamBindingAttribute>().Count == 0 
            && descriptor.ActionDescriptor.GetCustomAttributes<JSONParamBindingAttribute>().Count == 0)
            return null;

        var supportedMethods = descriptor.ActionDescriptor.SupportedHttpMethods;

        if (supportedMethods.Contains(HttpMethod.Post) || supportedMethods.Contains(HttpMethod.Get))
        {
            return new JSONParamBinding(descriptor);
        }

        return null;
    }

    private JObject GetJSONParameters(HttpRequestMessage request)
    {
        JObject jobj = null;
        object result = null;
        if (!request.Properties.TryGetValue("ParamsJSObject", out result))
        {
            if (request.Method == HttpMethod.Post)
            {
                jobj = JObject.Parse(request.Content.ReadAsStringAsync().Result);
            }
            else if (request.RequestUri.Query.StartsWith("?%7B"))
            {
                jobj = JObject.Parse(HttpUtility.UrlDecode(request.RequestUri.Query).TrimStart('?'));
            }
            else
            {
                jobj = new JObject();
                foreach (var kvp in request.GetQueryNameValuePairs())
                {
                    jobj.Add(kvp.Key, JToken.Parse(kvp.Value));
                }
            }
            request.Properties.Add("ParamsJSObject", jobj);
        }
        else
        {
            jobj = (JObject)result;
        }

        return jobj;
    }



    private struct AsyncVoid
    {
    }
}

Injectez la règle de liaison dans la méthode Register de WebApiConfig.cs: 

        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.ParameterBindingRules.Insert(0, JSONParamBinding.HookupParameterBinding);

            config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
        }

Cela permet des actions du contrôleur avec des valeurs de paramètre par défaut et une complexité variée, telles que:

[JSONParamBinding]
    [HttpPost, HttpGet]
    public Widget DoWidgetStuff(Widget widget, int stockCount, string comment="no comment")
    {
        ... do stuff, return Widget object
    }

exemple post corps:

{ 
    "widget": { 
        "a": 1, 
        "b": "string", 
        "c": { "other": "things" } 
    }, 
    "stockCount": 42, 
    "comment": "sample code"
} 

ou GET simple paramètre (nécessite un encodage d'URL)

controllerPath/DoWidgetStuff?{"widget":{..},"comment":"test","stockCount":42}

ou GET multiple param (nécessite un encodage d'URL)

controllerPath/DoWidgetStuff?widget={..}&comment="test"&stockCount=42
4
user6775030

Créez un objet complexe pour combiner le contenu et la configuration comme d'autres mentionnés, utilisez dynamique et faites simplement un .ToObject (); comme:

[HttpPost]
public void StartProcessiong([FromBody] dynamic obj)
{
   var complexObj= obj.ToObject<ComplexObj>();
   var content = complexObj.Content;
   var config = complexObj.Config;
}
1
harlandgomez

Voici un autre modèle qui peut vous être utile. C'est pour un Get mais le même principe et le même code s'appliquent pour un Post/Put mais en sens inverse. Cela fonctionne essentiellement sur le principe de la conversion d'objets vers cette classe ObjectWrapper qui conserve le nom du type de l'autre côté:

using Newtonsoft.Json;
using System;
using System.Collections.Generic;

namespace WebAPI
{
    public class ObjectWrapper
    {
        #region Public Properties
        public string RecordJson { get; set; }
        public string TypeFullName { get; set; }
        #endregion

        #region Constructors

        public ObjectWrapper() : this(null, null)
        {
        }

        public ObjectWrapper(object objectForWrapping) : this(objectForWrapping, null)
        {
        }

        public ObjectWrapper(object objectForWrapping, string typeFullName)
        {
            if (typeFullName == null && objectForWrapping != null)
            {
                TypeFullName = objectForWrapping.GetType().FullName;
            }
            else
            {
                TypeFullName = typeFullName;
            }

            RecordJson = JsonConvert.SerializeObject(objectForWrapping);
        }
        #endregion

        #region Public Methods
        public object ToObject()
        {
            var type = Type.GetType(TypeFullName);
            return JsonConvert.DeserializeObject(RecordJson, type);
        }
        #endregion

        #region Public Static Methods
        public static List<ObjectWrapper> WrapObjects(List<object> records)
        {
            var retVal = new List<ObjectWrapper>();
            records.ForEach
            (item =>
            {
                retVal.Add
                (
                    new ObjectWrapper(item)
                );
            }
            );

            return retVal;
        }

        public static List<object> UnwrapObjects(IEnumerable<ObjectWrapper> objectWrappers)
        {
            var retVal = new List<object>();

            foreach(var item in objectWrappers)
            {
                retVal.Add
                (
                    item.ToObject()
                );
            }

            return retVal;
        }
        #endregion
    }
}

Dans le code REST:

[HttpGet]
public IEnumerable<ObjectWrapper> Get()
{
    var records = new List<object>();
    records.Add(new TestRecord1());
    records.Add(new TestRecord2());
    var wrappedObjects = ObjectWrapper.WrapObjects(records);
    return wrappedObjects;
}

Il s'agit du code côté client (UWP) utilisant une bibliothèque client REST. La bibliothèque client utilise simplement la bibliothèque de sérialisation Newtonsoft Json - rien d’extrême.

private static async Task<List<object>> Getobjects()
{
    var result = await REST.Get<List<ObjectWrapper>>("http://localhost:50623/api/values");
    var wrappedObjects = (IEnumerable<ObjectWrapper>) result.Data;
    var unwrappedObjects =  ObjectWrapper.UnwrapObjects(wrappedObjects);
    return unwrappedObjects;
}
0
Melbourne Developer

Le meilleur moyen de transmettre plusieurs objets complexes aux services webapi consiste à utiliser Tuple autre que dynamique, chaîne json, classe personnalisée.

HttpClient.PostAsJsonAsync("http://Server/WebService/Controller/ServiceMethod?number=" + number + "&name" + name, Tuple.Create(args1, args2, args3, args4));

[HttpPost]
[Route("ServiceMethod")]
[ResponseType(typeof(void))]
public IHttpActionResult ServiceMethod(int number, string name, Tuple<Class1, Class2, Class3, Class4> args)
{
    Class1 c1 = (Class1)args.Item1;
    Class2 c2 = (Class2)args.Item2;
    Class3 c3 = (Class3)args.Item3;
    Class4 c4 = (Class4)args.Item4;
    /* do your actions */
    return Ok();
}

Il n'est pas nécessaire de sérialiser et de désérialiser l'objet qui passe en utilisant Tuple . Si vous souhaitez envoyer plus de sept objets complexes, créez un objet Tuple interne pour le dernier argument Tuple.

0
kota

Réponse tardive, mais vous pouvez tirer parti du fait que vous pouvez désérialiser plusieurs objets d'une chaîne JSON, à condition que les objets ne partagent aucun nom de propriété commun

    public async Task<HttpResponseMessage> Post(HttpRequestMessage request)
    {
        var jsonString = await request.Content.ReadAsStringAsync();
        var content  = JsonConvert.DeserializeObject<Content >(jsonString);
        var config  = JsonConvert.DeserializeObject<Config>(jsonString);
    }
0
Rob Sedgwick

Ici, j’ai trouvé une solution de contournement pour transmettre plusieurs objets génériques (tels que json) de jquery à une API Web à l’aide de JObject , puis les reconvertir dans le type d’objet spécifique requis dans le contrôleur api. Cet objet fournit un type concret spécialement conçu pour travailler avec JSON.

var combinedObj = {}; 
combinedObj["obj1"] = [your json object 1]; 
combinedObj["obj2"] = [your json object 2];

$http({
       method: 'POST',
       url: 'api/PostGenericObjects/',
       data: JSON.stringify(combinedObj)
    }).then(function successCallback(response) {
         // this callback will be called asynchronously
         // when the response is available
         alert("Saved Successfully !!!");
    }, function errorCallback(response) {
         // called asynchronously if an error occurs
         // or server returns response with an error status.
         alert("Error : " + response.data.ExceptionMessage);
});

et alors vous pouvez obtenir cet objet dans votre contrôleur 

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public [OBJECT] PostGenericObjects(object obj)
    {
        string[] str = GeneralMethods.UnWrapObjects(obj);
        var item1 = JsonConvert.DeserializeObject<ObjectType1>(str[0]);
        var item2 = JsonConvert.DeserializeObject<ObjectType2>(str[1]);

        return *something*;
    } 

J'ai créé une fonction générique pour dérouler l'objet complexe, il n'y a donc aucune limitation du nombre d'objets lors de l'envoi et du dépliage. Nous pouvons même envoyer plus de deux objets 

public class GeneralMethods
{
    public static string[] UnWrapObjects(object obj)
    {
        JObject o = JObject.Parse(obj.ToString());

        string[] str = new string[o.Count];

        for (int i = 0; i < o.Count; i++)
        {
            string var = "obj" + (i + 1).ToString();
            str[i] = o[var].ToString(); 
        }

        return str;
    }

}

J'ai posté la solution sur mon blog avec un peu plus de description avec un code plus simple à intégrer facilement.

Transmettre plusieurs objets complexes à l'API Web

J'espère que cela aiderait quelqu'un. Je serais intéressé d'entendre les experts ici concernant les avantages et les inconvénients de l'utilisation de cette méthodologie.

0
Sheikh M. Haris

Fondamentalement, vous pouvez envoyer un objet complexe sans rien ajouter à la fantaisie. Ou sans apporter de modifications à Web-Api. Je veux dire, pourquoi devrions-nous apporter des modifications à Web-Api, alors que la faute est dans notre code qui appelle Web-Api.

Tout ce que vous avez à faire est d'utiliser la bibliothèque Json de NewtonSoft comme suit.

string jsonObjectA = JsonConvert.SerializeObject(objectA);
string jsonObjectB = JsonConvert.SerializeObject(objectB);
string jSoNToPost = string.Format("\"content\": {0},\"config\":\"{1}\"",jsonObjectA , jsonObjectB );
//wrap it around in object container notation
jSoNToPost = string.Concat("{", jSoNToPost , "}"); 
//convert it to JSON acceptible content
HttpContent content = new StringContent(jSoNToPost , Encoding.UTF8, "application/json"); 

var response = httpClient.PutAsync("api/process/StartProcessiong", content);
0
Manzar Zafar