Search code examples
jqueryjsonasp.net-core.net-7.0

Formatting json to map Asp.Net Core Model binding


I have an Asp.Net Core web application client which uses Ajax JQuery to post changes to server. In a particular page, I have to append some data to the request so the model gets populated additionnal properties values because the data is created with an editable table, so there is no regular input fields for this data. However I cannot find a way to format the json for the model mapping. The server doesn't map values at all for this additionnal property.

Here is the model (simplified) :

public class ProductCategoryModel {
    public ProductCategoryDto Category { get; set; } = new();
}

public class ProductCategoryDto
{
    public string Description { get; set; } = default!;
    public List<ProductSubCategoryDto> ListProductSubCategories { get; set; } = new();
}

public class ProductSubCategoryDto
{
    public Guid? Id { get; set; }
    public string Description { get; set; } = default!;
}

Here is the original request data, which is working fine :

[{"name":"Model.Category.Description","value":"CATEGORY"}]

I tried to add an array like this :

[{"name":"Model.Category.Description","value":"CATEGORY"},
{"name":"Model.Category.ListProductSubCategories",
 "value":[
    {"Id":null,"Description":"Nouvelle sous-catégorie"},
    {"Id":null,"Description":"Nouvelle sous-catégorie 2"}
 ]
}]

And I tried to add each array item like this :

[{"name":"Model.Category.Description","value":"CATEGORY"},
{"name":"Model.Category.ListProductSubCategories","value":{"Id":null,"Description":"Nouvelle sous-catégorie"}},
{"name":"Model.Category.ListProductSubCategories","value":{"Id":null,"Description":"Nouvelle sous-catégorie 2"}}]

So I want to populate the ListProductSubCategories to get the values in the server-side's model, but I always receive an empty list. How can I achieve that?

Ajax call :


function mapAssociatedToSimpleArray(obj) {
    var a3 = Object.keys(obj).map(function (k) { return obj[k]; })
    return a3;
}

var data = $("#form").serializeArray();

var listSubCategories = mapAssociatedToSimpleArray(_dataSource); // <--  _dataSource is an associated array data to be appended to the request
                
data.push({
    name: "Model.Category.ListProductSubCategories", value: listSubCategories
});
        
$.ajax({
    url: "/inv/ProductsCategoriesController/Edit/@Model.Categorie.Id",
    type: 'POST',
    dataType: 'json',
    data: data,
    headers: {'RequestVerificationToken': $(".AntiForge input").val() },
    success: function (data) {        
        // ...
    },
    error: function (xhr, ajaxOptions, thrownError) {
        if (utils.hasJsonStructure(xhr.responseText)) {
            $("#form").handleModelErrors(JSON.parse(xhr.responseText));
        }
        notifications.error();
    }            
});   

Controller Post action :

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(Guid id, ProductCategoryModel model)
{
    if (!ModelState.IsValid)
    {
        return ModelState.ReturnErrorsToClient(Response);
    }

    model.Category.Id = id;
    await _productsCategoriesBackend.UpdateProductCategory(model.Category);

    return new JsonResult(model.Category.Id);            
}

In controller, the parameter model.Category.ListProductSubCategories is always empty.

EDIT

I should have made it clearer that the table data are inserted with input fields, but the fields are displayed only when a row needs to be edited. After editing is done, the input data is written to the table as well as in a json array, and the input fields are removed. So there is no fields available to the form when the form needs to be submitted. That's why I need to send a formatted asp.net structure of this table model when the form is submitted.


Solution

  • I found a recipe that is working well.

    For the action method, I added an explicit [FromBody] attribute on the model parameter, so the framework uses the Json input formatter.

    For the ajax call, I need to add the Content-Type header, as well as stringify the data.

    type: 'POST',
    dataType: 'json',
    data: JSON.stringify(data),
    headers: {
        "RequestVerificationToken": $(".antiForge input").val() , 
        "Content-Type": "application/json; charset=UTF-8"
    },
    

    I am then able to pass a json object matching the model properties, instead of trying to create a form-encoded object with the data.

    I used the jquery.serializeToJSON plugin to build a nice object of the form fields :

    var data = $('#form').serializeToJSON();
    data.Category.ListProductSubCategories = _dirtyRows;
    

    Then I can add additional property to this object to complete the model with the table's dirty rows data. Being able to pass a json object is much more handier than fiddling with a "x-www-form-urlencoded" object.

    The following articles helped me a lot in finding my way through this :

    https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-7.0

    https://andrewlock.net/model-binding-json-posts-in-asp-net-core/

    POST JSON fails with 415 Unsupported media type, Spring 3 mvc

    https://www.jqueryscript.net/form/Serialize-Form-Data-Into-JSON-Object-In-jQuery-serializeToJSON.html