OData-v4 Binding
OData (Open Data Protocol) defines best practices for building and consuming RESTful APIs. It provides built-in support for filtering, selecting, and expanding, among other query capabilities. By binding the ASP.NET Core Grid to OData-v4, you can extend your REST API with advanced query options.
For a runnable example, refer to the Grid OData-v4 binding demo.
Installing the OData Package
Install the required OData dependency by adding the Microsoft.AspNetCore.OData NuGet package to your project.
Building the EDM Model and Configuring the Service in ASP.NET Core 6
For applications that use .NET 6 and the minimal hosting model, configure the EDM model and OData service in Program.cs:
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 that use .NET Core 3.1 or ASP.NET Core 5, configure the EDM model and OData service in ConfigureServices:
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 read and write operations in OData format, inherit from the ODataController base class.
public class ProductsController : ODataController
{
...
}
Then, decorate the REST API endpoints with the EnableQuery attribute so the query options from the URL are applied automatically.
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 bind the Telerik UI for ASP.NET Core Grid to OData-v4, set .Type("odata-v4") in the DataSource configuration. This setting sends requests to the OData endpoint in the expected format.
@(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)
)
)Configuring the CRUD Operations
To configure CRUD operations for OData-v4 binding, add ClientHandlerDescriptor handlers that map each request to the corresponding OData-v4 endpoint.
@(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>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.C#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(); -
In the Grid, configure the batch operation with
ClientHandlerDescriptor, and enable the DataSourceBatch()option.Razor@(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>
Enabling Batch Editing in OData-v4 Binding Scenarios with ASP.NET Core 3.1 and 5
- Add a default batch handler in the
AddOData()extension method, and inject theUseODataBatching()middleware inStartup.cs.
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();
...
}
- In the Grid, configure the batch operation with
ClientHandlerDescriptor, and enable the DataSourceBatch()option.
@(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>