0

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.

Jeff Dege
  • 11,190
  • 22
  • 96
  • 165

2 Answers2

1

As far as I know, there is something wrong with your Json format, the right Json format should be like below:

{"anInt":1,"aDouble":1.1,"aDecimal":1,"aString":"One","aDateTime":"2020-01-01T01:01:01","aDateTimeOffset":"2020-01-01T01:01:01-06:00"}

So you should modify the jquery like below:

@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 )
                    }).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>
}

Result:

enter image description here

Brando Zhang
  • 22,586
  • 6
  • 37
  • 65
-1

You need to annotate your POST parameter withFromBodyAttribute:

    [HttpPost]
    public ActionResult<bool> Post([FromBody] Foo foo)
Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
  • It was my understanding that ASP.NET pulled complex types from the body, and that [FromBody] was only needed it you were passing a simple type, But be that as it may, I tried it, and it made no difference. I even went so far as to copy the string from DevTools and to paste it into a local variable on the server, and JsonSerializer.Deserialize() still didn't work. – Jeff Dege Jul 27 '21 at 23:01