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

53

请问有人能帮我了解如何像下面展示的那样从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)
{

}

这可能会对你有所帮助 https://stackoverflow.com/questions/40632551/how-to-pass-class-of-list-object-in-restful-web-api - Ahmed Elbendary
12个回答

62
在当前版本的Web API中,不允许在Web API方法签名中使用多个复杂对象(如您的ContentConfig复杂对象)。我打赌你的第二个参数config总是返回NULL。这是因为一个请求只能从正文中解析出一个复杂对象。出于性能原因,Web API请求正文仅允许访问和解析一次。因此,在扫描和解析请求正文以获取“content”参数后,所有后续正文解析都将以“NULL”结束。所以基本上:
  • 只能使用一个项目属性[FromBody]
  • 可以标注任意数量的项目[FromUri]

以下是Mike Stall的优秀博客文章的有用摘录(老但金贵!)。你需要注意第4点

以下是确定参数是由模型绑定还是格式化程序读取的基本规则:
  1. 如果参数上没有属性,则决策纯粹基于参数的.NET类型。"简单类型"使用模型绑定。复杂类型使用格式化程序。"简单类型"包括:primitives, TimeSpan, DateTime, Guid, Decimal, String,或者具有将字符串转换为其他类型的TypeConverter
  2. 您可以使用[FromBody]属性指定参数应来自正文。
  3. 您可以在参数或参数类型上使用[ModelBinder]属性指定参数应该是模型绑定的。此属性还允许您配置模型绑定器。[FromUri][ModelBinder]的派生实例,专门配置模型绑定器仅查找URI。
  4. 正文只能读取一次。因此,如果签名中有2个复杂类型,则其中至少一个必须具有[ModelBinder]属性。
这些规则的关键设计目标是静态和可预测的。 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!
}

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

让我再强调一下,我自己没有尝试过这些方法,虽然理论上应该都能行得通。


我该如何在JObject中传递不同的对象?如果我使用.Add(object1)和.Add(object2)添加不同的对象,它会抛出异常。 - SKumar
正如我之前所提到的,我没有个人经验,所以无法直接帮助你。不过你应该能够在网络上找到很多相关信息。如果由于某些原因不清楚,那么你可以创建自己的“主”数据传输对象,其中将简单包含ContentConfig。然后你只需要使用那个“主”对象来调用你的方法就行了。 - djikay
答案难道不就是使用传统方法吗?如果你所做的事情无法遵循RESTful范式,为什么要使用Web API呢?用正确的工具来做工作。 - Arminder Dahul
如果您使用multipart/form-data进行发布,可以发布任意数量的复杂对象类型。 - Brian Wenhold
传递到stuff变量的语法是什么,以便该示例能够正常工作?我尝试了20个不同的字符串,但没有一个有效的JObject。 - Mort

23

如 @djikay 所提到的,您无法传递多个 FromBody 参数。

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

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

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

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

这几乎(如果不是完全)等同于使用数据传输对象,我不确定这是否是错误的答案。虽然它确实打破了应用程序的严格API感觉,但它确实有点中庸。我对此处呈现的“业务模型的DTO”持保留态度,并可能鼓励使用“真正”的DTO,但这并不是错误的。;^) - ruffin

12
您可以尝试像这样从客户端发布多部分内容:
 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;

}

1
很棒的答案!展示了“MultipartFormDataContent”使用的绝佳示例。 - Aviram Fireberger

6
我知道这是一个老问题,但我遇到了同样的问题,这是我的解决方案,希望对某些人有用。这将允许在请求URL中单独传递JSON格式参数(GET),作为一个单独的JSON对象放置于?(GET)之后或者放置于单个JSON body对象(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
    }

示例帖子正文:

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

或者获取单个参数(需要进行URL编码)
controllerPath/DoWidgetStuff?{"widget":{..},"comment":"test","stockCount":42}

或者获取多个参数(需要URL编码)

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

3

根据其他人提到的建议,创建一个复杂对象来将内容和配置组合在其中,使用动态语言,并执行 .ToObject(); 如下:

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

2

将多个复杂对象传递给Web API服务的最佳方法是使用元组而不是动态,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();
}

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


1
在这里,我找到了一种解决方案,可以使用JObject从jquery传递多个通用对象(作为json)到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;
    }

}

我已经在我的博客上发布了解决方案,并附有更详细的描述和更简单的代码,以便轻松集成。 将多个复杂对象传递给Web API 我希望这能帮助到某些人。我很想听听这种方法的专家们的利弊分析。

0
这里有另一种模式可能对您有用。它适用于 Get 请求,但相同的原理和代码也适用于 Post / Put 请求,只是反过来而已。它基本上是通过将对象转换为 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;
}

这是客户端(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;
}

0

基本上,您可以发送复杂对象而不需要进行任何额外的花哨操作,也不需要对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);

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接