I just came across another nuisance in .NET WebAPI that I want to share with you quickly. In one of our LightSwitch projects we use WebAPI ODATA controller to help out when LightSwitch internal capabilities come to their end. As we are using a PowerShell once in a while as you might already have guessed from our web site we like to consume these ODATA controllers as conveniently as possible, i.e. not writing tedious ODATA REST URLs with ‘Invoke-RestMethod’. The preferred way is to consume them via a service reference from a class library – which works just fine, except … when you use WebAPI ODATA controller without EntityFramework.
The problem is that the WebAPI controller does not update/create the needed HTTP headers for you – at least not automatically. You will notice this when trying to create a new entity in one of your entity sets in your model, as you can see below:
Add-Type -Path ".\OdataWrapper.dll"; $lsutil = New-Object OdataWrapper.Utilities.Container('http://www.example.com/api/Utilities.svc'); $lsutil.Credentials = [System.Net.CredentialCache]::DefaultCredentials $eNew = New-Object OdataWrapper.Utilities.Entity $eNew.Id = 'MyId'; $eNew.Description = 'Some description' $eNew.Status = 'FAILED'; $eNew.Active = $true; $eNew.TimeStamp = [DateTime]::Now.ToString('yyyy-MM-ddTHH:mm:ss.ffffffzzz'); $lsutil.AddToEntities($eNew); $lsutil.UpdateObject($eNew); $lsutil.SaveChanges(); Exception calling "SaveChanges" with "0" argument(s): "The response to this POST request did not contain a 'location' header. That is not supported by this client." At line:1 char:1 + $lsutil.SaveChanges(); + ~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [], MethodInvocationException + FullyQualifiedErrorId : NotSupportedException
This is certainly not specific to PowerShell, but stems from the fact the POST method code of the controller returns with a standard
return StatusCode(HttpStatusCode.Created);
which actually does not return a ‘Location’ header. Though RFC 2616 ch10.2.2 ‘201 Created’ only specifies a ‘SHOULD’ for returning a location field, it is enough for the Microsoft WebAPI implementation to refuse the response an throw and exception to this. This is particular annoying as the resource has in deed been created on the server.
So to get around this you seem to go down the long way and code the ‘Location’ header manually:
var segments = new List<ODataPathSegment>(); segments.Add(new EntitySetPathSegment(this.ControllerContext.RouteData.Values["controller"].ToString())); segments.Add(new KeyValuePathSegment( entity.Id is string ? string.Format("'{0}'", entity.Id) : entity.Id) ); var uri = new Uri(this.Url.CreateODataLink( this.Request.ODataProperties().RouteName , this.Request.ODataProperties().PathHandler , segments )); var response = Request.CreateResponse(HttpStatusCode.Created, entity); response.Headers.Location = uri; return ResponseMessage(response);
static class ODataControllerHelper { public static HttpResponseMessage ResponseCreated<T>(ODataController Controller, T Entity, string Key = "Id") { var segments = new List<ODataPathSegment>(); segments.Add(new EntitySetPathSegment(controller.ControllerContext.RouteData.Values["controller"].ToString())); var propertyInfo = Entity.GetType().GetProperty(Key); var value = propertyInfo.GetValue(Entity, null); segments.Add(new KeyValuePathSegment( value is string ? string.Format("'{0}'", value.ToString()) : value.ToString()) ); var uri = new Uri(Controller.Url.CreateODataLink( Controller.Request.ODataProperties().RouteName , Controller.Request.ODataProperties().PathHandler , segments )); var response = Controller.Request.CreateResponse(HttpStatusCode.Created, Entity); response.Headers.Location = uri; return response; } }
This could easily be done automatically by the framework as the context is pretty clear.
And in case you wonder why the ‘KeyValuePathSegment()’ is wrapping the key with single quote, here is another not so great feature: though (in our scenario) the metadata model describes the key
/Id
property as a string the value is passed without quotes to the ODataPathSegment factory. Again this is not really understandable as the ODATA convention for string keys is to have them single quoted (and numbers not quoted) and the returned payload of the response also contains the entity key correctly quoted. I filed an issue on codeplex for this under CreateODataLink creates Uri with missing quotes for model keys of type string where I go the reply that there is a query helper method addressing this which I have not yet tested):
using Microsoft.Data.OData.Query; segments.Add( new KeyValuePathSegment( ODataUriUtils.ConvertToUriLiteral( entity.Id, Microsoft.Data.OData.ODataVersion.V3 )));
POST http://www.example.com/api/Model.svc/Entities HTTP/1.1 Content-Type: application/atom+xml DataServiceVersion: 1.0;NetFx MaxDataServiceVersion: 3.0;NetFx Accept: application/atom+xml,application/xml Accept-Charset: UTF-8 User-Agent: Microsoft ADO.NET Data Services Host: www.example.com Content-Length: 806 Expect: 100-continue <?xml version="1.0" encoding="utf-8"?> <entry xmlns="http://www.w3.org/2005/Atom" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"> <category term="LightSwitchApplication.Models.Entity" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> <id /> <title /> <updated>2014-08-08T18:52:34Z</updated> <author><name /></author> <content type="application/xml"> <m:properties> <d:Active m:type="Edm.Boolean">true</d:Active> <d:Description>Some description</d:Description> <d:Id>SomeKey</d:Id> <d:Status>FAILED</d:Status> <d:TimeStamp m:type="Edm.DateTime">2014-08-08T20:52:33.82635+02:00</d:TimeStamp> </m:properties> </content> </entry> HTTP/1.1 201 Created Cache-Control: no-cache Pragma: no-cache Content-Type: application/atom+xml; charset=utf-8 Expires: -1 Location: http://www.example.com/api/Model.svc/Entities('SomeKey') Server: Microsoft-IIS/8.0 DataServiceVersion: 3.0 X-AspNet-Version: 4.0.30319 X-Content-Type-Options: nosniff X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcQWRtaW5pc3RyYXRvclxEb2N1bWVudHNcdmlzdWFsIHN0dWRpbyAyMDEzXFByb2plY3RzXEFwcGxpY2F0aW9uMkF6dXJlXEFwcGxpY2F0aW9uMlxiaW5cRGVidWdcYXBpXFV0aWxpdGllcy5zdmNcSGVhbHRoQ2hlY2tz?= Persistent-Auth: true X-Powered-By: ASP.NET Date: Fri, 08 Aug 2014 18:52:56 GMT Content-Length: 1311 <?xml version="1.0" encoding="utf-8"?> <entry xml:base="http://www.example.com/api/Model.svc" xmlns="http://www.w3.org/2005/Atom" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml"> <id>http://www.example.com/api/Model.svc/Entities('SomeKey')</id> <category term="LightSwitchApplication.Models.Entity" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> <link rel="edit" href="http://www.example.com/api/Model.svc/Entities('SomeKey')" /> <link rel="self" href="http://www.example.com/api/Model.svc/Entities('SomeKey')" /> <title /> <updated>2014-08-08T18:52:55Z</updated> <author> <name /> </author> <content type="application/xml"> <m:properties> <d:Id>SomeKey</d:Id> <d:Description>Some description</d:Description> <d:TimeStamp m:type="Edm.DateTime">2014-08-08T20:52:55.6626896+02:00</d:TimeStamp> <d:Status>FAILED</d:Status> <d:Active m:type="Edm.Boolean">true</d:Active> </m:properties> </content> </entry>
Reblogged this on Dinesh Ram Kali..
Ronald, Thanks for this. Saved me today!