Pass multiple complex objects to a post/put Web API method

165,671

Solution 1

In the current version of Web API, the usage of multiple complex objects (like your Content and Config complex objects) within the Web API method signature is not allowed. I'm betting good money that config (your second parameter) is always coming back as NULL. This is because only one complex object can be parsed from the body for one request. For performance reasons, the Web API request body is only allowed to be accessed and parsed once. So after the scan and parsing occurs of the request body for the "content" parameter, all subsequent body parses will end in "NULL". So basically:

  • Only one item can be attributed with [FromBody].
  • Any number of items can be attributed with [FromUri].

Below is a useful extract from Mike Stall's excellent blog article (oldie but goldie!). You'll want to pay attention to item 4:

Here are the basic rules to determine whether a parameter is read with model binding or a formatter:

  1. If the parameter has no attribute on it, then the decision is made purely on the parameter's .NET type. "Simple types" use model binding. Complex types use the formatters. A "simple type" includes: primitives, TimeSpan, DateTime, Guid, Decimal, String, or something with a TypeConverter that converts from strings.
  2. You can use a [FromBody] attribute to specify that a parameter should be from the body.
  3. You can use a [ModelBinder] attribute on the parameter or the parameter's type to specify that a parameter should be model bound. This attribute also lets you configure the model binder. [FromUri] is a derived instance of [ModelBinder] that specifically configures a model binder to only look in the URI.
  4. The body can only be read once. So if you have 2 complex types in the signature, at least one of them must have a [ModelBinder] attribute on it.

It was a key design goal for these rules to be static and predictable.

A key difference between MVC and Web API is that MVC buffers the content (e.g. request body). This means that MVC's parameter binding can repeatedly search through the body to look for pieces of the parameters. Whereas in Web API, the request body (an HttpContent) may be a read-only, infinite, non-buffered, non-rewindable stream.

You can read the rest of this incredibly useful article on your own so, to cut a long story short, what you're trying to do is not currently possible in that way (meaning, you have to get creative). What follows is not a solution, but a workaround and only one possibility; there are other ways.

Solution/Workaround

(Disclaimer: I've not used it myself, I'm just aware of the theory!)

One possible "solution" is to use the JObject object. This objects provides a concrete type specifically designed for working with JSON.

You simply need to adjust the signature to accept just one complex object from the body, the JObject, let's call it stuff. Then, you manually need to parse properties of the JSON object and use generics to hydrate the concrete types.

For example, below is a quick'n'dirty example to give you an idea:

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

I did say there are other ways, for example you can simply wrap your two objects in a super-object of your own creation and pass that to your action method. Or you can simply eliminate the need for two complex parameters in the request body by supplying one of them in the URI. Or ... well, you get the point.

Let me just reiterate I've not tried any of this myself, although it should all work in theory.

Solution 2

As @djikay mentioned, you cannot pass multiple FromBody parameters.

One workaround I have is to define a CompositeObject,

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

and have your WebAPI takes this CompositeObject as the parameter instead.

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

Solution 3

You could try posting multipart content from the client like this:

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

On the server side you could read the the content like this:

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;

}

}

Solution 4

I know this is an old question, but I had the same issue and here is what I came up with and hopefully will be useful to someone. This will allow passing JSON formatted parameters individually in request URL (GET), as one single JSON object after ? (GET) or within single JSON body object (POST). My goal was RPC-style functionality.

Created a custom attribute and parameter binding, inheriting from 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
    {
    }
}

Inject binding rule inside WebApiConfig.cs's Register method:

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

This allows for controller actions with default parameter values and mixed complexity, as such:

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

or GET single param (needs URL encoding)

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

or GET multiple param (needs URL encoding)

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

Solution 5

Create one complex object to combine Content and Config in it as others mentioned, use dynamic and just do a .ToObject(); as:

[HttpPost]
public void StartProcessiong([FromBody] dynamic obj)
{
   var complexObj= obj.ToObject<ComplexObj>();
   var content = complexObj.Content;
   var config = complexObj.Config;
}
Share:
165,671
SKumar
Author by

SKumar

Updated on July 09, 2022

Comments

  • SKumar
    SKumar almost 2 years

    Can some please help me to know how to pass multiple objects from a C# console app to Web API controller as shown below?

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

    My Web API method is like this:

    public void StartProcessiong([FromBody]Content content, [FromBody]Config config)
    {
    
    }
    
  • SKumar
    SKumar almost 10 years
    How can i pass different object in JObject. its throwing exception if i add different object as .Add(object1) and .Add(object2)
  • djikay
    djikay almost 10 years
    As I mentioned, I have no personal experience with this, so I can't directly help you. You should be able to find a lot of information on the web about this though. If, for whatever reason, it's not clear, then you can create your own "master" data transfer object which will simply contain Content and Config. Then you'll just need to use that "master" object to call your method.
  • Arminder Dahul
    Arminder Dahul over 9 years
    Isn't the answer as simple as use traditional methods? Why use Web API if what you're doing isn't going to adhere to the RESTful paradigm? Use the right tool for the job.
  • Brian Wenhold
    Brian Wenhold over 9 years
    You can post as many complex object types as you like of you post using multipart/form-data.
  • Aviram Fireberger
    Aviram Fireberger almost 7 years
    Great answer!, and excellent example of the usage of "MultipartFormDataContent".
  • ruffin
    ruffin about 6 years
    This is almost (if not exactly) equivalent to saying to use a DTO, which I'm not sure is the wrong answer. It does sort of break the strict API feel of your app, though. I'm not sure how I feel about the middle ground presented here -- a DTO of business models -- and might encourage a "real" DTO, but it's Not Wrong. ;^)
  • Mort
    Mort over 4 years
    What is the syntax to pass into the stuff variable, in order for that example to work? I have tried 20 different strings and none of them result in a valid JObject.