Search code examples
c#kendo-gridodataasp.net-web-api

Web API 2.2 OData V4 - Kendo Grid - customize Created IHttpActionResult


I have a Kendo UI Grid wired up to odata CRUD services (Web API 2.2 OData V4). The dataSource configuration looks like the following - the baseUrl is the same for all, just the HTTP verb changes.

var dataSource = new kendo.data.DataSource({
    type: "odata",
    transport: {
        read: {
            beforeSend: prepareRequest,
            url: baseUrl,
            type: "GET",
            dataType: "json"
        },
        update: {
            beforeSend: prepareRequest,
            url: function (data) {
                return baseUrl + "(" + data.CategoryId + ")";
            },
            type: "PUT",
            dataType: "json"
        },
        create: {
            beforeSend: prepareRequest,
            url: baseUrl,
            type: "POST",
            dataType: "json"
        },
        destroy: {
            beforeSend: prepareRequest,
            url: function (data) {
                return baseUrl + "(" + data.CategoryId + ")";
            },
            type: "DELETE",
            dataType: "json"
        },
        parameterMap: function (data, operation) {
            if (operation == "read") {
                var paramMap = kendo.data.transports.odata.parameterMap(data);
                delete paramMap.$format;
                delete paramMap.$inlinecount;
                paramMap.$count = true;
                return paramMap;
            } else if (operation == "create" || operation == "update") {
                delete data["__metadata"];
                return JSON.stringify(data);
            }
        }
    },
    batch: false,
    pageSize: 10,
    serverPaging: true,
    serverSorting: true,
    serverFiltering: true,
    sort: { field: "CategoryCode", dir: "asc" },
    schema: {
        data: function (data) { return data.value; },
        total: function (data) { return data['@@odata.count']; },
        model: {
            id: "CategoryId",
            fields: {
                CategoryId: { editable: false, type: "number" },
                CategoryCode: { editable: true, type: "string", required: true, validation: { maxlength: 2 } },
                Description: { editable: true, type: "string", required: true, validation: { maxlength: 50 } },
                Created: { editable: false, type: "date" },
                CreatedBy: { editable: false, type: "string" },
                Updated: { editable: false, type: "date" },
                UpdatedBy: { editable: false, type: "string" }
            }
        }
    },
    error: function (e) {                
        commonNotification.hide();
        commonNotification.show(getRequestError(e), "error");
    },            
    change: function (e) {                
        commonNotification.hide();
        if (e.action == "sync") {                                
            commonNotification.show("@SharedResources.Changes_Saved", "success");
        }                
    },
    requestStart: function (e) {                
        if (e.type == "read" && this.hasChanges()) {
            if (confirm("@SharedResources.Dirty_Navigation_Confirmation") == false) {
                e.preventDefault();
            } else {
                this.cancelChanges();
            }
        }                
    }
});

Generally speaking, everything works great. The prepareRequest() function is used to apply a custom authorization header as well as setting the Accept header to "application/json;odata=verbose".

When reading, the JSON response looks something like the following - this is why the dataSource.schema.data function returns data.value

{
  "@odata.context":"https://localhost:44305/odata/$metadata#Categories",
  "@odata.count":2,
  "value":[
    {
      "CategoryId":1,
      "CategoryCode":"01",
      "Description":"Something",
      "Created":"2014-08-01T11:03:30.207Z",
      "CreatedBy":"DOMAIN\\User",
      "Updated":"2014-09-05T14:36:22.6323744-06:00",
      "UpdatedBy":"DOMAIN\\User"
    },{
      "CategoryId":2,
      "CategoryCode":"02",
      "Description":"Something Else",
      "Created":"2014-08-01T11:03:35.61Z",
      "CreatedBy":"DOMAIN\\User",
      "Updated":"2014-08-26T16:07:29.198241-06:00",
      "UpdatedBy":"DOMAIN\\User"
    }
  ]
}

However, when I create or update an entity, the JSON returned looks like this:

{
  "@odata.context":"https://localhost:44305/odata/$metadata#Categories/$entity",
  "CategoryId":3,
  "CategoryCode":"03",
  "Description":"Yet Another",
  "Created":"2014-09-06T07:55:52.4933275-06:00",
  "CreatedBy":"DOMAIN\\User",
  "Updated":"2014-09-06T13:55:34.054Z",
  "UpdatedBy":""
}

Because this is not wrapped by "value", the Kendo grid is not updating the data source correctly. The controller that does the POST or PUT, currently returns the entity as follows:

return Created(category);

OR

return Updated(category);

I was able to fix the issue by changing the response to a JsonResult as follows:

return Json(new { value = new[] { category } });

With that, everything works as desired...however, my HTTP response is now 200 (it seems that the JsonResult will always respond with 200). In a perfect world, I could return a 201 on create. Should I just accept that I have it working and live with the 200, or, is there a simple way to respond with a 201 and still format my JSON as necessary? It seems Web API 2 allowed a more customized http response, but my web api 2.2 controller actions are returning IHttpActionResult. I really don't want to create a custom class just to have a special return type and I can't seem to return my anonymous object with Created().

In summary, I'm really leaning toward just living with what I have. However, I would be interested in a way to return my anonymous object with a 201, or, a way to accept the non-"value wrapped" json in my kendo dataSource and have it update the data appropriately.


Solution

  • This is what I ended up doing in the end - I made my own "CreatedObject" method that would generate a ResponseMessageResult. This would wrap the object with the anonymous "value" object and serialize to json. Then, I could return this with the desired response code.

    public ResponseMessageResult CreatedObject(string location, object createdObject)
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        string json = serializer.Serialize(new { value = new[] { createdObject } });
    
        // Create the response and add the 201 response code
        HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Created);            
        response.Headers.Add("Location", location);
        response.Content = new System.Net.Http.StringContent(json);
    
        // return the result
        return ResponseMessage(response);
    }
    

    This solved the issue for the Kendo dataSource as the client. However, I didn't like the idea of manipulating the odata response for a particular client. So, instead, I modified the client to handle the normal Web API OData response as follows:

    schema: {
        data: function (data) {                    
            if (data.value) {
                return data.value;
            } else {
                delete data["@@odata.context"];
                return data;
            }
        },
        total: function (data) { return data['@@odata.count']; },
        model: { 
            etc...
    

    Now, the schema.data() function checks to see if the object(s) are wrapped in the "value" or not before returning the appropriate data. When returning the created object, I had to remove the @odata.context attribute as kendo didn't like it.