Short answer: To bind a Kendo Grid in ASP.NET Core, use the
Telerik UI for ASP.NET Core
Html.Kendo().Grid<T>() helper with an
Ajax Read action. The action accepts a
DataSourceRequest and returns a
DataSourceResult, which holds the current
page of rows and the total row count.
This tutorial covers Kendo Grid data binding in ASP.NET Core in one complete project. You'll create the app, set up a database with sample rows, and bind the grid four ways: local binding, Ajax binding with server-side paging, sorting and filtering, Ajax binding with client-side operations, and custom (OData) binding. Then you'll add create, update and delete, and finish with performance, security and troubleshooting advice. Each step ends with what you should see, so you can tell right away if something is off.
Choose the matching setup: the
Html.Kendo() examples use Telerik UI for
ASP.NET Core, a commercial package (a free trial is available), and not only
the Kendo UI for jQuery scripts. Both trial and commercial installs need a
Telerik license key file, otherwise you'll see build warnings and run-time
watermarks. Follow
Telerik's NuGet setup, the
license key guide
and the
Ajax binding guide
for the exact package, scripts and styles that match your release.
1. What the Kendo Grid does in ASP.NET Core
The Kendo Grid shows rows of data in a table and supports paging, sorting, filtering, grouping and editing. In ASP.NET Core you declare it in a Razor view with a C# helper. The helper renders the HTML and the JavaScript that powers the grid in the browser.
Binding means telling the grid where its rows come from. Pick the mode that fits your data size:
- Local binding: the rows are rendered with the page. Best for small, fixed lists.
- Ajax binding with server operations: the grid asks your controller for one page at a time. Best for large tables.
- Ajax binding with client operations: the grid loads all rows once, then pages and sorts in the browser. Best for a few hundred rows.
- Custom binding: the grid reads from a service with its own protocol, such as OData.
2. Prerequisites
- The .NET SDK (check Telerik's system requirements for the .NET versions your release supports)
- SQL Server or SQL Server LocalDB, or another database if you swap the EF Core provider
- A Telerik UI for ASP.NET Core license or free trial, plus the license key file from your Telerik account
- A project with nullable reference types and implicit usings enabled, which is the default in current MVC templates
- Basic C#, MVC and Razor knowledge
3. Create the project and install packages
-
Create an MVC project.
dotnet new mvc -n CodingvilaGridDemo cd CodingvilaGridDemo -
Install Entity Framework Core. The last command installs the EF Core
command-line tool, or updates it if you already have it.
dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design dotnet tool update --global dotnet-ef -
Install the Telerik wrapper.
Telerik.UI.for.AspNet.Coreis served from Telerik's NuGet feed, so add that feed first as described in the NuGet setup guide. Then install the package:
dotnet add package Telerik.UI.for.AspNet.Core -
Activate your license. The package depends on
Telerik.Licensing, which checks your license key during build. Downloadtelerik-license.txtfrom your Telerik account and save it where Telerik's license guide says, or in your project root.
Expected result: dotnet build succeeds
without license errors. If the Telerik package can't be found, the feed or
credentials aren't configured yet. If the build reports a license problem, the
key file is missing or belongs to a different product.
By the end, your project will contain these files:
CodingvilaGridDemo/
Controllers/HomeController.cs
Data/AppDbContext.cs
Models/CodingvilaModel.cs
Models/CodingvilaViewModel.cs
Views/Home/Index.cshtml
Views/Home/Local.cshtml
Views/Home/ClientOps.cshtml
Views/Home/Custom.cshtml
Views/Home/Editable.cshtml
Views/Shared/_Layout.cshtml
Views/_ViewImports.cshtml
appsettings.json
Program.cs
4. Create the models, database context and sample data
You'll use two classes. The entity maps to the database table. The view model is what the grid sees. Keeping them separate means you never send columns to the browser that the grid doesn't need.
-
Entity (
Models/CodingvilaModel.cs):
using System.ComponentModel.DataAnnotations; namespace CodingvilaGridDemo.Models; public class CodingvilaModel { public int Id { get; set; } [Required, MaxLength(100)] public string Name { get; set; } = ""; [MaxLength(250)] public string Description { get; set; } = ""; } -
View model (
Models/CodingvilaViewModel.cs). The validation attributes also drive the grid's editing validation in section 9.Descriptionis declaredstring?on purpose. With nullable reference types enabled, MVC treats a non-nullablestringas required, so an empty description would fail validation when you save a row.
using System.ComponentModel.DataAnnotations; namespace CodingvilaGridDemo.Models; public class CodingvilaViewModel { public int Id { get; set; } [Required, StringLength(100)] public string Name { get; set; } = ""; [StringLength(250)] public string? Description { get; set; } } -
Database context with 25 seed rows (
Data/AppDbContext.cs). Seeding gives you enough rows to see paging work.
using CodingvilaGridDemo.Models; using Microsoft.EntityFrameworkCore; namespace CodingvilaGridDemo.Data; public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<CodingvilaModel> CodingvilaModels => Set<CodingvilaModel>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<CodingvilaModel>().HasData( Enumerable.Range(1, 25).Select(i => new CodingvilaModel { Id = i, Name = $"Developer {i}", Description = $"Sample role {i}" })); } } -
Connection string (
appsettings.json). This example uses SQL Server LocalDB on Windows. Replace it with your own server if needed, and keep your existingLoggingandAllowedHostsentries. Don't commit real production credentials.
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=CodingvilaGridDemo;Trusted_Connection=True;TrustServerCertificate=True" } } -
Create the database.
dotnet ef migrations add InitialCreate dotnet ef database update
Expected result: a
CodingvilaGridDemo database with one table of
25 rows.
5. Configure Kendo in the app
-
Program.cs. Two settings matter here.
AddKendo()registers the Telerik services.PropertyNamingPolicy = nullkeeps JSON property names in PascalCase, because ASP.NET Core returns camelCase by default and the data-bound Telerik components expect PascalCase. Without it the grid shows empty or wrong cells.
using CodingvilaGridDemo.Data; using Kendo.Mvc.Extensions; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews() .AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = null); builder.Services.AddKendo(); builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run(); -
_ViewImports.cshtml. Add the Kendo imports below the existing
lines:
@using CodingvilaGridDemo @using CodingvilaGridDemo.Models @using Kendo.Mvc.UI @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Kendo.Mvc -
_Layout.cshtml. Add the styles and scripts inside
<head>, and keep@RenderBody()in the body. The pattern looks like this, where{VERSION}and{THEME}are your Telerik release and theme file. File names and paths differ between releases, so copy the real values from the installation guide. Load order matters: jQuery first, then Kendo, then the ASP.NET Core integration script.
<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - Codingvila Grid Demo</title> <link href="https://kendo.cdn.telerik.com/{VERSION}/styles/{THEME}.css" rel="stylesheet" /> <script src="https://kendo.cdn.telerik.com/{VERSION}/js/jquery.min.js"></script> <script src="https://kendo.cdn.telerik.com/{VERSION}/js/kendo.all.min.js"></script> <script src="https://kendo.cdn.telerik.com/{VERSION}/js/kendo.aspnetmvc.min.js"></script> </head>
Expected result: the app still builds and runs. You won't see a grid yet.
6. How the grid and your controller talk to each other
Two Telerik types do most of the work.
DataSourceRequest describes what the grid
wants: the page number, page size, sort order and filters.
DataSourceResult is what you send back: the
rows for that page and the total count, which the pager needs. The
ToDataSourceResultAsync extension method
connects the two by applying the request to your
IQueryable. Read the diagram below from top
to bottom.
7. Kendo Grid data binding techniques
All four techniques share one controller. Create or replace
Controllers/HomeController.cs with the code
below. The create, update and delete actions come later in section 9.
using CodingvilaGridDemo.Data;
using CodingvilaGridDemo.Models;
using Kendo.Mvc.Extensions;
using Kendo.Mvc.UI;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace CodingvilaGridDemo.Controllers;
public class HomeController : Controller
{
private readonly AppDbContext _context;
public HomeController(AppDbContext context) => _context = context;
// 7.2 Ajax binding with server operations
public IActionResult Index() => View();
// 7.1 Local binding
public async Task<IActionResult> Local()
{
var rows = await BaseQuery().ToListAsync();
return View(rows);
}
// 7.4 Client operations
public IActionResult ClientOps() => View();
// 7.5 Custom (OData) binding
public IActionResult Custom() => View();
// 9. Editing
public IActionResult Editable() => View();
// Shared Read action used by Index, ClientOps and Editable
[HttpPost]
public async Task<IActionResult> Read([DataSourceRequest] DataSourceRequest request)
{
var result = await BaseQuery().ToDataSourceResultAsync(request);
return Json(result);
}
// Projects the entity to the view model so only needed columns are queried
private IQueryable<CodingvilaViewModel> BaseQuery() =>
_context.CodingvilaModels
.AsNoTracking()
.Select(m => new CodingvilaViewModel
{
Id = m.Id,
Name = m.Name,
Description = m.Description
});
}
7.1 Local data binding
Local binding passes the rows to the view as the model. Use it for small lists
that don't change while the page is open. There is no
Read request. Create
Views/Home/Local.cshtml:
@model IEnumerable<CodingvilaViewModel>
@{
ViewData["Title"] = "Local binding";
}
<h2>Local binding</h2>
@(Html.Kendo().Grid(Model)
.Name("LocalGrid")
.Columns(columns =>
{
columns.Bound(c => c.Id).Title("ID");
columns.Bound(c => c.Name).Title("Name");
columns.Bound(c => c.Description).Title("Description");
})
)
Expected result at /Home/Local: a
table with all 25 rows. For paging, sorting and filtering in the browser, use
section 7.4.
7.2 Ajax binding with server-side paging, sorting and filtering
This is the setup most real projects need. The grid sends a request every time
the user changes the page, sort order or filter, and the
Read action from the controller answers it.
The Ajax Read uses POST by default, which is
why the action is marked [HttpPost]. Create
Views/Home/Index.cshtml:
@{
ViewData["Title"] = "Ajax binding";
}
<h2>Ajax binding with server operations</h2>
@(Html.Kendo().Grid<CodingvilaViewModel>()
.Name("CodingvilaGrid")
.Columns(columns =>
{
columns.Bound(c => c.Id).Title("ID");
columns.Bound(c => c.Name).Title("Name");
columns.Bound(c => c.Description).Title("Description");
})
.Pageable()
.Sortable()
.Filterable()
.DataSource(dataSource => dataSource
.Ajax()
.PageSize(10)
.Sort(sort => sort.Add(m => m.Id).Ascending())
.Read(read => read.Action("Read", "Home"))
)
.HtmlAttributes(new { style = "height: 400px;" })
)
Expected result at /: 10 rows per
page, a pager showing 25 items in total, and sortable and filterable columns.
Every page click, sort or filter sends a new request, which you can confirm in
the browser's network tab. The Sort call
gives the first load a stable default order, which paging needs.
7.3 What the Read action actually returns
Because EF Core translates the query, the database does the paging, sorting and filtering, and only one page of rows travels over the network. The result serializes to JSON like this (shortened):
{
"Data": [ { "Id": 1, "Name": "Developer 1", "Description": "Sample role 1" } ],
"Total": 25,
"AggregateResults": null,
"Errors": null
}
The grid uses Total to draw the pager. Don't
return a plain list from an action bound this way, or paging breaks.
7.4 Client-side operations (paging and sorting in the browser)
If the table is small, load all rows once and let the browser handle paging,
sorting and filtering. Set
ServerOperation(false) on the data source.
The same Read action works, because the grid
doesn't ask for a page, so the action returns every row. Create
Views/Home/ClientOps.cshtml:
@{
ViewData["Title"] = "Client operations";
}
<h2>Ajax binding with client operations</h2>
@(Html.Kendo().Grid<CodingvilaViewModel>()
.Name("ClientOpsGrid")
.Columns(columns =>
{
columns.Bound(c => c.Id).Title("ID");
columns.Bound(c => c.Name).Title("Name");
columns.Bound(c => c.Description).Title("Description");
})
.Pageable()
.Sortable()
.Filterable()
.DataSource(dataSource => dataSource
.Ajax()
.PageSize(10)
.ServerOperation(false)
.Read(read => read.Action("Read", "Home"))
)
.HtmlAttributes(new { style = "height: 400px;" })
)
Expected result at /Home/ClientOps:
the grid looks the same as in 7.2, but after the first load, paging, sorting
and filtering send no new requests. Check the network tab to confirm.
Limit: every row is downloaded and held in browser memory, so this suits a few hundred rows, not tens of thousands. For larger tables, use server operations from section 7.2.
7.5 Custom data binding (OData v4)
Use custom binding when the data comes from a service that doesn't follow the
DataSourceRequest pattern, such as an OData
v4 endpoint. You describe the service type and URL, and the grid builds its
requests in that service's format. This example reads from Telerik's public
OData v4 demo service. It's meant for demos and can change, so point it at
your own service in a real project. Create
Views/Home/Custom.cshtml:
@{
ViewData["Title"] = "Custom binding";
}
<h2>Custom binding (OData v4)</h2>
@(Html.Kendo().Grid<dynamic>()
.Name("OrdersGrid")
.Columns(columns =>
{
columns.Bound("OrderID").Title("Order ID");
columns.Bound("ShipName").Title("Ship name");
columns.Bound("ShipCity").Title("Ship city");
})
.Pageable()
.Sortable()
.DataSource(dataSource => dataSource
.Custom()
.Type("odata-v4")
.PageSize(10)
.ServerPaging(true)
.ServerSorting(true)
.Transport(transport => transport
.Read(read => read.Url("https://demos.telerik.com/service/v2/odata/Orders"))
)
)
.HtmlAttributes(new { style = "height: 400px;" })
)
Expected result at /Home/Custom: a
paged, sortable grid of sample orders. No controller action is involved,
because the browser talks to the OData service directly. That also means the
service must allow cross-origin requests (CORS) from your site. If the grid
stays empty, open the browser console. A CORS error means the service rejects
your origin, so switch to your own endpoint. Telerik's
OData-v4 binding guide
shows how to build one with ASP.NET Core.
8. Which binding mode should you choose?
Answer the two questions in the flowchart below, starting at the top.
| Mode | Where operations run | Best for |
|---|---|---|
Local (Grid(Model))
|
Rows rendered with the page | Small, fixed lists |
| Ajax, server operations | Database, through your action | Large tables |
| Ajax, client operations | Browser | A few hundred rows |
| Custom (OData) | The external service | Services with their own protocol |
9. Add create, update and delete
Once the grid reads data correctly, you can let users edit it. Each operation
is a separate POST action. Each one returns the affected row through
ToDataSourceResultAsync(request, ModelState),
so validation errors reach the grid and appear next to the fields.
-
Add the actions to
HomeController:
[HttpPost] public async Task<IActionResult> Create([DataSourceRequest] DataSourceRequest request, CodingvilaViewModel model) { if (ModelState.IsValid) { var entity = new CodingvilaModel { Name = model.Name, Description = model.Description ?? "" }; _context.CodingvilaModels.Add(entity); await _context.SaveChangesAsync(); model.Id = entity.Id; } var result = await new[] { model }.ToDataSourceResultAsync(request, ModelState); return Json(result); } [HttpPost] public async Task<IActionResult> Update([DataSourceRequest] DataSourceRequest request, CodingvilaViewModel model) { if (ModelState.IsValid) { var entity = await _context.CodingvilaModels.FindAsync(model.Id); if (entity == null) { ModelState.AddModelError("", "This row no longer exists."); } else { entity.Name = model.Name; entity.Description = model.Description ?? ""; await _context.SaveChangesAsync(); } } var result = await new[] { model }.ToDataSourceResultAsync(request, ModelState); return Json(result); } [HttpPost] public async Task<IActionResult> Destroy([DataSourceRequest] DataSourceRequest request, CodingvilaViewModel model) { var entity = await _context.CodingvilaModels.FindAsync(model.Id); if (entity != null) { _context.CodingvilaModels.Remove(entity); await _context.SaveChangesAsync(); } var result = await new[] { model }.ToDataSourceResultAsync(request, ModelState); return Json(result); } -
Create the view
Views/Home/Editable.cshtml. TheModelcall tells the grid which field is the row key, and the ID is read-only because the database generates it.
@{ ViewData["Title"] = "Editable grid"; } <h2>Editable grid</h2> @(Html.Kendo().Grid<CodingvilaViewModel>() .Name("EditableGrid") .Columns(columns => { columns.Bound(c => c.Id).Title("ID").Width(80); columns.Bound(c => c.Name).Title("Name"); columns.Bound(c => c.Description).Title("Description"); columns.Command(command => { command.Edit(); command.Destroy(); }).Width(220); }) .ToolBar(toolbar => toolbar.Create()) .Editable(editable => editable.Mode(GridEditMode.InLine)) .Pageable() .Sortable() .DataSource(dataSource => dataSource .Ajax() .PageSize(10) .Sort(sort => sort.Add(m => m.Id).Ascending()) .Model(model => { model.Id(m => m.Id); model.Field(m => m.Id).Editable(false); }) .Read(read => read.Action("Read", "Home")) .Create(create => create.Action("Create", "Home")) .Update(update => update.Action("Update", "Home")) .Destroy(destroy => destroy.Action("Destroy", "Home")) ) .HtmlAttributes(new { style = "height: 460px;" }) )
Expected result at /Home/Editable: an
"Add new record" button above the grid, and Edit and Delete buttons on each
row. Add a row with an empty name and the grid shows a validation message
instead of saving it. Add a valid row, with or without a description, and it
appears with a database-generated ID. Refresh the page to confirm it
persisted.
Beyond editing, a few one-line additions are worth trying.
.Groupable() lets users drag a column header
into a group bar, and .Scrollable() keeps a
fixed-height grid scrollable. With server operations,
ToDataSourceResultAsync processes grouping as
well.
10. Using the tag helper instead of the HTML helper
Telerik also ships tag helpers, which look like HTML elements. They configure
the same component. Use whichever style your team prefers, and don't mix both
styles for one grid. This tag helper version binds to the same
Read action (the
addTagHelper line in
_ViewImports.cshtml from section 5 enables
it):
<kendo-grid name="TagHelperGrid" height="400">
<datasource type="DataSourceTagHelperType.Ajax" page-size="10" server-operation="true">
<schema data="Data" total="Total">
<model id="Id"></model>
</schema>
<transport>
<read url="@Url.Action("Read", "Home")" type="POST"/>
</transport>
</datasource>
<columns>
<column field="Id" title="ID"></column>
<column field="Name" title="Name"></column>
<column field="Description" title="Description"></column>
</columns>
<pageable enabled="true"/>
<sortable enabled="true"/>
<filterable enabled="true"/>
</kendo-grid>
The schema element tells the grid to read
rows from Data and the count from
Total. This snippet covers reading only.
Attribute names can vary slightly between Telerik releases, so check them
against the documentation for your version. For Razor Pages, the approach is
the same, with handler methods such as
OnPostRead that return
ToDataSourceResult(request) in a
JsonResult.
11. Performance and security best practices
-
Keep the work in the database. Pass an
IQueryabletoToDataSourceResultAsync. If you callToList()first, every row loads into memory and paging happens in your app. -
Project to a view model. The
SelectinBaseQueryfetches only the columns the grid shows. -
Use
AsNoTracking()on read-only queries, since the grid's rows don't need change tracking. -
Index sorted and filtered columns. If users sort by
Nameoften, an index on that column helps. Check real query plans on your data. - Set a sensible page size. Ten to fifty rows per page is typical. Avoid loading thousands of rows into one page.
-
Protect your actions. Add
[Authorize]to private data, and check that the signed-in user may read or change each row. -
Validate on the server. The grid's client-side validation is a
convenience. The
ModelState.IsValidchecks are what protect your data. - Handle antiforgery tokens. If your app validates antiforgery tokens on POST requests, send the token with every grid read and write request. Telerik documents how to do this for your release.
- Keep your license key out of source control. In CI/CD, supply it through Telerik's documented environment variables instead of committing it.
- Use HTTPS for all pages and for any custom binding endpoint.
12. Run and test the project
-
Run
dotnet runand open the HTTPS URL it prints. -
Open
/and confirm 10 rows and a pager that shows 25 items. - Click the Name header twice and confirm the order flips between ascending and descending.
- Use the filter on the Name column and confirm the total updates.
-
Open the browser's network tab and confirm each page, sort or filter sends a
POST to
/Home/Read. -
Open
/Home/ClientOpsand confirm paging sends no new requests after the first load. -
Open
/Home/Editable, add, edit and delete a row, and confirm the changes survive a page refresh.
13. Common errors and how to fix them
-
The grid renders but cells are empty or show
undefined: the JSON is camelCase. SetPropertyNamingPolicy = nullas shown in section 5. -
The request fails with 404, 405 or 400: the action's HTTP verb
doesn't match the grid's read request. The default is POST, so use
[HttpPost], or change the grid's read request type. Also check for antiforgery validation. -
The pager shows no total or paging doesn't work: the action returned
a plain list. Return
ToDataSourceResultAsync(request)instead. -
Kendo is not definedor the grid doesn't appear: a script is missing or loaded in the wrong order. Load jQuery, thenkendo.all.min.js, thenkendo.aspnetmvc.min.js. -
The helper isn't recognized in the view: check the Kendo imports in
_ViewImports.cshtmland confirm the NuGet package restored. -
Build errors or warnings about a Telerik license, or a watermark or
banner on the page:
the license key file is missing, expired or belongs to a different product.
Download a fresh
telerik-license.txtfrom your Telerik account and rebuild. -
Saving a row fails with "The Description field is required":
Descriptionis a non-nullablestringin the view model. Declare it asstring?as shown in section 4. -
Edits don't save, or new rows lose their ID: make sure the data
source declares
Modelwith anId, and thatCreatereturns the model with the database-generated ID. -
Database errors on first run: confirm the connection string and that
dotnet ef database updatecompleted.
14. Frequently asked questions
What is the difference between server and client operations? With server operations, each page, sort or filter is a new request and the database does the work. With client operations, all rows load once and the browser does the work.
Can I use the grid without the Telerik NuGet package? The
Html.Kendo() helpers need the Telerik UI for
ASP.NET Core package. Without it, you'd use the Kendo UI for jQuery grid and
write the JavaScript yourself.
Do I need a license key to try it? Yes. Telerik UI for ASP.NET Core needs a license key file for both trial and commercial licenses. Without it you'll see build warnings and run-time watermarks.
Why is my grid empty even though the request succeeds? Almost always JSON casing. See the first item in the common errors section.
Do I need DataSourceRequest for every
action?
You need it on any action that serves an Ajax-bound grid with server
operations, because it carries the paging, sorting and filter state.
Should I return my entity classes directly? Avoid it. Use a view model, as in this tutorial, so you control exactly which fields reach the browser.
15. Conclusion and key takeaways
You now have a working project that binds a Kendo Grid four ways and supports full editing. The binding choice comes down to where you want paging, sorting and filtering to happen. Use server operations for large tables, and client operations only when the data is small enough to hold in the browser.
-
Install the Telerik wrapper, activate your license key, call
AddKendo(), and turn off camelCase JSON before anything else. -
For large tables, use Ajax binding and return a
DataSourceResultfrom an action that acceptsDataSourceRequest. - Match the HTTP verb between the grid and the action.
-
Project to a view model and keep queries as
IQueryableso the database does the work. - Validate on the server and protect every action that reads or changes private data.
