0

I have an ASP.NET Core web app that works fine, when using this controller implementation:

[HttpPost]
public ActionResult SetAssignment()
{
    string b = "";

    using (StreamReader rdr = new StreamReader(Request.Body)) b = rdr.ReadToEnd();
    Assignment asg = JsonConvert.DeserializeObject<Assignment>(b);

    // asg is valid here

    // ... do other stuff ...
}

But it does NOT work when using this implementation:

[HttpPost]
public ActionResult SetAssignment([FromBody] Assignment asg)
{
    // asg is always NULL here

    // ... do other stuff...
}

Theoretically, these implementations should be identical (although the second would be way more elegant), no?

I seem to be missing something...

The way this method is called looks like this, btw:

function asgupdate() {
   var asp = {};
   asp.AssignmentId = $("#asp_idx").val();
   ...
        
   $.ajax({
      type: "POST",
      url: "/Project/SetAssignment",
      data: JSON.stringify(asp),
      contentType: "application/json; charset=utf-8",
      dataType: "json",
      success: function (r) {
          ...
      }
   });
};

The JSON request does get sent by the browser and it look correct (afaict).

The assignment model looks like this:

  public class Assignment
    {
        [Key]
        public int AssignmentId { get; set; }
        [Required]
        public int CrewMemberId { get; set; }
        [Required]
        public int ProjectId { get; set; }
        [DisplayName("Function")]
        public string Function { get; set; } = "";
        public string? Account { get; set; }
        public float Rate { get; set; } = 0;
        public float PrepWeeks { get; set; } = 0;
        public float ShootWeeks { get; set; } = 0;
        public float WrapWeeks { get; set; } = 0;
        public float PostWeeks { get; set; } = 0;
        public DateTime Created { get; set; } = DateTime.UtcNow;
        public DateTime Updated { get; set; } = DateTime.UtcNow;
        public AssignmentTypes? AssignmentType { get; set; } = AdditionalClasses.AssignmentTypes.SALARYNORMAL;
        public virtual CrewMember CrewMember { get; set; }
        public virtual Project? Project { get; set; }
     }
Remo Pini
  • 45
  • 6
  • What version of .Net are you using? – SBFrancies Sep 14 '22 at 17:51
  • Pls post the line services.AddController of your startup – Serge Sep 14 '22 at 17:53
  • Version: .Net 6.0 – Remo Pini Sep 14 '22 at 19:05
  • Program.cs: ... builder.Services.AddControllers().AddJsonOptions(x => x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles); ... Weirdly enough, other AJAX entry points in the same controller work just fine... only this one doesn't... – Remo Pini Sep 14 '22 at 19:07
  • Are you sure you're sending correct request? Maybe it's missing content-type header? Just guessing – Quercus Sep 14 '22 at 19:26
  • Added the calling function to the post above... – Remo Pini Sep 14 '22 at 19:47
  • [tag:asp.net-core] now uses [System.Text.Json](https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/) instead of Json.NET for JSON deserialization. Thus the statement *Theoretically, these implementations should be identical (although the second would be way more elegant), no?* is false. If you want to revert back to Json.NET see [Where did IMvcBuilder AddJsonOptions go in .Net Core 3.0?](https://stackoverflow.com/a/55666898/3744182). – dbc Sep 14 '22 at 20:02
  • 1
    If you want to move forward with System.Text.Json you will need to convert your `Assignment` data model to use [JSON attributes](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonattribute) from the [`System.Text.Json.Serialization`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization) namespace. To help you with that we need a [mcve], specifically your `Assignment` data model and a JSON sample. See [ask]. – dbc Sep 14 '22 at 20:03
  • @dbc I would absolutely understand that, if other AJAX requests in the same controller also using JSON and objects wouldn't work just fine. However, only this one doesn't. So the basic JSON (de)serialization works just fine. That is what confuses me to no end... – Remo Pini Sep 14 '22 at 20:09
  • 1
    System.Text.Json and Json.NET often produce identical results. But not always. See MSFT's [Table of differences between Newtonsoft.Json and System.Text.Json](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-6-0#table-of-differences-between-newtonsoftjson-and-systemtextjson) for differences. That's why a [mcve] is necessary, probably one of the differences from the table is tripping you up specifically for `Assignment` and not elsewhere. – dbc Sep 14 '22 at 20:11
  • 1
    Just as an example, System.Text.Json is **case-sensitive** by default when binding property names, while Json.NET is not. This can be overridden by setting `JsonSerializerOptions.PropertyNameCaseInsensitive = false`. So it's important to see a full [mcve] including both the data model and JSON when diagnosing problems. There are also differences in parsing enums, so if `AssignmentTypes` is an enum it would be helpful to include it also. I started a fiddle here: https://dotnetfiddle.net/wr1i7Q. You could fork it and complete it if you want. – dbc Sep 14 '22 at 20:19
  • @dbc: That seems to be the issue. I have replaced the Deserialization from "JsonConvert.DeserializeObject" to "System.Text.Json.JsonSerializer.Deserialize" and now I get meaningful error messages (mostly around numbers represented as strings). So now I need to figure out how to fix the ajax sending and/or some Json global settings... – Remo Pini Sep 14 '22 at 20:25
  • 1
    Set [`JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.numberhandling?view=net-6.0#system-text-json-jsonserializeroptions-numberhandling). See [System.Text.Json: Deserialize JSON with automatic casting](https://stackoverflow.com/q/59097784) which now looks to be a duplicate. – dbc Sep 14 '22 at 20:27
  • I added [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] to my controller, but I still get an error: System.Text.Json.JsonException Message=The JSON value could not be converted to System.Int32. Path: $.AssignmentId InvalidOperationException: Cannot get the value of a token type 'String' as a number. The JSON String is {"AssignmentId":"33", ...} so should theoretically work, no? – Remo Pini Sep 14 '22 at 20:41
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/248054/discussion-between-remo-pini-and-dbc). – Remo Pini Sep 14 '22 at 20:48

1 Answers1

1

Ok, the solution was a mix of several things, thanks to @dbc for pointing me in the right direction...

  1. I wrongly assumed that the automatic JSON conversion would be identical to Newtonsoft.JSON, which it is definitely NOT.

  2. Since System.Text.Json is more strict in how it interprets stuff, I had to add some options:

builder.Services.AddControllers().AddJsonOptions(x =>
    {
        x.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString;
        x.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
    });
  1. Since Enums are also treated differently (the NumberHandling options does NOT apply to Enum parsing, I had to change the generating JavaScript to always send an integer instead of a string. I tried to use JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()), however that broke the sending back of the enum (because it would now send the string name of the enum instead of the integer value back to the webfrontend, which couldn't deal with it).
Remo Pini
  • 45
  • 6