Cannot Serialize Navigation Properties Correctly When Performing API Patch Operation from Kendo Grid
I am currently using Web API v2 with OData v3 linked to Kendo grid. I'm having trouble getting the mesh to serialize the model correctly for a method PatchEntityAsync
per AsyncEntitySetController<TEntity, TKey>
class . Delta<TEntity>
which is passed to the method PatchEntityAsync
, null
which is obviously wrong.
First, the Entity Framework models. I have a definition GameSeries
:
[Table("stats.GameSeries")]
public class GameSeries
{
[Key]
public int GameSeriesId { get; set; }
[MaxLength(500)]
[Required]
public string Description { get; set; }
public string Notes { get; set; }
}
And then there is a definition Game
, each instance Game
has an instance reference GameSeries
:
[Table("stats.Game")]
public class Game
{
[Key]
public int GameId { get; set; }
[MaxLength(500)]
[Required]
public string Description { get; set; }
public int GameSeriesId { get; set; }
[ForeignKey("GameSeriesId")]
public virtual GameSeries GameSeries { get; set; }
public int Revision { get; set; }
[MaxLength(100)]
public string Tag { get; set; }
public string Notes { get; set; }
}
When requesting for Game
using JSON and issuing $expand
in a property, GameSeries
I get the following, which is expected / correct:
{
"odata.metadata":
"http://localhost:7566/odata/$metadata#Games",
"odata.count":"58",
"value":[
{
"GameSeries": {
"GameSeriesId": 1,
"Description":"Street Fighter IV",
"Notes":null
},
"GameId": 1,
"Description": "Street Fighter IV",
"GameSeriesId": 1,
"Revision": 1,
"Tag": null,
"Notes": null
}, {
"GameSeries": {
"GameSeriesId":1,
"Description": "Street Fighter IV",
"Notes": null
},
"GameId": 2,
"Description": "Super Street Fighter IV",
"GameSeriesId": 1,
"Revision": 2,
"Tag": null,
"Notes": null
},
// And so on...
]
}
I view them through the OData Web API endpoint (Microsoft.AspNet.WebApi.OData 5.2.0) into the Kendo UI grid. Here's the mesh configuration:
function initializeGrid(selector, entitySet, key, modelFields, columns, expand) {
// Edit and destroy commands.
columns.push({ command: ["edit", "destroy"], title: "Operations" });
// The main key is not editable.
modelFields[key].editable = false;
modelFields[key].defaultValue = 0;
var baseODataUrl = "/odata/" + entitySet,
options = {
dataSource: {
type: "odata",
pageSize: 50,
//autoSync: true,
transport: {
read: {
url: baseODataUrl,
dataType: "json",
data: {
$expand: expand
}
},
update: {
url: function(data) {
return baseODataUrl + "(" + data[key] + ")";
},
type: "patch",
dataType: "json"
},
destroy: {
url: function (data) {
return baseODataUrl + "(" + data[key] + ")";
},
dataType: "json"
},
create: {
url: baseODataUrl,
dataType: "json",
contentType: "application/json;odata=verbose"
}
},
batch: false,
serverPaging: true,
serverSorting: true,
serverFiltering: true,
schema: {
data: function (data) {
return data.value;
},
total: function (data) {
return data["odata.count"];
},
model: {
id: key,
fields: modelFields
}
}
},
height: 550,
toolbar: ["create"],
filterable: true,
sortable: true,
pageable: true,
editable: "popup",
navigatable: true,
columns: columns
};
selector.kendoGrid(options);
}
$(function () {
var baseODataUrl = "/odata/",
gameSeriesIdDataSource = new kendo.data.DataSource({
type: "odata",
schema: {
data: function (data) {
return data.value;
},
total: function (data) {
return data["odata.count"];
}
},
transport: {
read: {
url: baseODataUrl + "GameSeries",
dataType: "json"
}
}
}),
gameSeriesIdAutoCompleteEditor = function(container, options) {
$('<input data-text-field="Description" data-value-field="GameSeriesId" data-bind="value:GameSeriesId"/>')
.appendTo(container)
.kendoDropDownList({
autoBind: false,
dataSource: gameSeriesIdDataSource,
dataTextField: "Description",
dataValueField: "GameSeriesId"
});
};
initializeGrid($("#grid"), "Games", "GameId", {
GameId: {
title: "Game ID",
editable: false
},
Description: { type: "string" },
GameSeriesId: { type: "integer" },
Revision: { type: "integer" },
Tag: { type: "string" },
Notes: { type: "string" }
}, [
{ field: "GameId", title: "Game ID" },
"Description",
{ field: "GameSeries.Description", title: "Game Series", editor: gameSeriesIdAutoCompleteEditor },
"Revision",
"Tag",
"Notes"
], "GameSeries");
});
}(jQuery));
This will lead to the correct grid where I get the GameSeries.Description
numeric ID instead GameSeries
.
However, I believe that part of the problem has to do with how I define the custom editor, specifically the attributes data
that Kendo requires:
$('<input data-text-field="Description" data-value-field="GameSeriesId" data-bind="value:GameSeriesId"/>')
It seems to me that I should be using dot notation to refer to a property GameSeries
on an instance Game
, but I'm not sure how.
Also, I believe the binding here is causing the create command to fail. There must be some way to set the data-binding attributes that will allow new creations to be created as well as editing existing ones.
However, when I get the editor for the popup for an existing instance, it does so correctly with the dropdown populated with all instances GameSeries
.
I can make changes and when I do, I notice through Fiddler that the body is going through, although I do notice some inconsistencies:
{
"GameSeries": {
"GameSeriesId": 1,
"Description": "Street Fighter IV",
"Notes": null
},
"GameId": "1",
"Description":
"Street Fighter IV",
"GameSeriesId": "4",
"Revision": "1",
"Tag": "Test",
"Notes": null
}
In this case, the property is GameSeriesId
populated correctly with the change (I want 4), but the extended property GameSeries
has a "GameSeriesId" of 1.
When this call is made, the instance Delta<Game>
that passed is null.
What I have tried:
I noticed that the property GameSeriesId
in the extended property is GameSeries
not a string. I changed the value to "1"
"and the instance Delta<Game>
is still zero.
I have replicated the call to the OData point to not include the extended property GameSeries
, so the payload looks like this:
{
"GameId": "1",
"Description":
"Street Fighter IV",
"GameSeriesId": "4",
"Revision": "1",
"Tag": "Test",
"Notes": null
}
And it fills up Delta<Game>
. I'm not sure if getting an extended property GameSeries
falling out of the payload is the right approach or should be done server side or Kendo grid.
source to share
Since the foreign key ID has been successfully changed, you can simply exclude the navigation property GameSeries
when doing the update.
EF works so well when updating a relationship with only foreign key id.
So let OData point to include GameSeries
, but exclude it when doing an update. You can use the Map parameter to intercept the update operation.
parameterMap: function (data, type) {
if (type === "update") {
delete data.GameSeries;
return JSON.stringify(data);
}
// Returns as it is.
return data;
}
Update
To keep the editor in sync with the grid, you need to bind the change event and manually change the model property on the grid.
gameSeriesIdAutoCompleteEditor = function (container, options) {
/* omitted code */
.kendoDropDownList({
/* omitted code */
change: function (e) {
options.model.GameSeries = this.dataItem();
}
source to share