Data binding and serialization issue with System.Text.Json and JsonStringEnumConverter

1 Answer 3298 Views
Grid
David
Top achievements
Rank 1
Iron
Iron
Veteran
David asked on 29 Mar 2023, 05:38 PM

Hi,

I am using System.Text.Json and Kendo UI in a .NET Core 6.0 project. I have followed this article in order to solve the property name casing issue: https://docs.telerik.com/aspnet-core/installation/json-serialization.

However, I am also using a JsonStringEnumConverter as follows:

c.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());


.AddJsonOptions(c =>
{
      c.JsonSerializerOptions.PropertyNamingPolicy = null;
      c.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
      c.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
})

This results in all enums being serialised as null in grids when read using an Ajax DataSource call. How can I get Kendo to serialise enums as strings?

Kind regards,

David 

1 Answer, 1 is accepted

Sort by
0
Aleksandar
Telerik team
answered on 03 Apr 2023, 09:29 AM

Hello David,

The JSON Serialization article linked demonstrates how to configure the ASP.NET Core application to use PascalCase serialization rather than the default option - camelCase. As demonstrated one could use the default library - System.Text.Json - as well as Newtonsoft.JSON, if that's preferred.

That said, the AddJsonOptions method is an extension method that configures JsonOptions for the specified builder. In Microsoft's documentation I see that using the JsonStringEnumConverter serializes enums, including nullable enums, so if there is an issue with serialization of enums in general I can suggest investigating further in that direction.  From the details provided I do not understand how setting the configuration  indicated is related to the Telerik UI for ASP.NET Core library. If I am missing something I will ask you to provide a minimal reproducible example where enums are serialized as expected by the framework, but using Telerik UI for ASP.NET Core causes the reported behavior. This way I will be able to observe and debug the issue and advise further.

Regards,
Aleksandar
Progress Telerik

Virtual Classroom, the free self-paced technical training that gets you up to speed with Telerik and Kendo UI products quickly just got a fresh new look + new and improved content including a brand new Blazor course! Check it out at https://learn.telerik.com/.

David
Top achievements
Rank 1
Iron
Iron
Veteran
commented on 06 Apr 2023, 06:03 AM

Hi Aleksandar,

The issue is pretty simple. I'm not sure how I'm supposed to provide you with a minimal reproducible example given I can only edit the Razor code in your online examples, but I'll describe the problem using the example here: https://demos.telerik.com/aspnet-core/grid/customajaxbinding. If there's a way I can edit the Controller and Model files to reproduce this for you let me know, but the description below should be pretty clear and easily reproducible for you.

In the example above there is an Kendo.Mvc.Examples.Models.Order. It has no enums but imagine I add an OrderStatusEnum to the Order model:

public enum OrderStatusEnum
{
    Received = 1,
    Shipped = 2
}
using System;
using System.Collections.Generic;

namespace Kendo.Mvc.Examples.Models
{
    public partial class Order
    {
        public Order()
        {
            OrderDetails = new HashSet<OrderDetail>();
        }

        public int OrderID { get; set; }
        public OrderStatusEnum OrderStatus { get; set; }
        ...        
    }
}

I then configure JSON serialisation as specified here https://docs.telerik.com/aspnet-core/installation/json-serialization but I also add a converter to serialise enums as strings:

.AddJsonOptions(c =>
{
      c.JsonSerializerOptions.PropertyNamingPolicy = null;
      c.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
})

I then create a Grid with custom AJAX binding as per the example but also including the enum property:

@(Html.Kendo().Grid<Kendo.Mvc.Examples.Models.Order>()
    .Name("Grid")
    .Columns(columns => {
        columns.Bound(o => o.OrderID).Groupable(false);
        columns.Bound(o => o.OrderStatus);
        columns.Bound(o => o.ShipCity);
        columns.Bound(o => o.ShipCountry);
        columns.Bound(o => o.ShipName);
    })
    .Pageable()
    .Sortable()
    .Filterable()
    .Scrollable()
    .Groupable()
    .DataSource(dataSource => dataSource
        .Ajax()
        .Read(read => read.Action("CustomAjaxBinding_Read", "Grid"))
        .PageSize(15)
    )
)

And if you inspect the data being returned, OrderStatus is null for every row because the Grid is expecting JSON with an int for the OrderStatus.

I have already decided that it's too onerous to deal with so have removed the JsonStringEnumConverter and just added descriptive enum names using an OpenAPI extension. I discovered that it's possible to force the grid to read the OrderStatus as a string using the AjaxDataSourceBuilder.Model configurator:


@(Html.Kendo().Grid<Kendo.Mvc.Examples.Models.Order>()
    .Name("Grid")
    .Columns(columns => {
        columns.Bound(o => o.OrderID).Groupable(false);
        columns.Bound(o => o.OrderStatus);
        columns.Bound(o => o.ShipCity);
        columns.Bound(o => o.ShipCountry);
        columns.Bound(o => o.ShipName);
    })
    .Pageable()
    .Sortable()
    .Filterable()
    .Scrollable()
    .Groupable()
    .DataSource(dataSource => dataSource
        .Ajax()
        .Model(model => model.Field<string>("OrderStatus"))
        .Read(read => read.Action("CustomAjaxBinding_Read", "Grid"))
        .PageSize(15)
    )
)

I'm assuming there is also some attribute I could put on Order.OrderStatus such as a DataType, but if it's something we have to do for every enum property used with custom AJAX binding then it's not worth the risk or effort. 

That is my question - is there a way to configure ASP.NET Core Grid custom AJAX binding to expect enums as strings without having to decorate or configure the model for every single enum property?

Kind regards,

David

Aleksandar
Telerik team
commented on 10 Apr 2023, 09:22 AM

Thank you, David, for the details and elaborating more. I see what you are trying to do, though I am not sure I understand the reason for the need to use enums as strings. The approach you have taken, if serializing enums as strings is mandatory, is a viable one, though you will have to configure the model definition, so the DataSource threats the properties as strings.

By default the Grid can bind to Enums and will render the corresponding member as text. The Grid will indeed expect an integer for that field. If the reason is related to editing enum fields, then you can easily create an EnumEditor and place it in the EditorTemplates Folder. All it would take for a property to use the editor would be to decorate it with the [UIHint] attribute:

EnumEditor.cshtml

@model object

@(
 Html.Kendo().DropDownListFor(m => m)
        .BindTo(Html.GetEnumSelectList(ViewContext.ViewData.ModelMetadata.ModelType))
        .HtmlAttributes(new { title = Html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName("")})
)

Model:

    public class OrderViewModel
    {
...
        [UIHint("EnumEditor")]
        public OrderStatusEnum OrderStatus { get; set; }

        [UIHint("EnumEditor")]
        public OrderTypeEnum OrderType { get; set; }
    }
public enum OrderStatusEnum { Received = 1, Shipped = 2 }
public enum OrderTypeEnum { Fragile = 0, Heavy = 1 }

View:

        @(Html.Kendo().Grid <TelerikAspNetCoreApp684.Models.OrderViewModel>()
            .Name("grid")
            .Columns(columns =>
            {
                columns.Bound(p => p.OrderID).Filterable(false);
                columns.Bound(o => o.OrderStatus);
                columns.Bound(o => o.OrderType);
                columns.Bound(p => p.Freight);
                columns.Bound(p => p.OrderDate).Format("{0:MM/dd/yyyy}");
                columns.Bound(p => p.ShipName);
                columns.Bound(p => p.ShipCity);
            })
            .Editable(e=>e.Mode(GridEditMode.InCell))
            .Pageable()
            .ToolBar(t=> { t.Create(); t.Save(); })
            .Sortable()
            .Scrollable()
            .Groupable()
            .Filterable()
            .HtmlAttributes(new { style = "height:550px;" })
            .DataSource(dataSource => dataSource
                .Ajax()
                .PageSize(20)
                .Model(m=>{
                    m.Id(i=>i.OrderID);
                    m.Field(f=>f.OrderID).Editable(false);
                    m.Field(f=>f.OrderStatus).DefaultValue(1);
                    m.Field(f=>f.OrderType).DefaultValue(1);
                })
                .Read(read => read.Action("Orders_Read", "Grid"))
                .Create(read => read.Action("Orders_Create", "Grid"))
                .Update(read => read.Action("Orders_Update", "Grid"))
                )
        )

Controller:

public ActionResult Orders_Read([DataSourceRequest] DataSourceRequest request)
        {
            var result = Enumerable.Range(1, 50).Select(i => new OrderViewModel
            {
                OrderID = i,
                Freight = i * 10,
                OrderDate = new DateTime(2016, 9, 15).AddDays(i % 7),
                ShipName = "ShipName " + i,
                ShipCity = "ShipCity " + i,
                OrderStatus = i % 2 == 0 ? OrderStatusEnum.Received : OrderStatusEnum.Shipped,
                OrderType = i % 2 == 0 ? OrderTypeEnum.Heavy : OrderTypeEnum.Fragile
            }) ;

            var dsResult = result.ToDataSourceResult(request);
            return Json(dsResult);
        }
        public ActionResult Orders_Create([DataSourceRequest] DataSourceRequest request,OrderViewModel model)
        {
            //create new item
            return Json(new[] { model }.ToDataSourceResult(request));
        }

        public ActionResult Orders_Update([DataSourceRequest] DataSourceRequest request, OrderViewModel model)
        {
            //update item
            return Json(new[] { model }.ToDataSourceResult(request));
        }

Here is a screencast of the behavior. The member name is rendered in the column by default, instead of the underlying value. The EnumEditor above generates a DropDownList with the available options for the particular type. Binding works as expected and the only decoration is the need to add the [UIHint] attribute to the ViewModel property. A sample application is attached for you to review.

David
Top achievements
Rank 1
Iron
Iron
Veteran
commented on 10 Apr 2023, 01:21 PM | edited

Thanks for the response Aleksandar.

I see what you are trying to do, though I am not sure I understand the reason for the need to use enums as strings.

We have a legacy MVC application that we are embedding React components into. In our WebAPIs we serialise enums as strings to improve readability in Swagger and in the TypeScript client enums generated by NSwag. We were attempting to do the same in our MVC application.

As mentioned, I have already decided that it's too risky to change how enums are serialised - we have many examples of MVC Kendo grids using custom AJAX binding that use enums not just for display but also in codebehind. In one example, the enum is not displayed at all but used in a script to select a cell template. I removed the JsonStringEnumConverter and added descriptive enum names using an OpenAPI extension, so we still have enums serialised as integers by default but the enums in the generated TypeScript client have meaningful names as well.

Thanks for your help. I'm not sure if it's common to use KendoReact components embedded in MVC applications using Telerik UI for ASP.NET Core, but we are doing so and haven't had many issues so far. I would still be interested to know if it's possible to globally configure how enums are serialised using custom AJAX binding or if it's something that has to be done for each property in Telerik UI for ASP.NET Core?

Kind regards,

David

Aleksandar
Telerik team
commented on 13 Apr 2023, 08:22 AM

Most often Telerik UI for ASP.NET Core components are used with .NET Core applications. We have a general sample and documentation demonstrating the use of KendoReact with Telerik UI for ASP.NET Core, but I suspect you are already aware of these examples:

However I have no knowledge on how often both suits of components are used together in order to provide further details. 

That said, serializing the Enums as strings would also work for the Telerik UI for ASP.NET Core Grid:

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddMvc()
    .AddJsonOptions(options =>
        {
            options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
            options.JsonSerializerOptions.PropertyNamingPolicy = null;
        });
builder.Services.AddKendo();

var app = builder.Build();

EnumEditor.cshtml

@model object

@(
 Html.Kendo().DropDownListFor(m => m)
        .BindTo(Html.GetEnumSelectList(ViewContext.ViewData.ModelMetadata.ModelType))
        .DataTextField("Text")
        .DataValueField("Text")
        .HtmlAttributes(new { title = Html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName("")})
)

Grid definition

        @(Html.Kendo().Grid <TelerikAspNetCoreApp684.Models.OrderViewModel>()
            .Name("grid")
            .Columns(columns =>
            {
                columns.Bound(p => p.OrderID).Filterable(false);
                columns.Bound(p => p.Freight);
                columns.Bound(o => o.OrderStatus).ClientTemplate("#=OrderStatus#");
                columns.Bound(o => o.OrderType).ClientTemplate("#=OrderType#");
                columns.Bound(p => p.OrderDate).Format("{0:MM/dd/yyyy}");
                columns.Bound(p => p.ShipName);
                columns.Bound(p => p.ShipCity);
            })
            .Editable(e=>e.Mode(GridEditMode.InCell))
            .Pageable()
            .ToolBar(t=> { t.Create(); t.Save(); })
            .Sortable()
            .Scrollable()
            .Groupable()
            .Filterable()
            .HtmlAttributes(new { style = "height:550px;" })
            .DataSource(dataSource => dataSource
                .Ajax()
                .PageSize(20)
                .Model(m=>{
                    m.Id(i=>i.OrderID);
                    m.Field(f=>f.OrderID).Editable(false);
                    m.Field<string>("OrderStatus").DefaultValue(OrderStatusEnum.Received);
                    m.Field<string>("OrderType").DefaultValue(OrderTypeEnum.Fragile);
                })
                .Read(read => read.Action("Orders_Read", "Grid"))
                .Create(read => read.Action("Orders_Create", "Grid"))
                .Update(read => read.Action("Orders_Update", "Grid"))
                )
        )

I updated the previously sent application to serialize enums as strings, updated the DataValueField for the custom editor and set the DataSource Model configuration to indicate that the enum fields should expect strings rather than integers. I also set default value to support creation of new items. The only caveat is that I cannot set the Enum serialization to PascalCase as the only JsonNamingPolicy currently available is CamelCase. I found the following issue in the dotnet repository on the matter, though besides the lower case for newly created items this doesn't seem to affect the binding behavior as you can see in this screencast. So if you update the model definition in the above-demonstrated way you should be able to use enums serialized as strings. I am also attaching the updated application, for reference.

I hope this helps

David
Top achievements
Rank 1
Iron
Iron
Veteran
commented on 13 Apr 2023, 08:56 AM

Thanks Aleksandar,

I already identified that as a workaround in my previous post. As I mentioned we have gone with a different approach, our project is too large to specify model configuration for every enum property using custom AJAX binding.

Kind regards,

David

Tags
Grid
Asked by
David
Top achievements
Rank 1
Iron
Iron
Veteran
Answers by
Aleksandar
Telerik team
Share this question
or