将多个复杂对象传递给post / put Web API方法

时间:2014-07-21 20:50:17

标签: c# asp.net asp.net-web-api asp.net-web-api2 dotnet-httpclient

有些人可以帮我了解如何将多个对象从C#控制台应用程序传递到Web 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)
{

}

11 个答案:

答案 0 :(得分:49)

在当前版本的Web API中,在Web API方法签名中使用多个复杂对象(如ContentConfig复杂对象) 不允许 。我打赌好的钱config(你的第二个参数)总是回归为NULL。这是因为对于一个请求,只能从主体中解析一个复杂对象。出于性能原因,只允许访问和解析Web API请求正文一次。因此,在对内容"的请求主体进行扫描和解析之后。参数,所有后续的主体解析将以" NULL"结束。所以基本上:

  • [FromBody]只能归因一件商品。
  • 可以使用[FromUri]
  • 归因于任意数量的项目

以下是来自Mike Stall's excellent blog article的有用摘录(oldie但goldie!)。您需要关注第4项

  

以下是确定是使用模型绑定还是格式化程序读取参数的基本规则:

     
      
  1. 如果参数没有属性,那么决定完全取决于参数的.NET类型。 "简单类型"使用模型绑定。复杂类型使用格式化程序。 A"简单类型"包括:primitivesTimeSpanDateTimeGuidDecimalString或具有TypeConverter转换自的内容的内容字符串。
  2.   
  3. 您可以使用[FromBody]属性指定参数应来自正文。
  4.   
  5. 您可以在参数或参数的类型上使用[ModelBinder]属性来指定参数应该是模型绑定的。此属性还允许您配置模型绑定器。 [FromUri][ModelBinder]的派生实例,专门配置模型绑定器以仅查看URI。
  6.   
  7. 身体只能读一次。因此,如果签名中包含2种复杂类型,则其中至少有一种必须具有[ModelBinder]属性。
  8.         

    这些规则的关键设计目标是静态和可预测的。

         

    MVC和Web API之间的关键区别在于MVC缓冲内容(例如请求主体)。这意味着MVC的参数绑定可以重复搜索正文以查找参数片段。而在Web 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!
}

我确实说过还有其他方法,例如你可以简单地将两个对象包装在你自己创建的超级对象中,并将其传递给你的action方法。或者,您可以通过在URI中提供其中一个参数来简单地消除请求体中两个复杂参数的需要。或者......好吧,你明白了。

我要重申一下,我自己没有尝试过任何一项,尽管理论上它应该都是

答案 1 :(得分:18)

正如@djikay所提到的,你不能传递多个FromBody参数。

我的一个解决方法是定义CompositeObject

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

并让您的WebAPI将此CompositeObject作为参数。

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

答案 2 :(得分:8)

您可以尝试从客户端发布多部分内容,如下所示:

 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;

}

}

答案 3 :(得分:4)

我知道这是一个老问题,但我遇到了同样的问题,这就是我提出的问题,希望对某人有用。这将允许在请求URL(GET)中单独传递JSON格式的参数,作为之后的单个JSON对象? (GET)或单个JSON正文对象(PO​​ST)。我的目标是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的注册方法中注入绑定规则:

        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
    }

示例帖子正文:

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

或GET单一参数(需要URL编码)

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

或GET多个参数(需要URL编码)

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

答案 4 :(得分:2)

创建一个复杂的对象,将Content和Config组合在其中,如同其他人提到的那样,使用dynamic并只做一个.ToObject();为:

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

答案 5 :(得分:1)

将多个复杂对象传递给webapi服务的最佳方法是使用除动态,json字符串,自定义类之外的元组。

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

使用元组时无需序列化和反序列化传递的对象。 如果要发送七个以上的复杂对象,请为最后一个元组参数创建内部元组对象。

答案 6 :(得分:0)

这是另一种可能对您有用的模式。这是一个获取,但相同的原则和代码适用于邮政/投入,但相反。它本质上是基于将对象转换为ObjectWrapper类的原则,该类将Type的名称保存到另一端:

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

这是使用REST客户端库的客户端(UWP)上的代码。客户端库只使用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 :(得分:0)

基本上你可以发送复杂的对象而不做任何额外的花哨的事情。或者不对Web-Api进行更改。我的意思是为什么我们必须对Web-Api进行更改,而错误是在我们的代码中调用Web-Api。

所有你需要做的就是使用NewtonSoft的Json库。

to_char()

答案 8 :(得分:0)

在这里,我找到了一种解决方法,使用JObject将多个通用对象(如json)从jquery传递到WEB API,然后在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;
    }

}

我已经将解决​​方案发布到我的博客上,并提供了更简单的代码,以便轻松集成。

Pass multiple complex objects to Web API

我希望它会对某人有所帮助。我很想听听这里的专家关于使用这种方法的利弊。

答案 9 :(得分:0)

迟到的答案,但是你可以利用这样一个事实,即你可以从一个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);
    }

答案 10 :(得分:0)

创建一个复合对象

 public class CollectiveObject<X, Y>
{
    public X FirstObj;

    public Y SecondObj;
}

使用您要发送的任意两个对象初始化一个复合对象。

 CollectiveObject<myobject1, myobject2> collectiveobj =
                   new CollectiveObject<myobject1, myobject2>();
            collectiveobj.FirstObj = myobj1;
            collectiveobj.SecondObj = myobj2;

执行序列化

   var req = JSONHelper.JsonSerializer`<CollectiveObject<myobject1, `myobject2>>(collectiveobj);`

`

您的API必须像

    [Route("Add")]


public List<APIAvailibilityDetails> Add([FromBody]CollectiveObject<myobject1, myobject2> collectiveobj)
    { //to do}