Передача нескольких сложных объектов методу 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)
{
}
Ответы
Ответ 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);
}