This is a migrated thread and some comments may be shown as answers.

Web API + OData - PATCH request 400 error

4 Answers 530 Views
Data Source
This is a migrated thread and some comments may be shown as answers.
Josh
Top achievements
Rank 2
Josh asked on 03 Mar 2014, 10:45 PM
I have a KendoUI DataSource linked up to a WebApi 2 OData controller and am having problems with update operations. I can create and delete just fine.

When I make the call to sync the datasource to the server after making any updates I get a 400 error:

{
  "odata.error":{
    "code":"","message":{
      "lang":"en-US","value":"The request is invalid."
    },"innererror":{
      "message":"patch : Invalid JSON. A token was not recognized in the JSON content.\r\n","type":"","stacktrace":""
    }
  }
}


Debugging in Visual Studio shows that the patch function is being passed the Id but not the Company object. Firebug shows that the PATCH request looks like this when I change the title of "Test Company" to "Test Company test" and click save:

models=%7B%22Id%22%3A1026%2C%22Title%22%3A%22Test+Company+test%22%7D

I have a hunch there is something wonky about this that the server doesn't understand.

The model is simple and I left the controller as whatever VS generated for me:

Model:

01.public class Company {
02.    public Company() { }
03. 
04.    public Company(Company company) {
05.        this.Id = company.Id;
06.        this.Title = company.Title;
07.        this.Projects = company.Projects;
08.    }
09. 
10.    public int Id { get; set; }
11.    public string Title { get; set; }
12. 
13.    public virtual ICollection<Project> Projects { get; set; }
14.}


Controller:

001.public class CompanyController : ODataController
002.{
003.    private ApplicationDbContext db = new ApplicationDbContext();
004. 
005.    // GET odata/Company
006.    [Queryable]
007.    public IQueryable<Company> GetCompany()
008.    {
009.        return db.Companies;
010.    }
011. 
012.    // GET odata/Company(5)
013.    [Queryable]
014.    public SingleResult<Company> GetCompany([FromODataUri] int key)
015.    {
016.        return SingleResult.Create(db.Companies.Where(company => company.Id == key));
017.    }
018. 
019.    // PUT odata/Company(5)
020.    public async Task<IHttpActionResult> Put([FromODataUri] int key, Company company)
021.    {
022.        if (!ModelState.IsValid)
023.        {
024.            return BadRequest(ModelState);
025.        }
026. 
027.        if (key != company.Id)
028.        {
029.            return BadRequest();
030.        }
031. 
032.        db.Entry(company).State = EntityState.Modified;
033. 
034.        try
035.        {
036.            await db.SaveChangesAsync();
037.        }
038.        catch (DbUpdateConcurrencyException)
039.        {
040.            if (!CompanyExists(key))
041.            {
042.                return NotFound();
043.            }
044.            else
045.            {
046.                throw;
047.            }
048.        }
049. 
050.        return Updated(company);
051.    }
052. 
053.    // POST odata/Company
054.    public async Task<IHttpActionResult> Post(Company company)
055.    {
056.        if (!ModelState.IsValid)
057.        {
058.            return BadRequest(ModelState);
059.        }
060. 
061.        db.Companies.Add(company);
062.        await db.SaveChangesAsync();
063. 
064.        return Created(company);
065.    }
066. 
067.    // PATCH odata/Company(5)
068.    [AcceptVerbs("PATCH", "MERGE")]
069.    public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Company> patch)
070.    {
071.        if (!ModelState.IsValid)
072.        {
073.            return BadRequest(ModelState);
074.        }
075. 
076.        Company company = await db.Companies.FindAsync(key);
077.        if (company == null)
078.        {
079.            return NotFound();
080.        }
081. 
082.        patch.Patch(company);
083. 
084.        try
085.        {
086.            await db.SaveChangesAsync();
087.        }
088.        catch (DbUpdateConcurrencyException)
089.        {
090.            if (!CompanyExists(key))
091.            {
092.                return NotFound();
093.            }
094.            else
095.            {
096.                throw;
097.            }
098.        }
099. 
100.        return Updated(company);
101.    }
102. 
103.    // DELETE odata/Company(5)
104.    public async Task<IHttpActionResult> Delete([FromODataUri] int key)
105.    {
106.        Company company = await db.Companies.FindAsync(key);
107.        if (company == null)
108.        {
109.            return NotFound();
110.        }
111. 
112.        db.Companies.Remove(company);
113.        await db.SaveChangesAsync();
114. 
115.        return StatusCode(HttpStatusCode.NoContent);
116.    }
117. 
118.    // GET odata/Company(5)/Projects
119.    [Queryable]
120.    public IQueryable<Project> GetProjects([FromODataUri] int key)
121.    {
122.        return db.Companies.Where(m => m.Id == key).SelectMany(m => m.Projects);
123.    }
124. 
125.    protected override void Dispose(bool disposing)
126.    {
127.        if (disposing)
128.        {
129.            db.Dispose();
130.        }
131.        base.Dispose(disposing);
132.    }
133. 
134.    private bool CompanyExists(int key)
135.    {
136.        return db.Companies.Count(e => e.Id == key) > 0;
137.    }
138.}

Finally, the KendoUI, HTML/Javascript is this:

001.<h2>Company List</h2>
002. 
003.<div id="company-data">
004.    <div class="col-md-3 col-sm-5 col-xs-5">
005.        <div id="company-list" style="padding: 0px; height: 500px; overflow: auto" data-role="listview" data-template="list-template" data-bind="source: companies, events: {change: OnSelect}" data-selectable="true"></div>
006.        <div>
007.            <button class="btn btn-success btn-sm" id="btn-add-company"><span class="glyphicon glyphicon-plus"></span> Add</button>
008.            <button class="btn btn-danger btn-sm" id="btn-delete-company" data-bind="visible: hasSelection, click: deleteSelection"><span class="glyphicon glyphicon-remove"></span> Delete</button>
009.            <button class="btn btn-default btn-sm" id="btn-clear-company" data-bind="visible: hasSelection, click: clearSelection"><span class="glyphicon glyphicon-ban-circle"></span> Clear</button>
010.            <button class="btn btn-primary btn-sm btn-block" id="btn-save" data-bind="visible: hasChanges, click: saveChanges"><span class="glyphicon glyphicon-cloud-upload"></span> Save All</button>
011.        </div>
012.    </div>
013.    <div class="col-md-9 col-sm-7 col-xs-7" data-bind="visible: hasSelection">
014.        <label for="company-title">Title:</label><br />
015.        <input id="company-title" data-bind="value: selectedItem.Title" ><br />
016.    </div>
017.</div>
018. 
019.<script type="text/x-kendo-template" id="list-template">
020.    <div class="company" style="cursor: pointer">
021.        <span data-bind="text: Title"></span>
022.    </div>
023.</script>
024. 
025.<script>
026.    $(function () {
027.        var firstSync = true;
028.        var companyVM = new kendo.observable({
029.            // Data Source.
030.            companies: new kendo.data.DataSource({
031.                type: 'odata',
032.                transport: {
033.                    create: {
034.                        url: '/odata/Company',
035.                        dataType: 'json',
036.                        type: 'POST'
037.                    },
038.                    read: {
039.                        url: '/odata/Company',
040.                        dataType: 'json'
041.                    },
042.                    update: {
043.                        url: function (data) {
044.                            return '/odata/Company(' + data.Id + ')';
045.                        },
046.                        dataType: 'json',
047.                        type: 'PATCH'
048.                    },
049.                    destroy: {
050.                        url: function (data) {
051.                            return '/odata/Company(' + data.Id + ')';
052.                        },
053.                        dataType: 'json',
054.                        type: 'DELETE'
055.                    },
056.                    parameterMap: function (options, operation) {
057.                        if (operation !== "read" && options) {
058.                            console.log(operation + '*: ' + kendo.stringify(options));
059.                            return {
060.                                models: kendo.stringify(options)
061.                            };
062.                        }
063.                        console.log(operation + ': ' + kendo.stringify(options));
064.                        return options;
065.                    }
066.                },
067.                schema: {
068.                    data: function (data) {
069.                        return data['value'];
070.                    },
071.                    total: function (data) {
072.                        return data['odata.count'];
073.                    },
074.                    model: {
075.                        id: 'Id',
076.                        fields: {
077.                            Title: { type: 'string' }
078.                        }
079.                    }
080.                },
081.                change: function () {
082.                    // We don't want to fire the first time the data loads because that counts as changed.
083.                    if (!firstSync)
084.                        companyVM.set('hasChanges', true);
085.                    else
086.                        firstSync = false;
087.                }
088.            }),
089. 
090.            // Properties.
091.            selectedItem: null,
092.            hasSelection: function () {
093.                return this.get('selectedItem') != null;
094.            },
095.            hasChanges: false,
096. 
097.            // Functions.
098.            clearSelection: function() {
099.                this.set('selectedItem', null);
100.                $('#company-list').getKendoListView().clearSelection();
101.            },
102.            saveChanges: function() {
103.                this.companies.sync();
104.                this.set('hasChanges', false);
105.            },
106.            deleteSelection: function () {
107.                if (confirm('Warning, deletion is permanent! Are you sure you wish to delete this item?')) {
108.                    this.companies.remove(this.selectedItem);
109.                    this.set('hasChanges', true);
110.                    this.clearSelection();
111.                }
112.            },
113. 
114.            // Events.
115.            OnSelect: function (e) {
116.                var list = $(e.sender.element).getKendoListView();
117.                var row = list.select();
118.                var item = list.dataSource.getByUid(row.data('uid'));
119. 
120.                this.set('selectedItem', item);
121.            }
122.        });
123. 
124.        kendo.bind($('#company-data'), companyVM);
125.    });
126.</script>

4 Answers, 1 is accepted

Sort by
0
Atanas Korchev
Telerik team
answered on 04 Mar 2014, 08:37 AM
Hello Josh,

Please try converting the data to JSON in your parameterMap:

parameterMap: function (data, operation) {
     return JSON.stringify(data);
}

In addition you can check this sample application which shows how to implement CRUD with a WebAPI controller: https://github.com/telerik/kendo-examples-asp-net/blob/master/grid-webapi-crud/

Regards,
Atanas Korchev
Telerik
Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
0
Josh
Top achievements
Rank 2
answered on 04 Mar 2014, 02:26 PM
Works great! Thanks.
0
Josh
Top achievements
Rank 2
answered on 04 Mar 2014, 02:30 PM
One question though, because I thought I had looked into this area before... When I log the two:

console.log(operation + ': ' + kendo.stringify(data));
console.log(operation + ': ' + JSON.stringify(data));

I get identical results:

update: {"Id":1009,"Title":"Test"}
update: {"Id":1009,"Title":"Test"}

Why does the latter option work in this case?
0
Atanas Korchev
Telerik team
answered on 04 Mar 2014, 03:05 PM
Hi Josh,

Indeed kendo.stringify is a wrapper for JSON.stringify. It just provides JSON serialization in browsers that don't support JSON.stringify such as IE7.

Regards,
Atanas Korchev
Telerik
Join us on our journey to create the world's most complete HTML 5 UI Framework - download Kendo UI now!
Tags
Data Source
Asked by
Josh
Top achievements
Rank 2
Answers by
Atanas Korchev
Telerik team
Josh
Top achievements
Rank 2
Share this question
or