I'm currently using Web API v2 with OData v3 linked up to a Kendo Grid. I'm having problems getting the grid to serialize a model correctly to the PatchEntityAsync
method on the AsyncEntitySetController<TEntity, TKey>
class. The Delta<TEntity>
that is passed to the PatchEntityAsync
method is null
which obviously is not correct.
First, the Entity Framework models. I have a GameSeries
definition:
[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 the Game
definition, each Game
instance has a reference to a GameSeries
instance:
[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 querying for a Game
using JSON, and issuing an $expand
on the GameSeries
property, 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'm exposing these through an OData Web API (Microsoft.AspNet.WebApi.OData 5.2.0) endpoint to a Kendo UI Grid. Here's the configuration for the grid:
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 render the grid correctly, where I'll get GameSeries.Description
displayed instead of the numeric ID of the GameSeries
.
However, I believe part of the problem comes from how I define the custom editor, specifically the data
attributes that Kendo requires:
$('<input data-text-field="Description" data-value-field="GameSeriesId" data-bind="value:GameSeriesId"/>')
I get the feeling I should use dot notation to reference the GameSeries
property on the Game
instance, but I'm not sure how.
Additionally, I believe the binding here is contributing to the create command failing. There should be some way to set the data binding attributes which will allow for new creations, as well as editing existing ones.
However, when I get the editor to pop up for an existing instance, it does so correctly with the drop down list populated with all of the GameSeries
instances.
I can make changes, and when I do, I notice through Fiddler that the body is being passed through, although I notice some discrepancies:
{
"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 GameSeriesId
property is populated correctly with the change (I want 4) but the expanded GameSeries
property has a "GameSeriesId" of 1.
When this call is made, the Delta<Game>
instance passed in is null.
What I've tried:
I've noticed that the GameSeriesId
property on the expanded GameSeries
property isn't stringified. I've changed the value to "1"
" and the Delta<Game>
instance is still null.
I've replicated the call to the OData point to not include the extended GameSeries
property, so the payload looks like this:
{
"GameId": "1",
"Description":
"Street Fighter IV",
"GameSeriesId": "4",
"Revision": "1",
"Tag": "Test",
"Notes": null
}
And the Delta<Game>
is populated. I'm not sure if getting the expanded GameSeries
property droped from the payload is the right approach, or whether or not it should be tackled on the server side, or in the Kendo grid.
Since the foreign key id is changed successfully you can just exclude the navigation property GameSeries
when doing the update.
EF works so well when updating the relationship just by using the foreign key id.
So let the OData point to include the GameSeries
, but exclude it when doing update. You can use parameterMap 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 sync the editor with the grid, you need to bind the change event and change the property of the model in the grid manually.
gameSeriesIdAutoCompleteEditor = function (container, options) {
/* omitted code */
.kendoDropDownList({
/* omitted code */
change: function (e) {
options.model.GameSeries = this.dataItem();
}