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

SKu*_*mar 43 c# asp.net asp.net-web-api dotnet-httpclient asp.net-web-api2

有些人可以帮我了解如何将多个对象从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);
}
Run Code Online (Sandbox Code Playgroud)

我的Web API方法是这样的:

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

}
Run Code Online (Sandbox Code Playgroud)

dji*_*kay 56

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

  • 只有一个项目可归属于[FromBody].
  • 可以归因于任意数量的项目[FromUri].

以下是Mike Stall的精彩博客文章(老人,但goldie!)的有用摘录.你要注意第4项:

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

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

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

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

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

让我重申一下,我自己没有尝试过任何这些,尽管它应该在理论上都有效.


Mag*_*ing 19

正如@djikay所提到的,你无法传递多个FromBody参数.

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

public class CompositeObject
{
    public Content Content { get; set; }
    public Config Config { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

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

public void StartProcessiong([FromBody] CompositeObject composite)
{ ... }
Run Code Online (Sandbox Code Playgroud)


小智 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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在服务器端,您可以阅读如下内容:

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;

}
Run Code Online (Sandbox Code Playgroud)

}


use*_*030 5

我知道这是一个老问题,但是我遇到了同样的问题,这是我想出的,希望对某人有用。这将允许在请求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
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

在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 }
        );
        }
Run Code Online (Sandbox Code Playgroud)

这允许具有默认参数值和混合复杂度的控制器动作,例如:

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

示例帖子正文:

{ 
    "widget": { 
        "a": 1, 
        "b": "string", 
        "c": { "other": "things" } 
    }, 
    "stockCount": 42, 
    "comment": "sample code"
} 
Run Code Online (Sandbox Code Playgroud)

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

controllerPath/DoWidgetStuff?{"widget":{..},"comment":"test","stockCount":42}
Run Code Online (Sandbox Code Playgroud)

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

controllerPath/DoWidgetStuff?widget={..}&comment="test"&stockCount=42
Run Code Online (Sandbox Code Playgroud)