Reputation: 10614
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;
}
}
Upvotes: 9
Views: 4758
Reputation: 10614
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.
Upvotes: 6
Reputation: 43931
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;
});
Upvotes: 0