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>

2 Comments »

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.