I'm trying to move a ASP.NET/MVC-.NET4.5 web application to ASP.NET CORE .NET 5.0, and I can't get json serialization/deserialization to work.
This depends upon the server being able to send json objects to the browser, and having the browser send them back - and the serialization/deserialization simply doesn't work.
I've created a simple test application, and it fails with no extraneous complexities.
I started with dotnet new webapp -o JsonTesting
.
To it I added an ApiController
.
My Startup:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
[...]
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapDefaultControllerRoute();
});
}
My model (Note - I'm intentionally using inconsistent casing):
public class Foo
{
public int AnInt { get; set; }
public double aDouble { get; set; }
public decimal ADecimal { get; set; }
public string aString { get; set; }
public DateTime aDateTime { get; set; }
public DateTimeOffset ADateTimeOffset { get; set; }
}
My controller:
[ApiController]
[Area("api")]
[Route("[area]/[controller]")]
public class FooController : ControllerBase
{
private List<Foo> _foos;
public FooController()
{
_foos = new List<Foo>
{
new Foo{
AnInt = 1,
aDouble = 1.1,
ADecimal = 1m,
aString = "One",
aDateTime = DateTime.Parse("2020-01-01T01:01:01"),
ADateTimeOffset = DateTimeOffset.Parse("2020-01-01T01:01:01")
},
new Foo{
AnInt = 2,
aDouble = 2.2,
ADecimal = 2m,
aString = "Two",
aDateTime = DateTime.Parse("2020-01-02T02:02:02"),
ADateTimeOffset = DateTimeOffset.Parse("2020-01-02T02:02:02")
}
};
}
[HttpGet]
public ActionResult<IEnumerable<Foo>> Get()
{
return _foos;
}
[HttpPost]
public ActionResult<bool> Post(Foo foo)
{
var match = _foos.SingleOrDefault(f => f.AnInt == foo.AnInt);
if (match == null)
return false;
if (foo.aDouble != match.aDouble)
return false;
if (foo.ADecimal != match.ADecimal)
return false;
if (foo.aString != match.aString)
return false;
if (foo.aDouble != match.aDouble)
return false;
if (foo.aDateTime != match.aDateTime)
return false;
if (foo.ADateTimeOffset != match.ADateTimeOffset)
return false;
return true;
}
}
The Get returns a list of Foos, a Post sends a Foo and returns true if it's in the list, false if not.
Then I added a table
to Pages/Index.cshtml:
<div class="text-center">
<table>
<th>
<td>Initial</td>
<td>Buttons</td>
</th>
<tbody id="thebody"></tbody>
</table>
</div>
And then some Javascript to call the GET endpoint to populate the table, and then to call the PUT endpoint when one of the buttons is clicked:
@section Scripts {
<script type="text/javascript">
debugger;
$(document).ready(function ()
{
var $thebody = $("#thebody");
$thebody.empty();
$.ajax({
url: '/api/Foo',
type: 'GET',
dataType: 'json',
contentType: 'application/json',
data: null
}).done(function (data, textStatus, jqXHR)
{
data.forEach(element =>
{
var $tr = $("<tr />");
var $td1 = $("<td />");
$td1.text(JSON.stringify(element));
$tr.append($td1);
var $td2 = $("<td />");
var $btn = $("<input type='button' />");
$btn.val(element.aString)
$btn.data('foo', element);
$td2.append($btn);
$tr.append($td2);
var $td3 = $("<td class='out'/>");
$tr.append($td3);
$btn.on('click', function (event)
{
var $target = $(event.target);
var foo = $target.data('foo');
$.ajax({
url: '/api/Foo',
type: 'POST',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({ 'foo': foo })
}).done(function (data, textStatus, jqXHR)
{
if (data)
{
alert('Matched');
} else
{
alert('Didn\'t match');
}
}).fail(function (jqXHR, textStatus, errprThrow)
{
alert(textStatus);
});
});
$thebody.append($tr);
});
}).fail(function (jqXHR, textStatus, errprThrow)
{
alert(textStatus);
});;
});
</script>
}
Walking through this in the debugger it looks like the Foo objects are being properly sent to the browser, and are being properly saved. When I click one of the buttons what I see in the Chrome DTools looks correct:
foo: {
"anInt": 1,
"aDouble": 1.1,
"aDecimal": 1,
"aString": "One",
"aDateTime": "2020-01-01T01:01:01",
"aDateTimeOffset": "2020-01-01T01:01:01-06:00"
}
But what shows up in FooController.Post() is zeroed out. None of the fields have been set from the values that were provided by the browser.
Yet when I look in the DevTools network tab, what I see in the Request Payload looks correct:
{"foo":{"anInt":1,"aDouble":1.1,"aDecimal":1,"aString":"One","aDateTime":"2020-01-01T01:01:01","aDateTimeOffset":"2020-01-01T01:01:01-06:00"}}
Any ideas as to what I'm doing wrong?
This just worked, using NewtonSoft.JSON in .NET 4.5. I hadn't expected to have issues moving to .NET 5.0.
===
OK, here's the thing. I'm a mostly backend developer, and I was copying a pattern our front-end developers commonly use. Or half-copied.
On a JQuery POST, what I put in the data
field needs to match the parameters of the endpoint function. Of course.
Our frontend devs routinely do this using data transfer objects (DTOs). I was supplying a DTO in the javascript, but I was not accepting a DTO in the endpoint.
One fix would be to pass the Foo object itself, as suggested in Brando Zhang's answer:
data: JSON.stringify(foo)
And for the endpoint to continue to accept a Foo:
[HttpPost]
public ActionResult<bool> Post(Foo foo)
{
[...]
}
The other would be to continue to pass a DTO:
data: JSON.stringify({ 'foo': foo })
And then to change the endpoint to accept a DTO:
public class FooDto
{
public Foo Foo { get; set; }
}
[HttpPost]
public ActionResult<bool> Post(FooDto dto)
{
Foo foo = dto.Foo;
[...]
}
Either works.