Binding .NET Core Json to simple parameters - c#

I used to just post simple json objects to ASP.NET MVC controllers and the binding engine would parse the body out into the method's simple parameters:
{
"firstname" : "foo",
"lastname" : "bar"
}
and I could have a MVC controller like this:
public method Blah(string firstname, string lastname) {}
And firstname and lastname would automatically be pulled from the Json object and mapped to the simple parameters.
I've moved a backend piece to .NET Core 5.0 with the same method signatures, however, when I post the same simple JSON object, my parameters are null. I even tried doing [FromBody] on each parameter but they would still be null. It wasn't until I created an extra class that contained the parameter names would the model binding work:
public class BlahRequest
{
public string firstname { get; set;}
public string lastname { get; set; }
}
And then I have to update my method signature to look like this:
public method Blah([FromBody]BlahRequest request) { }
And now, the request has the properties firstname and lastname filled out from the post request.
Is there a model binder setting where I can go back to binding from a simple Json object to the method's parameters? Or do I have to update all my method signatures to contain a class with properties?
How the web api method is called
The original application is written in Angular but I can recreate it with a simple Fiddler request:
POST https://localhost:5001/Blah/Posted HTTP/1.1
Host: localhost:5001
Connection: keep-alive
Accept: application/json, text/plain, */*
Content-Type: application/x-www-form-urlencoded
{"firstname":"foo","lastname":"bar"}
In previous versions of the .Net framework, the controller's method would parse those values automatically. Now, on core, it requires a model to be passed in. I've tried application/json, multipart/form-data, and application/x-www-form-urlencoded as the Content-type and they all end up with null values.
Smallest .Net Core project
public class Startup {
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services){
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
app.UseHttpsRedirection();
app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
[Route("[controller]/[action]")]
public class BlahController : ControllerBase {
public object Posted(string firstname, string lastname) {
Console.WriteLine(firstname);
Console.WriteLine(lastname);
return true;
}
}

Looking into the guts of the .Net core source code, I was able to find an example of how the form values provider worked and then reverse engineered a solution for this project. I think if one was starting fresh, they wouldn't have to solve this problem, but we are moving an existing UI built on Angular onto this new backend on .Net core and didn't want to rewrite all the server side calls as models with the parameters of the methods on the controllers.
So, just to review, in previous versions of .Net, you could post a Json object to an Mvc controller with multiple parameters in the method signature.
public object GetSalesOrders(string dashboardName, int fillableState = 0){}
and a simple method to call this would like the following:
POST https://localhost:5001/api/ShippingDashboard/GetSalesOrders HTTP/1.1
Content-Type: application/json
{"dashboardName":"/shippingdashboard/bigshipping","fillableState":1}
Looking into value providers, I created my own value provider and pushed it to the top of the list during the configuration of the Startup class.
services
.AddControllers( options =>{
options.ValueProviderFactories.Insert(0, new JsonBodyValueProviderFactory());
})
JsonBodyValueProviderFactory is a factory class I wrote. It inspects the request and if the content type is application/json, it will add a provider to the content:
public class JsonBodyValueProviderFactory : IValueProviderFactory{
public Task CreateValueProviderAsync(ValueProviderFactoryContext context) {
if (context == null) {
throw new ArgumentNullException(nameof(context));
}
var request = context.ActionContext.HttpContext.Request;
if (request.HasJsonContentType()) {
// Allocating a Task only when the body is json data
return AddValueProviderAsync(context);
}
return Task.CompletedTask;
}
private static async Task AddValueProviderAsync(ValueProviderFactoryContext context) {
var request = context.ActionContext.HttpContext.Request;
var body = "";
Dictionary<string,object> asDict = new Dictionary<string, object>();
try {
using (StreamReader stream = new StreamReader(request.Body)){
body = await stream.ReadToEndAsync();
}
var obj = JObject.Parse(body);
foreach(var item in obj.Children()){
asDict.Add(item.Path, item.Values().First());
}
} catch (InvalidDataException ex) {
Console.WriteLine(ex.ToString());
} catch (IOException ex) {
Console.WriteLine(ex.ToString());
}
var valueProvider = new JsonBodyValueProvider(BindingSource.Form, asDict);
context.ValueProviders.Add(valueProvider);
}
}
Since I don't always know the shape of the data, I utilized Json.Net's JObject and suck the Json body into a JObject. I then use the top level properties and add them to a dictionary for easy lookup.
The actual class that takes the values and responds to the parameter name is JsonBodyValueProvider:
public class JsonBodyValueProvider : BindingSourceValueProvider, IEnumerableValueProvider {
private readonly Dictionary<string, object> values;
private PrefixContainer? _prefixContainer;
public JsonBodyValueProvider( BindingSource bindingSource, Dictionary<string,object> values) : base(bindingSource) {
if (bindingSource == null) {
throw new ArgumentNullException(nameof(bindingSource));
}
if (values == null) {
throw new ArgumentNullException(nameof(values));
}
this.values = values;
}
protected PrefixContainer PrefixContainer {
get {
if (_prefixContainer == null) {
_prefixContainer = new PrefixContainer(values.Keys);
}
return _prefixContainer;
}
}
public override bool ContainsPrefix(string prefix) {
return PrefixContainer.ContainsPrefix(prefix);
}
public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix) {
if (prefix == null) {
throw new ArgumentNullException(nameof(prefix));
}
return PrefixContainer.GetKeysFromPrefix(prefix);
}
public override ValueProviderResult GetValue(string key){
if (key == null) {
throw new ArgumentNullException(nameof(key));
}
if (key.Length == 0) {
return ValueProviderResult.None;
}
var _values = values[key];
if (!values.ContainsKey(key)) {
return ValueProviderResult.None;
} else {
return new ValueProviderResult(_values.ToString());
}
}
}
This is pretty much a carbon copy of the FormValueProvider in .Net Core, I just adjusted it to work with a dictionary of input values.
Now my controllers can stay the same from prior versions of .Net without changing method signatures.

It depends on the content type. You must be using application/json content type. This is why you have to create a viewmodel and to add [FromBody] attribute
public method Blah([FromBody]BlahRequest request) { }
but if you use application/x-www-form-urlencoded or multipart/form-data form enctype or ajax content-type then this will be working
public method Blah(string firstname, string lastname) {}
if you still have some problems, it means that you are using ApiController. In this case add this code to startup
using Microsoft.AspNetCore.Mvc;
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressInferBindingSourcesForParameters = true;
});

Related

How to post and cast payload?

My goal is to call api (via post), accept payload as base type and later cast it to concrete type. If I do that from main solution (where my api stands), everything works well. But I can't understand why same code doesn't work from other solutions.
So I have my request (declared in different solutions)
namespace Nb
{
public class NbRequestBase
{
public string BaseProp { get; set; }
}
public class NbRequestConcrete : NbRequestBase
{
public string ConcreteProp { get; set; }
}
}
And this is my endpoint:
[HttpPost]
[Route("payments/nb")]
public IHttpActionResult Prepare(NbRequestBase request)
{
if(request is NbRequestConcrete)
{
}
try
{
// <<< INSERT CODE HERE >>>
NbRequestConcrete nbRequestConcrete = (NbRequestConcrete)request;
return Ok();
}
catch (Exception ex)
{
_logger.Error(ex);
return InternalServerError();
}
}
and this is my calling code:
NbRequestConcrete requestTwo = new NbRequestConcrete()
{
BaseProp = "BaseProp",
ConcreteProp = "ConcreteProp"
};
using (var client = new HttpClient())
{
var _clientId = "_clientId";
var _clientSecret = "_clientSecret";
client.BaseAddress = new Uri("http://localhost:50228");
#region Formatter
JsonMediaTypeFormatter formatter = new JsonMediaTypeFormatter();
formatter.SerializerSettings.TypeNameHandling = TypeNameHandling.All;
List<MediaTypeFormatter> formatters = new List<MediaTypeFormatter>();
formatters.Add(formatter);
#endregion
var responseMessage = client.PostAsync($"payments/nb?clientId={_clientId}&clientSecret={_clientSecret}", requestTwo, formatter).Result;
responseMessage.EnsureSuccessStatusCode();
}
If I put my calling code into other project/solution (for example just new console app), API endpoint is hit, but payload is null.
payload when called form console app
If I put exacly same calling code into project where my api is (for example in same API endpoint method, at try/catch block start and call it again), API endpoint is hit, payload is NOT null and casting works. Why is it? And how to fix it?
payload when called from same solution try/catch start
And BTW. How to make this call via postman?
Regards
This line tells the model binder to set the values of any matching properties in request to the value that was passed to the API:
public IHttpActionResult Prepare(NbRequestBase request)
The model binder does not attach all the other properties to the request, because it has no idea what they would be.
Problem was Assemblies name where NbRequestConcrete in console app lived in one assembly and on API lived in other. So request was different.
{
"$type": "Nb.NbRequestConcrete, Tester",
"ConcreteProp": "ConcreteProp",
"BaseProp": "BaseProp"
}
VS
{
"$type": "Nb.NbRequestConcrete, MYApi",
"ConcreteProp": "ConcreteProp",
"BaseProp": "BaseProp"
}

Getting OData Count in ASP.NET Core WebAPI

Using the sample code from Hassan Habib's Supercharging ASP.NET Core API with OData blog post, I am able to get the record count using an OData query of $count=true:
What needs to be configured to get the response object to be wrapped in an OData context so that the #odata.count property will show?
In my own ASP.NET Core web API project, I cannot get the simple $count parameter to work and I have no idea why.
With Hassan's sample code, the response JSON is wrapped in an OData context and the payload (an IEnumerable<Student> object) is in the value property of the JSON response. In my project, the OData context wrapper does not exist; my code never returns OData context, it only returns the payload object of type IEnumerable<T>:
I've also noticed that the Content-Type in the response header is application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8 in the sample project, where as it is simply application/json; charset=utf-8 in my project. I don't see any setting that controls this in either project, so I'm assuming the Microsoft.AspNetCore.Odata NuGet package is magically changing the response when it's configured properly.
My project is also using .NET Core 2.2 (Upgraded from 2.1), all the same versions of NuGet packages as Hassan's sample projects, and all the same settings in the StartUp.cs class... although my StartUp.cs is way more complicated (hence the reason I'm not posting it's content here.)
I could reproduce your issue when i use [Route("api/[controller]")]and [ApiController] with the startup.cs like below:
app.UseMvc(routeBuilder =>
{
routeBuilder.Expand().Select().Count().OrderBy().Filter();
routeBuilder.EnableDependencyInjection();
});
To fix it,be sure you have built a private method to do a handshake between your existing data models (OData model in this case) and EDM.
Here is a simple demo:
1.Controller(comment on Route attribute and ApiController attribute):
//[Route("api/[controller]")]
//[ApiController]
public class StudentsController : ControllerBase
{
private readonly WSDbContext _context;
public StudentsController(WSDbContext context)
{
_context = context;
}
// GET: api/Students
[HttpGet]
[EnableQuery()]
public IEnumerable<Student> Get()
{
return _context.Students;
}
}
//[Route("api/[controller]")]
//[ApiController]
public class SchoolsController : ControllerBase
{
private readonly WSDbContext _context;
public SchoolsController(WSDbContext context)
{
_context = context;
}
// GET: api/Schools
[HttpGet]
[EnableQuery()]
public IEnumerable<School> Get()
{
return _context.Schools;
}
2.Startup.cs():
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(action => action.EnableEndpointRouting = false);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
var connection = #"Server=(localdb)\mssqllocaldb;Database=WSDB;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<WSDbContext>(options => options.UseSqlServer(connection));
services.AddOData();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc(routeBuilder =>
{
routeBuilder.Expand().Select().Count().OrderBy().Filter();
routeBuilder.MapODataServiceRoute("api", "api", GetEdmModel());
});
}
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Student>("Students");
builder.EntitySet<Student>("Schools");
return builder.GetEdmModel();
}
}
Just been battling this.
I found that if I request my controller at /api/Things that most of the OData options work but $count doesn't.
However, $count does work if I request the same method via /odata/Things.
In my case I wanted to extend existing Api methods with [EnableQuery] but have it include the count metadata.
I ended up extending the EnableQuery attribute to return a different reponse, it worked perfectly.
public class EnableQueryWithMetadataAttribute : EnableQueryAttribute
{
public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
if (actionExecutedContext.Result is ObjectResult obj && obj.Value is IQueryable qry)
{
obj.Value = new ODataResponse
{
Count = actionExecutedContext.HttpContext.Request.ODataFeature().TotalCount,
Value = qry
};
}
}
public class ODataResponse
{
[JsonPropertyName("#odata.count")]
public long? Count { get; set; }
[JsonPropertyName("value")]
public IQueryable Value { get; set; }
}
}
You can just set an empty preffix route when you map OData, and you will receive OData with your request your endpoint.
routeBuilder.MapODataServiceRoute("ODataEdmModel", "", GetEdmModel());
In my case I've created a special action named $count that users the OData Filter query my collection (EF Database) and return the Count;
[HttpGet("$count")]
public async Task<int> Count(ODataQueryOptions<MyBookEntity> odataQueryOptions)
{
var queryable = this.dataContext.MyBooks;
return await odataQueryOptions.Filter
.ApplyTo(queryable, new ODataQuerySettings())
.Cast<MyBookEntity>()
.CountAsync();
}

Modelbinding JSON in .Net Core 2.2 Web API

I just wasted a lot of hours with trying to get a custom ComplexTypeModelBinder to work. Whatever I did, it never worked. As it turns out, this only works when the data is POSTed as form data; when you post a JSON object (in my case from a Swagger "try out" form) the ComplexTypeModelBinder never invokes the SetProperty method.
I have a lot of models, some more complex than others, and I have annotated some of the properties with a custom attribute. Whenever that property is bound I want it to 'normalized' (apply some 'formatting' to it) so that by the time the model gets validated the validator gets to see the 'normalized' data instead of the user-entered data.
I really, really, want to keep the current modelbinding behavior because that currently works fine but with the one exception that the annotated properties are processed by some code implemented by me. All other properties and behavior should be kept as-is. That is why I hoped to inherit from ComplexTypeModelBinder, but, as it turns out, this doesn't work if data is POSTed as JSON.
My (example) model looks like:
public class MyComplexModel
{
public int Id { get; set; }
public string Name { get; set; }
[FormatNumber(NumberFormat.E164)]
public string PhoneNumber { get; set; }
}
My controller method looks like this:
[HttpPost]
public MyComplexModel Post(MyComplexModel model)
{
return model;
}
My (not working) custom ComplexTypeModelBinder looks like:
public class MyModelBinder : ComplexTypeModelBinder
{
private readonly INumberFormatter _numberformatter;
private static readonly ConcurrentDictionary<Type, Dictionary<string, FormatNumberAttribute>> _formatproperties = new ConcurrentDictionary<Type, Dictionary<string, FormatNumberAttribute>>();
public MyModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders, INumberFormatter numberFormatter, ILoggerFactory loggerFactory)
: base(propertyBinders, loggerFactory)
{
_numberformatter = numberFormatter;
}
protected override object CreateModel(ModelBindingContext bindingContext)
{
// Index and cache all properties having the FormatNumber Attribute
_formatproperties.GetOrAdd(bindingContext.ModelType, (t) =>
{
return t.GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(FormatNumberAttribute))).ToDictionary(pi => pi.Name, pi => pi.GetCustomAttribute<FormatNumberAttribute>(), StringComparer.OrdinalIgnoreCase);
});
return base.CreateModel(bindingContext);
}
protected override Task BindProperty(ModelBindingContext bindingContext)
{
return base.BindProperty(bindingContext);
}
protected override void SetProperty(ModelBindingContext bindingContext, string modelName, ModelMetadata propertyMetadata, ModelBindingResult result)
{
if (_formatproperties.TryGetValue(bindingContext.ModelType, out var props) && props.TryGetValue(modelName, out var att))
{
// Do our formatting here
var formatted = _numberformatter.FormatNumber(result.Model as string, att.NumberFormat);
base.SetProperty(bindingContext, modelName, propertyMetadata, ModelBindingResult.Success(formatted));
} else
{
// Do nothing
base.SetProperty(bindingContext, modelName, propertyMetadata, result);
}
}
}
(The actual MyModelBinder checks for the FormatNumber attribute and handles the property accordingly, but I left it out for brevity since it doesn't really matter).
And my ModelBinderProvider:
public class MyModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
var modelType = context.Metadata.ModelType;
if (!typeof(MyComplexModel).IsAssignableFrom(modelType))
return null;
if (!context.Metadata.IsComplexType || context.Metadata.IsCollectionType)
return null;
var propertyBinders = context.Metadata.Properties
.ToDictionary(modelProperty => modelProperty, context.CreateBinder);
return new MyModelBinder(
propertyBinders,
(INumberFormatter)context.Services.GetService(typeof(INumberFormatter)),
(ILoggerFactory)context.Services.GetService(typeof(ILoggerFactory))
);
}
}
And ofcourse, I added the provider in the StartUp class:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(config =>
{
config.ModelBinderProviders.Insert(0, new MyModelBinderProvider());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
Again, this works fine when data is posted as form-data but not when posted as JSON. What would be the correct way to implement this? I have read somewhere that I shouldn't be looking in the ModelBinding direction but in the "JSON converters" direction but I haven't found anything that actually worked (yet).
Edit: I have created a git repository to demonstrate my problem here. To see my problem, set a breakpoint here in the TestController where the model is returned in the Post method. Start the project; a simple webpage will be shown with two buttons. The left one will post the form data as, well, form-data and you will see the model being returned with a reversed phonenumber (as an example). Click the right button and the data will be posted as a JSON model. Notice the model being returned having a 0 id and null values for both properties.

How to bind Json Query string in asp.net core web api

following code in asp.net web API worked fine but doesn't work in Asp.net core.
Endpoint api/devices?query={"deviceName":"example"}
[HttpGet]
public Device ([FromUri] string deviceName)
{
var device = context.Computers.Where(x => x.deviceName == deviceName);
return device;
}
[FromUri] attribute is not present asp.net core web API, and I tried to use following , but no success.
[HttpGet]
public Device Get([FromQuery] string deviceName)
{
return repo.GetDeviceByName(deviceName);
}
Unfortunately there is no way to bind JSON in a GET query like you have there. What you are looking for is to use a custom model binder to tell ASP.net Core how you want to bind.
First, you want to build your model for your JSON object.
public class MyCustomModel
{
public string DeviceName { get; set; }
}
Next you need to build your model binder. A simple example is given below but you would obviously want other checks around if it can be converted, Try/Catch blocks etc. Essentially a model binder tells ASP.net Core how a model should be bound. You might also run into TypeConverters which are given a type, how can I change this to another type during model binding. For now let's just use modelbinders.
public class MyViewModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var jsonString = bindingContext.ActionContext.HttpContext.Request.Query["query"];
MyCustomModel result = JsonConvert.DeserializeObject<MyCustomModel>(jsonString);
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
So all we are doing is taking the query string and deserializing it to our model.
Next we build a provider. A provider is what tells ASP.net core which modelbinder to use. In our case it's simple, if the model type is our custom type, then use our custom binder.
public class MyViewModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(MyCustomModel))
return new MyViewModelBinder();
return null;
}
}
And the final piece of the puzzle. In our startup.cs, we find where we add MVC services and we insert our model binder to the front of the list. This is important. If we just add our modelbinder to the list, another model binder might think it should be used instead (First in first served), so we might not ever make it to ours. So be sure to insert it at the start.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(config => config.ModelBinderProviders.Insert(0, new MyViewModelBinderProvider()));
}
Now we just create an action where we read the data, no attributes required.
[HttpGet]
public void Get(MyCustomModel model)
{
}
Further reading :
http://dotnetcoretutorials.com/2016/12/28/custom-model-binders-asp-net-core/
https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding
I came along this question because I have a similar problem.
In my scenario it was more convinient to implement ModelBinderAttribute:
public class FromUriJsonAttribute : ModelBinderAttribute
{
public FromUriJsonAttribute()
{
BinderType = typeof(JsonBinder);
}
public FromUriJsonAttribute(string paramName)
: this()
{
Name = paramName;
}
}
As in #MindingData answer the binder is needed:
public class JsonBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
try
{
var name = bindingContext.ModelName;
var json = actionContext.Request.GetQueryNameValuePairs().FirstOrDefault(x => x.Key == name).Value;
var targetType = bindingContext.ModelType;
var model = Newtonsoft.Json.JsonConvert.DeserializeObject(json, targetType);
bindingContext.Model = model;
return true;
}
catch
{
}
return false;
}
}
And the usage:
[Route("{id}/items")]
public Item GetItem(string id, [FromUriJson] Filter filter)
{
//logic
}
or:
[Route("{id}/items")]
public Item GetItem(string id, [FromUriJson("queryStringKey")] Filter filter)
{
//logic
}
It can be useful if you want to have Json formatted query string parameter only in some endpoints and/or don't want to mess with default IServiceCollection configuration by inserting a new IModelBinderProvider implementation.

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

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)
{
}
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:
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.
You can use a [FromBody] attribute to specify that a parameter should be from the body.
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.
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.
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)
{ ... }
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;
}
}
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
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;
}
Best way to pass multiple complex object to webapi services is by using tuple other than dynamic, json string, custom class.
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();
}
No need to serialize and deserialize passing object while using tuple.
If you want to send more than seven complex object create internal tuple object for last tuple argument.
Here's another pattern that may be useful to you. It's for a Get but the same principle and code applies for a Post/Put but in reverse. It essentially works on the principle of converting objects down to this ObjectWrapper class which persists the Type's name to the other side:
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
}
}
In the REST code:
[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;
}
This is the code on the client side (UWP) using a REST client library. The client library just uses the Newtonsoft Json serialization library - nothing fancy.
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;
}
Basically you can send complex object without doing any extra fancy thing. Or without making changes to Web-Api. I mean why would we have to make changes to Web-Api, while the fault is in our code that's calling the Web-Api.
All you have to do use NewtonSoft's Json library as following.
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);
Here I found a workaround to pass multiple generic objects (as json) from jquery to a WEB API using JObject, and then cast back to your required specific object type in api controller. This objects provides a concrete type specifically designed for working with 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);
});
and then you can get this object in your controller
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*;
}
I have made a generic function to unwrap the complex object, so there is no limitation of number of objects while sending and unwrapping. We can even send more than two objects
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;
}
}
I have posted the solution to my blog with a little more description with simpler code to integrate easily.
Pass multiple complex objects to Web API
I hope it would help someone. I would be interested to hear from the experts here regarding the pros and cons of using this methodology.
Late answer, but you can take advantage of the fact that you can deserialize multiple objects from one JSON string, as long as the objects don't share any common property names,
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);
}
Create a Composite object
public class CollectiveObject<X, Y>
{
public X FirstObj;
public Y SecondObj;
}
initialize a composite object with any two objects which you willing to send.
CollectiveObject<myobject1, myobject2> collectiveobj =
new CollectiveObject<myobject1, myobject2>();
collectiveobj.FirstObj = myobj1;
collectiveobj.SecondObj = myobj2;
Do serialization
var req = JSONHelper.JsonSerializer`<CollectiveObject<myobject1, `myobject2>>(collectiveobj);`
`
your API must be like
[Route("Add")]
public List<APIAvailibilityDetails> Add([FromBody]CollectiveObject<myobject1, myobject2> collectiveobj)
{ //to do}

Categories

Resources