Подтвердить что ты не робот

Передача нескольких сложных объектов методу Web/Post/put

Может кто-нибудь помочь мне узнать, как передать несколько объектов из приложения консоли С# в контроллер веб-API, как показано ниже?

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

Мой метод Web API выглядит следующим образом:

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

}
4b9b3361

Ответ 1

В текущей версии веб-API использование нескольких сложных объектов (например, сложных Content и Config сложных объектов) в методе веб-API не допускается. Я ставлю хорошие деньги, что Config (ваш второй параметр) всегда возвращается как NULL. Это связано с тем, что только один сложный объект может быть проанализирован из тела для одного запроса. По соображениям производительности тело запроса веб-API доступно только для доступа и анализа один раз. Таким образом, после того, как на экране запроса и разбора тела запроса будет отображаться "контент", все последующие анализы тела заканчиваются на "NULL". Итак, в основном:

  • Только один элемент можно отнести к [FromBody].
  • Любое количество элементов можно отнести к [FromUri].

Ниже приведен полезный отрывок из Майка Столла превосходной статьи в блоге (oldie but goldie!). Вы хотите обратить внимание на пункт 4:

Вот основные правила, чтобы определить, читается ли параметр с привязкой к модели или форматированием:

  • Если параметр не имеет на нем атрибута, то решение принимается исключительно по типу параметра .NET. "Простые типы" используют привязку модели. В сложных типах используются форматирующие элементы. "Простой тип" включает в себя: примитивы, TimeSpan, DateTime, Guid, Decimal, String или что-то с TypeConverter который преобразуется из строк.
  • Вы можете использовать атрибут [FromBody], чтобы указать, что параметр должен быть из тела.
  • Вы можете использовать атрибут [ModelBinder] для параметра или типа параметра, чтобы указать, что параметр должен быть привязан к модели. Этот атрибут также позволяет настраивать связующее устройство модели. [FromUri] - это производный экземпляр [ModelBinder], который специально настраивает связующее устройство для просмотра только в URI.
  • Тело можно читать только один раз. Поэтому, если у вас есть 2 сложных типа в сигнатуре, по крайней мере один из них должен иметь атрибут [ModelBinder].

Это была ключевая цель дизайна для этих правил быть статичной и предсказуемой.

Ключевое различие между MVC и Web API заключается в том, что MVC буферизует содержимое (например, тело запроса). Это означает, что привязка параметра MVC может многократно искать тело, чтобы искать фрагменты параметров. Принимая во внимание, что в веб-API тело запроса (HttpContent) может быть непрерывным, бесконечным, не буферизированным, неперематываемым потоком.

Вы можете прочитать остальную часть этой невероятно полезной статьи самостоятельно, чтобы сократить длинную историю, то, что вы пытаетесь сделать, в настоящее время не представляется возможным (это означает, что вы должны получить творческий подход). Дальше это не решение, а обходное решение и только одна возможность; есть и другие способы.

Решение/Обход

( Отказ от ответственности: Я не использовал его сам, я просто знаю теорию!)

Одним из возможных "решений" является использование объекта JObject. Эти объекты обеспечивают конкретный тип, специально предназначенный для работы с JSON.

Вам просто нужно настроить подпись, чтобы принять только один сложный объект из тела, JObject, пусть назовите его stuff. Затем вам необходимо вручную проанализировать свойства объекта JSON и использовать дженерики для гидратации конкретных типов.

Например, ниже приведен краткий пример, дающий вам представление:

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

Я сказал, что есть другие способы, например, вы можете просто обернуть ваши два объекта в супер-объект вашего собственного создания и передать его вашему методу действий. Или вы можете просто устранить необходимость в двух сложных параметрах в теле запроса, предоставив один из них в URI. Или... ну, вы поняли.

Позвольте мне еще раз повторить, что я не пробовал ничего из этого сам, хотя он должен все работать теоретически.

Ответ 2

Как упоминалось в @djikay, вы не можете передавать несколько параметров FromBody.

Один способ обхода - определить a CompositeObject,

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

и ваш WebAPI принимает этот CompositeObject как параметр.

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

Ответ 3

Вы можете попробовать опубликовать многостраничный контент от клиента следующим образом:

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

На стороне сервера вы можете прочитать содержимое, подобное этому:

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;

}

}

Ответ 4

Я знаю, что это старый вопрос, но у меня была такая же проблема, и вот что я придумал и, надеюсь, будет кому-то полезен. Это позволит передавать параметры JSON отдельно в URL запроса (GET), как один единственный объект JSON после? (GET) или внутри одного объекта тела JSON (POST). Моя цель была в стиле RPC.

Создан собственный атрибут и привязка параметров, наследующий от 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
    {
    }
}

Правило вложения привязки внутри метода WebApiConfig.cs Register:

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

Это позволяет выполнять действия контроллера со значениями параметров по умолчанию и смешанной сложностью:

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

example post body:

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

или GET single param (требуется кодировка URL)

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

или GET multiple param (требуется кодировка URL)

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

Ответ 5

Создайте один сложный объект, чтобы объединить контент и Config в нем, как упоминалось в других, использовать динамические и просто делать .ToObject(); как:

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

Ответ 6

Вот еще один шаблон, который может быть вам полезен. Это для Get, но тот же принцип и код применяется для Post/Put, но наоборот. Он по существу работает по принципу преобразования объектов вплоть до этого класса ObjectWrapper, который сохраняет имя типа с другой стороны:

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

В коде 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;
}

Это код на стороне клиента (UWP) с использованием клиентской библиотеки REST. В клиентской библиотеке просто используется библиотека сериализации Newtonsoft Json - ничего необычного.

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

Ответ 7

В принципе, вы можете отправить сложный объект, не делая ничего необычного. Или без внесения изменений в Web-Api. Я имею в виду, почему мы должны вносить изменения в Web-Api, в то время как ошибка в нашем коде, вызывающая Web-Api.

Все, что вам нужно сделать, это использовать библиотеку NewtonSoft Json следующим образом.

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

Ответ 8

Здесь я нашел обходное решение для передачи нескольких общих объектов (как json) из jquery в WEB API с помощью JObject, а затем верните требуемый тип объекта в контроллер api. Эти объекты обеспечивают конкретный тип, специально предназначенный для работы с 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);
});

а затем вы можете получить этот объект в контроллере

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

Я создал универсальную функцию для разворачивания сложного объекта, поэтому при отправке и распаковке нет ограничения количества объектов. Мы даже можем отправить более двух объектов

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

}

Я опубликовал решение для своего блога с небольшим описанием с более простым кодом, чтобы легко интегрировать.

Передача нескольких сложных объектов в веб-API

Я надеюсь, что это поможет кому-то. Мне было бы интересно услышать от экспертов здесь о плюсах и минусах использования этой методологии.

Ответ 9

Поздний ответ, но вы можете воспользоваться тем фактом, что вы можете десериализовать несколько объектов из одной строки JSON, если объекты не имеют общих имен свойств,

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