API Controllers

  • 7 minutes to read

This topic describes how to implement API controllers and bind DevExtreme-based ASP.NET Core controls to them.

Implement an API Controller

NOTE

The code below shows how to implement the OrdersController. This is a shortened version of the controller code the DevExtreme Scaffolder generates. CRUD operations are performed on the Northwind database’s Orders table via Entity Framework Core database context.

Pay attention to the Get method:

using DevExtreme.AspNet.Data;
using DevExtreme.AspNet.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Linq;
using System.Threading.Tasks;

namespace MyApp.Controllers {

    [Route("api/[controller]/[action]")]
    public class OrdersController : Controller {
        NorthwindContext _context;

        public OrdersController(NorthwindContext context) {
            _context = context;
        }

        const string VALIDATION_ERROR = "The request failed due to a validation error";

        // Load orders according to load options
        [HttpGet]
        public async Task<IActionResult> Get(DataSourceLoadOptions loadOptions) {
            var orders = _context.Orders.Select(i => new {
                i.OrderId,
                i.OrderDate,
                i.ShipCity
            });

            return Json(await DataSourceLoader.LoadAsync(orders, loadOptions));
        }

        // Insert a new order
        [HttpPost]
        public async Task<IActionResult> Post(string values) {
            var newOrder = new Orders();
            PopulateModel(newOrder, JsonConvert.DeserializeObject<IDictionary>(values));

            if(!TryValidateModel(newOrder))
                return BadRequest(VALIDATION_ERROR);

            var result = _context.Orders.Add(newOrder);
            await _context.SaveChangesAsync();
            return Json(new { result.Entity.OrderId });
        }

        // Update an order
        [HttpPut]
        public async Task<IActionResult> Put(int key, string values) {
            var order = await _context.Orders.FirstOrDefaultAsync(item => item.OrderId == key);
            PopulateModel(order, JsonConvert.DeserializeObject<IDictionary>(values));

            if(!TryValidateModel(order))
                return BadRequest(VALIDATION_ERROR);

            await _context.SaveChangesAsync();
            return Ok();
        }

        // Remove an order
        [HttpDelete]
        public async Task Delete(int key) {
            var order = await _context.Orders.FirstOrDefaultAsync(item => item.OrderId == key);
            _context.Orders.Remove(order);
            await _context.SaveChangesAsync();
        }

        void PopulateModel(Orders order, IDictionary values) {
            if(values.Contains("OrderId"))
                order.OrderId = Convert.ToInt32(values["OrderId"]);

            if(values.Contains("OrderDate"))
                order.OrderDate = values["OrderDate"] != null ? Convert.ToDateTime(values["OrderDate"]) : (DateTime?)null;

            if(values.Contains("ShipCity"))
                order.ShipCity = Convert.ToString(values["ShipCity"]);
        }
    }
}
IMPORTANT

Starting from version 2.1, ASP.NET Core provides the ApiController attribute. If you add this attribute to your controller, specify the [FromForm] binding source attribute in the Insert, Update and Delete actions. See the example code below.

public IActionResult Insert([FromForm]string values) {
    // ...
}

public IActionResult Update([FromForm]int key, [FromForm]string values) {
    // ...
}

public void Delete([FromForm]int key) {
    // ...
}

Bind to a Controller

Use the Mvc() method the DataSource()‘s lambda parameter exposes to configure access to API controllers. The following example shows the DataGrid control that accesses the OrdersController whose actions (Get, Insert, Update, and Delete) implement CRUD operations:

@(Html.DevExtreme().DataGrid()
    .DataSource(ds => ds.Mvc()
        .Controller("Orders")
        .Key("OrderId")
        .LoadAction("Get")
        .InsertAction("Insert")
        .UpdateAction("Update")
        .DeleteAction("Delete")
    )
)

Bind to a Controller on a Different Site

Previous examples show use-cases in which a control is on the same site as the data it accesses, and routing finds the right controller and actions. If the data is on a different site, use the RemoteController() method the DataSource()‘s lambda parameter exposes. This method opens a chain whose members mirror the client-side API of the DevExtreme.AspNet.Data library. In the LoadUrl, InsertUrl, UpdateUrl, and DeleteUrl methods, specify the URLs that handle corresponding CRUD operations.

@(Html.DevExtreme().DataGrid()
    .DataSource(ds => ds.RemoteController()
        .Key("OrderId")
        .LoadUrl("http://www.example.com/Orders/Get")
        .InsertUrl("http://www.example.com/Orders/Insert")
        .UpdateUrl("http://www.example.com/Orders/Update")
        .DeleteUrl("http://www.example.com/Orders/Delete")
    )
)
NOTE

A remote controller should be implemented in the way described above.

Bind to a Controller in a Different Area

In an app organized into areas, a control from a view that belongs to one area may need to access a data controller that belongs to another. Use the Area() method to specify the data controller’s area in this case.

@(Html.DevExtreme().DataGrid()
    .DataSource(ds => ds.Mvc()
        .Area("DifferentArea")
        // ...
    )
)

Load Mode

You can use two modes to load data:

  • Processed (default) - all data shaping operations are performed on the server.
  • Raw - all data shaping operations are performed on the client.

    Use the raw mode when a control operates with a small amount of data. Once loaded, data is stored and processed on the client. This should reduce network round-trips, and therefore network latency.

Use the DataSource.LoadMode method to specify the mode:

@(Html.DevExtreme().DataGrid()
    .DataSource(ds => ds.Mvc()
        .Controller("Orders")
        .Key("OrderId")
        .LoadAction("Get")
        .LoadMode(DataSourceLoadMode.Raw) // specifies the raw mode
        // ...
    )
)

Pass Additional Load Parameters

DevExtreme-based ASP.NET Core controls internally transform your Razor code into HTML and JavaScript. However, you cannot use Razor to pass load parameters. Instead, you need to access JavaScript variables directly.

The following code example implements the master-detail interface in the DataGrid control. This interface is a grid inside a grid. The inner grid filters its data source according to the master row key. The API reference states that the master row key and data are available in the master-detail template through the key and data JavaScript variables. Only the key‘s value is needed for the master-detail, and it is accessed using the new JS() expression.

@(Html.DevExtreme().DataGrid()
    .DataSource(d => d.Mvc().Controller("Employees").Key("ID"))
    .Columns(columns => {
        columns.Add().DataField("FirstName");
        columns.Add().DataField("LastName");
        // ...
    })
    .MasterDetail(md => md
        .Enabled(true)
        .Template(@<text>
            @(Html.DevExtreme().DataGrid()
                .DataSource(d => d.Mvc()
                    .Controller("Employees")
                    .LoadAction("TasksDetails")
                    .LoadParams(new { id = new JS("key") })
                )
            )
        </text>)
    )
)

If the control is not in a template and no variables are provided, you can use a JavaScript function accessed with the same new JS() expression to get data for a load parameter. In the following code, the DataGrid control uses the getDateBoxValue() function to get a value for the orderDate load parameter from the DateBox control. The refreshGrid() function is called each time the DateBox value changes. This function calls the DataGrid‘s refresh method that makes a new load query with the updated load parameter.

@(Html.DevExtreme().DateBox()
    .ID("orderDate")
    .Type(DateBoxType.Date)
    .Value(DateTime.Now)
    .OnValueChanged("refreshGrid")
)

@(Html.DevExtreme().DataGrid()
    .ID("targetDataGrid")
    .DataSource(ds => ds.Mvc()
        .Controller("Orders")
        .LoadAction("GetOrdersByDate")
        .LoadParams(new { orderDate = new JS("getDateBoxValue") })
    )
    .Columns(cols => {
        cols.Add().DataField("OrderId");
        cols.Add().DataField("OrderDate");
        // ...
    })
)

<script type="text/javascript">
    function getDateBoxValue() {
        return $("#orderDate").dxDateBox("option", "text");
    }

    function refreshGrid() {
        $("#targetDataGrid").dxDataGrid("refresh");
    }
</script>

To pass additional parameters to other requests (insert, update, etc.), use the OnBeforeSend() method. See Customize Requests for more information.

Customize Requests

Use the DataSource.OnBeforeSend() method to customize HTTP requests. This method takes a JavaScript function that makes changes to a request before it is sent. This function is transformed internally into the DevExtreme.AspNet.Data library‘s client-side onBeforeSend function.

@(Html.DevExtreme().DataGrid()
    .DataSource(ds => ds.Mvc()
        .Controller("Orders")
        .LoadAction("GetOrdersByDate")
        //...
        .OnBeforeSend("dataGrid_beforeSend")
    )
)

<script>
    function dataGrid_beforeSend(operation, ajaxOptions) {
        //...
    }
</script>

You can customize requests in the following ways:

  • Specify HTTP headers.

    For example, if you use request verification to prevent XSRF/CSRF attacks, you should pass an anti-forgery token as part of the request header. The code below shows how to implement the beforeSend function that adds an anti-forgery token to a request header. Then this function can be used in the DataSource.OnBeforeSend method.

    @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
    
    <script>
        function beforeSend(operation, ajaxSettings) {
            ajaxSettings.headers = {
                RequestVerificationToken: "@Xsrf.GetAndStoreTokens(Model.HttpContext).RequestToken"
            };
        }
    </script>
    
  • Pass additional parameters to requests.

    The following code shows the beforeSend function that adds a new parameter to all requests (excluding the load operation):

    function beforeSend(operation, ajaxSettings) {  
        if (operation !== "load") {  
            ajaxSettings.data.extraParam = "extraValue";  
        }  
    }
    
  • Modify a URL.

    function beforeSend(operation, ajaxSettings) {  
        if (operation !== "load") {  
            ajaxSettings.url = ajaxSettings.url.replace(...);  
        }  
    }