OData-v4 Binding
OData (Open Data Protocol) defines best practices when it comes to building and consuming RESTful APIs in a dependable manner. It enables you to mimic a Web API but with built-in support for filtering, selecting, and expanding amongst other capabilities. Binding the ASP.NET Core Grid through OData-v4, allows you to elevate its REST API by introducing advanced querying options.
For a runnable example, refer to the demo on OData binding of the Grid component.
Installing the OData Package
To install the required dependencies for using OData, install the autonomous Microsoft.AspNetCore.OData
NuGet package in your project.
Building the Edm Model and Configuring the Service in ASP.NET Core 6
For applications using .NET Core 6 and the minimal hosting model, the Edm model and service need to be configured through the Program.cs
file in the following way:
builder.Services.AddControllers().AddOData(options =>
{
options.AddRouteComponents("odata", GetEdmModel());
options.Select() // Querying options.
.Filter()
.Count()
.OrderBy()
.Expand()
.Select()
.SetMaxTop(null);
});
static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products"); // Model that will be used for the Grid.
return builder.GetEdmModel();
}
Building the Edm Model and Configuring the Service in ASP.NET Core 3.1 and ASP.NET Core 5
For applications using .NET Core 3.1 and 5, the Edm model and service need to be configured through the ConfigureServices
method in the following way:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddOData(
options => options.Select().Filter().OrderBy().Expand().Count().SetMaxTop(null).AddRouteComponents(
"odata",
modelBuilder.GetEdmModel()));
}
static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products"); // Model that will be used for the Grid.
return builder.GetEdmModel();
}
Adding an OData Controller
To support writing and reading data using the OData formats, the ODataController
base class needs to be inherited for a given controller instance.
public class ProductsController : ODataController
{
...
}
From there, the REST API endpoints need to be decorated with the EnableQuery
attribute. This attribute is responsible for applying the query options that are passed in the query string.
public class ProductsController : ODataController
{
[HttpGet]
[EnableQuery]
public List<Product> GetProducts()
{
var products = GetProducts(); // Call to the database.
return products;
}
[HttpPut]
[EnableQuery]
public IActionResult Put([FromODataUri] int key, [FromBody] Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Custom update logic.
return Updated(product);
}
[HttpPost]
[EnableQuery]
public IActionResult Post([FromBody] Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Custom create logic.
return Created(product);
}
[HttpDelete]
public IActionResult Delete([FromODataUri] int key)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Custom delete logic.
return NoContent();
}
}
Configuring the Grid for OData-v4 Binding
To implement the OData binding within the boundaries of the Telerik UI for ASP.NET Core Grid, specify the .Type("odata-v4")
configuration method within the DataSource. This ensures that the requests can be sent to the OData endpoint in the expected format and out of the box.
@(Html.Kendo().Grid<Product>()
.Name("grid")
.Columns(columns =>
{
columns.Bound(p => p.ProductName);
columns.Bound(p => p.UnitsInStock);
columns.Bound(p => p.Discontinued);
columns.Bound(p => p.UnitsOnOrder);
columns.Bound(p => p.UnitPrice).Width(150);
})
.Pageable()
.DataSource(dataSource => dataSource
.Custom()
.Type("odata-v4")
.Schema(schema => schema
.Model(model =>
{
model.Id(t => t.ProductID);
model.Field(t => t.ProductID).Editable(false);
model.Field(t => t.ProductName);
model.Field(t => t.UnitPrice);
model.Field(t => t.UnitsInStock);
model.Field(t => t.UnitsOnOrder);
model.Field(t => t.Discontinued);
}))
.Transport(transport =>
{
transport.Read(read => read.Url("/odata/Products"));
})
.PageSize(10)
.ServerPaging(true)
)
)
<script>
// Initialize DataSource with the dataStore option.
var dataSourceOptions = {
type: "odata-v4",
transport: {
read: {
url: function () {
return "/odata/Products";
}
}
},
schema: {
model: {
id: "ProductID",
fields: {
ProductID: { editable: false },
ProductName: { type: "string" },
UnitPrice: { type: "number" },
UnitsInOrder: {type: "number"},
Discontinued: { type: "boolean" },
UnitsInStock: { type: "number" }
}
}
},
pageSize: 10
};
var dataSource = new kendo.data.DataSource(dataSourceOptions);
</script>
<kendo-grid name="Grid" datasource-id="dataSource">
<columns>
<column field="ProductName"/>
<column field="UnitPrice" width="140"/>
<column field="UnitsInStock" width="140"/>
<column field="Discontinued" width="100"/>
</columns>
<pageable enabled="true"/>
</kendo-grid>
Configuring the CRUD Operations
To configure CRUD operations that support OData-v4 Binding, explicitly add a ClientHandlerDescriptor
that will be responsible for mapping the OData-v4 endpoints.
@(Html.Kendo().Grid<Product>()
.Name("grid")
.Columns(columns =>
{
columns.Bound(p => p.ProductName);
columns.Bound(p => p.UnitsInStock);
columns.Bound(p => p.Discontinued);
columns.Bound(p => p.UnitsOnOrder);
columns.Bound(p => p.UnitPrice).Width(150);
columns.Command(c => {c.Edit(); c.Destroy();}).Width(150);
})
.Toolbar(toolbar => toolbar.Create())
.Pageable()
.Editable(editable => editable.Mode(GridEditMode.InLine))
.DataSource(dataSource => dataSource
.Custom()
.Type("odata-v4")
.Schema(schema => schema
.Model(model =>
{
model.Id(t => t.ProductID);
model.Field(t => t.ProductID).Editable(false);
model.Field(t => t.ProductName);
model.Field(t => t.UnitPrice);
model.Field(t => t.UnitsInStock);
model.Field(t => t.UnitsOnOrder);
model.Field(t => t.Discontinued);
}))
.Transport(transport =>
{
transport.Read(read => read.Url("/odata/Products"));
transport.Update(new { url = new Kendo.Mvc.ClientHandlerDescriptor() { HandlerName = "update" } });
transport.Destroy(new { url = new Kendo.Mvc.ClientHandlerDescriptor() { HandlerName = "destroy" } });
transport.Create(new { url = new Kendo.Mvc.ClientHandlerDescriptor() { HandlerName = "create" } });
})
.PageSize(10)
.ServerPaging(true)
)
)
<script>
function update(dataItem){
return "/odata/Products(" + dataItem.ProductID + ")"
}
function create(){
return "/odata/Products"
}
function destroy(dataItem){
return "/odata/Products(" + dataItem.ProductID + ")"
}
</script>
<script>
// Initialize DataSource with the dataStore option.
var dataSourceOptions = {
type: "odata-v4",
transport: {
read: {
url: function () {
return "/odata/Products";
}
},
update: {
url: function (dataItem) {
return "/odata/Products(" + dataItem.ProductID + ")";
}
},
create: {
url: function (dataItem) {
delete dataItem.ProductID;
return "/odata/Products";
}
},
destroy: {
url: function (dataItem) {
return "/odata/Products(" + dataItem.ProductID + ")";
}
}
},
schema: {
model: {
id: "ProductID",
fields: {
ProductID: { editable: false },
ProductName: { type: "string" },
UnitPrice: { type: "number" },
UnitsInOrder: {type: "number"},
Discontinued: { type: "boolean" },
UnitsInStock: { type: "number" }
}
}
},
pageSize: 10
};
var dataSource = new kendo.data.DataSource(dataSourceOptions);
</script>
<kendo-grid name="Grid" datasource-id="dataSource">
<columns>
<column field="ProductName"/>
<column field="UnitsInStock"/>
<column field="Discontinued"/>
<column field="UnitPrice" width="150"/>
<column width="150">
<commands>
<column-command text="Edit" name="edit"></column-command>
<column-command text="Delete" name="destroy"></column-command>
</commands>
</column>
</columns>
<editable mode="inline"/>
<toolbar>
<toolbar-button name="create"></toolbar-button>
</toolbar>
<pageable enabled="true"/>
</kendo-grid>
Configuring Batch Editing
Enabling Batch Editing in OData-v4 Binding Scenarios with ASP.NET Core 6
-
Add a default batch handler within the
AddOData()
extensions method and inject theUseODataBatching()
middleware.var defaultBatchHandler = new DefaultODataBatchHandler(); defaultBatchHandler.MessageQuotas.MaxNestingDepth = 2; builder.Services.AddControllers() .AddOData(options => { options.AddRouteComponents("odata", GetEdmModel(), defaultBatchHandler); options.Select() .Filter() .Count() .OrderBy() .Expand() .Select() .SetMaxTop(null); }); app.UseODataBatching();
-
Within the Grid, set the Batch operation with
ClientHandlerDescriptor
to support batching and enable theBatch()
option of the Grid's DataSource.@(Html.Kendo().Grid<ProductViewModel>() .Name("grid") .Columns(columns => { columns.Bound(p => p.ProductName); columns.Bound(p => p.QuantityPerUnit); columns.Bound(p => p.UnitsInStock); columns.Bound(p => p.Discontinued); columns.Bound(p => p.UnitsOnOrder); columns.Bound(p => p.UnitPrice).Width(150); columns.Command(command => command.Destroy()).Width(150); }) .ToolBar(toolBar => { toolBar.Create(); toolBar.Save(); }) .Editable(editable => editable.Mode(GridEditMode.InCell)) .Pageable() .Sortable() .Scrollable() .HtmlAttributes(new { style = "height:550px;" }) .DataSource(dataSource => dataSource .Custom() .Batch(true) .Type("odata-v4") .Schema(schema => schema .Model(m => { m.Id(t => t.ProductID); m.Field(t => t.ProductID).Editable(false); m.Field(t => t.ProductName); m.Field(t => t.QuantityPerUnit); m.Field(t => t.UnitPrice); m.Field(t => t.UnitsInStock); m.Field(t => t.UnitsOnOrder); m.Field(t => t.Discontinued); })) .Transport(t => { t.Read(read => read.Url("/odata/Products")); t.Update(new { url = new Kendo.Mvc.ClientHandlerDescriptor() { HandlerName = "update" } }); t.Destroy(new { url = new Kendo.Mvc.ClientHandlerDescriptor() { HandlerName = "destroy" } }); t.Create(new { url = new Kendo.Mvc.ClientHandlerDescriptor() { HandlerName = "create" } }); t.Batch(new { url = new Kendo.Mvc.ClientHandlerDescriptor() { HandlerName = "batch" } }); }) .PageSize(10) .ServerPaging(true) ) ) <script> function batch(){ return "/odata/$batch" } function update(dataItem){ return "/odata/Products(" + dataItem.ProductID + ")" } function create(){ return "/odata/Products" } function destroy(dataItem){ return "/odata/Products(" + dataItem.ProductID + ")" } </script>
<script> // Initialize DataSource with the dataStore option. var dataSourceOptions = { batch: true, type: "odata-v4", transport: { read: { url: function () { return "/odata/Products"; } }, update: { url: function (dataItem) { return "/odata/Products(" + dataItem.ProductID + ")"; } }, batch: { url: function () { return "/odata/$batch"; } }, create: { url: function (dataItem) { delete dataItem.ProductID; return "/odata/Products"; } }, destroy: { url: function (dataItem) { return "/odata/Products(" + dataItem.ProductID + ")"; } } }, schema: { model: { id: "ProductID", fields: { ProductID: { editable: false }, ProductName: { type: "string" }, UnitPrice: { type: "number" }, UnitsInOrder: {type: "number"}, Discontinued: { type: "boolean" }, UnitsInStock: { type: "number" } } } }, pageSize: 10 }; var dataSource = new kendo.data.DataSource(dataSourceOptions); </script> <kendo-grid name="Grid" datasource-id="dataSource"> <columns> <column field="ProductName"/> <column field="UnitsInStock"/> <column field="Discontinued"/> <column field="UnitPrice" width="150"/> <column width="150"> <commands> <column-command text="Delete" name="destroy"></column-command> </commands> </column> </columns> <editable mode="incell"/> <toolbar> <toolbar-button name="create"></toolbar-button> <toolbar-button name="save"></toolbar-button> </toolbar> <pageable enabled="true"/> </kendo-grid>
Enabling Batch Editing in OData-v4 Binding Scenarios with ASP.NET Core 3.1 and 5:
-
Add a default batch handler within the
AddOData()
extensions method and inject theUseODataBatching()
middleware within theStartup.cs
file.public void ConfigureServices(IServiceCollection services) { var defaultBatchHandler = new DefaultODataBatchHandler(); defaultBatchHandler.MessageQuotas.MaxNestingDepth = 2; services.AddControllers().AddOData( options => options.Select().Filter().OrderBy().Expand().Count().SetMaxTop(null).AddRouteComponents( "odata", modelBuilder.GetEdmModel(), defaultBatchHandler)); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseODataBatching(); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); ... }
-
Within the Grid, set the Batch operation with
ClientHandlerDescriptor
to support batching and enable theBatch()
option of the Grid's DataSource.@(Html.Kendo().Grid<ProductViewModel>() .Name("grid") .Columns(columns => { columns.Bound(p => p.ProductName); columns.Bound(p => p.QuantityPerUnit); columns.Bound(p => p.UnitsInStock); columns.Bound(p => p.Discontinued); columns.Bound(p => p.UnitsOnOrder); columns.Bound(p => p.UnitPrice).Width(150); columns.Command(command => command.Destroy()).Width(150); }) .ToolBar(toolBar => { toolBar.Create(); toolBar.Save(); }) .Editable(editable => editable.Mode(GridEditMode.InCell)) .Pageable() .Sortable() .Scrollable() .HtmlAttributes(new { style = "height:550px;" }) .DataSource(dataSource => dataSource .Custom() .Batch(true) .Type("odata-v4") .Schema(schema => schema .Model(m => { m.Id(t => t.ProductID); m.Field(t => t.ProductID).Editable(false); m.Field(t => t.ProductName); m.Field(t => t.QuantityPerUnit); m.Field(t => t.UnitPrice); m.Field(t => t.UnitsInStock); m.Field(t => t.UnitsOnOrder); m.Field(t => t.Discontinued); })) .Transport(t => { t.Read(read => read.Url("/odata/Products")); t.Update(new { url = new Kendo.Mvc.ClientHandlerDescriptor() { HandlerName = "update" } }); t.Destroy(new { url = new Kendo.Mvc.ClientHandlerDescriptor() { HandlerName = "destroy" } }); t.Create(new { url = new Kendo.Mvc.ClientHandlerDescriptor() { HandlerName = "create" } }); t.Batch(new { url = new Kendo.Mvc.ClientHandlerDescriptor() { HandlerName = "batch" } }); }) .PageSize(10) .ServerPaging(true) ) ) <script> function batch(){ return "/odata/$batch" } function update(dataItem){ return "/odata/Products(" + dataItem.ProductID + ")" } function create(){ return "/odata/Products" } function destroy(dataItem){ return "/odata/Products(" + dataItem.ProductID + ")" } </script>
<script> // Initialize DataSource with the dataStore option. var dataSourceOptions = { batch: true, type: "odata-v4", transport: { read: { url: function () { return "/odata/Products"; } }, update: { url: function (dataItem) { return "/odata/Products(" + dataItem.ProductID + ")"; } }, batch: { url: function () { return "/odata/$batch"; } }, create: { url: function (dataItem) { delete dataItem.ProductID; return "/odata/Products"; } }, destroy: { url: function (dataItem) { return "/odata/Products(" + dataItem.ProductID + ")"; } } }, schema: { model: { id: "ProductID", fields: { ProductID: { editable: false }, ProductName: { type: "string" }, UnitPrice: { type: "number" }, UnitsInOrder: {type: "number"}, Discontinued: { type: "boolean" }, UnitsInStock: { type: "number" } } } }, pageSize: 10 }; var dataSource = new kendo.data.DataSource(dataSourceOptions); </script> <kendo-grid name="Grid" datasource-id="dataSource"> <columns> <column field="ProductName"/> <column field="UnitsInStock"/> <column field="Discontinued"/> <column field="UnitPrice" width="150"/> <column width="150"> <commands> <column-command text="Delete" name="destroy"></column-command> </commands> </column> </columns> <editable mode="incell"/> <toolbar> <toolbar-button name="create"></toolbar-button> <toolbar-button name="save"></toolbar-button> </toolbar> <pageable enabled="true"/> </kendo-grid>