HOWTO: Create a LightSwitch compatible OData REST Web API Endpoint without EntityFramework

LightSwitch can consume ODATA data source that you can use to gain more flexibility than using the internal data sources that also generate ODATA REST endpoints. Microsoft Web API provides an easy way on how to generate these endpoints. In this post I quickly describe the steps you need to take to actually create an endpoint that is actually usable for LightSwitch HTML as it currently is forced to use the ‘$batch’ method to update which you have to explicitly configure in order to get things working (see External OData data source that does not support $batch).

Of course, there are other steps on how you can create such an endpoint but with this example you have particular control over the whole implementation (i.e. you are not using Entity Framework for automatically persisting data to a database backend).

1. Setting up a ODATA endpoint with Web API is really easy. You can start with a regular ASP.Net application and choosing an ‘Empty’ project with only ‘Web API’ selected.

2. You then create your model from with a controller later on will derive its actual implementation and functionality. This is also pretty straightforward as you can ready Mike Wasson in Getting Started with ASP.NET Web API 2 (C#) and others.
You basically create a regular class with properties that make up your entity.

In our example we create a class called ‘Note’ in a ‘Note.cs’ file that has basic properties such as Id, Title, Description and a Created date.

Note: convention is a model should be placed in the ‘Models’ subfolder.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ODATAWebApi.Models
{
    public class Note
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public DateTime Created { get; set; }
    }
}

Note: you only need ‘using System.ComponentModel.DataAnnotations.Schema;’ when you later want to include annotations such as ‘[ForeignKey]’.

2a. An additional hint when you want to use relationships: you can define relationships in the model as follows (see the commented section below the actual model). LightSwitch will then automatically builds the relations of the models. In this example we have a ‘Task’ Entity that is being referred to. Please also check Entity Relations in OData v4 Using ASP.NET Web API 2.2 for further details on that.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ODATAWebApi.Models
{
    public class Note
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public DateTime Created { get; set; }
		
		// have a relationship as
		// Note: zero-to-one (a 'Note' can have many 'Task' instances)
		// -- > 
		// Task: Many (a 'Task' can have one 'Note')
		// public ICollection<Task> Tasks { get; set; }
		
		// have a relationship as 
		// Note: zero-or-one (a 'Note' must have a 'Task')
		// -- > 
		// Task: one (a 'Task' can have on 'Note')
    }
}

3. When you add the controller for that model you first select a generic ‘Controller’ and later pick your options as ‘Web API 2 OData Controller with read/write actions’. If you selected ‘Web API 2 OData Controller with actions, using Entity Framework’ you would also get the complete serialization logic to a backend which we (as in this example do not want).
Note: convention is a controller should be placed in the ‘Controllers’ subfolder.

4. As soon you selected the controller type you have to define the scaffolding that will actually connect the controller to you model. The Controller must end with ‘Controller’ and should be prefixed with the class name of you model (in plural form).

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using System.Web.Http.OData;
using System.Web.Http.OData.Query;
using System.Web.Http.OData.Routing;
using ODATAWebApi.Models;
using Microsoft.Data.OData;

namespace ODATAWebApi.Controllers
{
  public class NotesController : ODataController
  {
    private static ODataValidationSettings _validationSettings = new ODataValidationSettings();

    private static List<Note> _list = new List<Note>();

    public NotesController()
    {
      if (0 == _list.Count)
      {
        var e1 = new ODATAWebApi.Models.Note();
        e1.Id = 5;
        e1.Title = "a first note";
        e1.Description = string.Format("This is a first note with Id '{0}'", e1.Id);
        e1.Created = DateTime.UtcNow;
        _list.Add(e1);

        var e2 = new ODATAWebApi.Models.Note();
        e2.Id = 42;
        e2.Title = "another note";
        e2.Description = string.Format("This is a another note with Id '{0}'", e2.Id);
        e2.Created = DateTime.UtcNow;
        _list.Add(e2);
      }
    }

    // GET: odata/Notes
    public async Task<IHttpActionResult> GetNotes(ODataQueryOptions<Note> queryOptions)
    {
      // validate the query.
      try
      {
        queryOptions.Validate(_validationSettings);
      }
      catch (ODataException ex)
      {
        return BadRequest(ex.Message);
      }

      return Ok<IEnumerable<Note>>(_list);
    }

    // GET: odata/Notes(5)
    public async Task<IHttpActionResult> GetNote([FromODataUri] int key, ODataQueryOptions<Note> queryOptions)
    {
      // validate the query.
      try
      {
        queryOptions.Validate(_validationSettings);
      }
      catch (ODataException ex)
      {
        return BadRequest(ex.Message);
      }

      var entity =
        (
        from e in _list
        where e.Id == key
        select e
        )
        .FirstOrDefault();
      return Ok<Note>(entity);
    }

    // PUT: odata/Notes(5)
    public async Task<IHttpActionResult> Put([FromODataUri] int key, Note note)
    {
      if (!ModelState.IsValid)
      {
        return BadRequest(ModelState);
      }

      if (key != note.Id)
      {
        return BadRequest();
      }

      note.Id = key;
      note.Description = string.Format("UPDATED: {0}", note.Description);
      return Updated(note);
    }

    // POST: odata/Notes
    public async Task<IHttpActionResult> Post(Note note)
    {
      if (!ModelState.IsValid)
      {
        return BadRequest(ModelState);
      }

      // TODO: Add create logic here.

      int? eId =
        (
        from ec in _list
        orderby ec.Id descending
        select ec.Id
        )
        .FirstOrDefault();
      eId = (null != eId) ? (eId + 1) : 1;
      note.Id = (int) eId;
      note.Created = DateTime.UtcNow;
      _list.Add(note);
      return Created(note);
    }

    // PATCH: odata/Notes(5)
    [AcceptVerbs("PATCH", "MERGE")]
    public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Note> delta)
    {
      if (!ModelState.IsValid)
      {
        return BadRequest(ModelState);
      }

      var entity =
        (
        from e in _list
        where e.Id == key
        select e
        )
        .FirstOrDefault();
      if (null == entity)
      {
        return StatusCode(HttpStatusCode.Gone);
      }
      delta.Patch(entity);
      return Updated(entity);
    }

    // DELETE: odata/Notes(5)
    public async Task<IHttpActionResult> Delete([FromODataUri] int key)
    {
      // TODO: Add delete logic here.

      var entity =
        (
        from e in _list
        where e.Id == key
        select e
        )
        .FirstOrDefault();
      if (null == entity)
      {
        return StatusCode(HttpStatusCode.Gone);
      }
      _list.Remove(entity);

      return StatusCode(HttpStatusCode.NoContent);
    }
  }
}

5. With this you will end up with a general controller template already containing CRUD operations in a standard naming convention.
But for the controller to actually accept http request we have to adjust ‘Register’ method of the ‘WebApiConfig’ class under the ‘App_Start’ folder.

Here you have to pay special attention the non-default ‘MapODataRoute()’ call, as it sets up the ‘$batch’ handler we need so LigthSwitch can make batch updates. This handler effectively needs a custom ‘GetModel()’ method where you place all your entites that should be handled via this route. There you can also define the ‘plural’ names for your entity sets (in this example ‘Notes’ as the EntitySet name for the Entity ‘Note’).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using System.Web.Http;
using System.Web.Http.OData.Builder;
using System.Web.Http.OData.Batch;
using System.Web.Http.OData.Formatter;
using Microsoft.Data.Edm;
using ODATAWebApi.Models;
using System.Web;

namespace ODATAWebApi
{
  public static class WebApiConfig
  {
    public static void Register(HttpConfiguration config)
    {
      config.Routes.MapODataRoute(
        routeName: "odata",
        routePrefix: "odata",
        model: GetModel(),
        batchHandler: new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));
      config.MapHttpAttributeRoutes();
	  config.EnableQuerySupport();
    }
    private static IEdmModel GetModel()
    {
      ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
      builder.Namespace = "ODATAWebApi.Controllers";
      builder.EntitySet<Note>("Notes");
      return builder.GetEdmModel();
    }
  }
}

Note: ‘config.EnableQuerySupport();’ is only needed if you want to enable the query options in ODATA (e.g. ‘$filter’, ‘$top’ etc). If you use that please also check ODataHttpConfigurationExtensions.EnableQuerySupport Method and Supporting Entity Relations in OData v3 with Web API 2.

6. With all these steps you can now publish the endpoint to Azure or wherever and have LightSwitch consume your data source as if it was from the internal database.
And because we added the code for the batch handler we don’t get any errors when saving or changing entities via that endpoint. You will also note because of our ‘GetModel’ method we have the same plural names for the entity set in LightSwitch (and LS named them automatically in the correct way).

You can certainly test the endpoint without LS as well by using fiddler or something similar. Or of ycource also with PowerShell (by creating an assembly from the service or via ‘Invoke-RestMethod’)

<!-- GET http://api.example.com/odata/$metadata HTTP/1.1 -->
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
  <edmx:DataServices m:DataServiceVersion="3.0" m:MaxDataServiceVersion="3.0" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
    <Schema Namespace="ODATAWebApi.Models" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
      <EntityType Name="Note">
        <Key>
          <PropertyRef Name="Id" />
        </Key>
        <Property Name="Id" Type="Edm.Int32" Nullable="false" />
        <Property Name="Title" Type="Edm.String" />
        <Property Name="Description" Type="Edm.String" />
        <Property Name="Created" Type="Edm.DateTime" Nullable="false" />
      </EntityType>
    </Schema>
    <Schema Namespace="ODATAWebApi.Controllers" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
      <EntityContainer Name="Container" m:IsDefaultEntityContainer="true">
        <EntitySet Name="Notes" EntityType="ODATAWebApi.Models.Note" />
      </EntityContainer>
    </Schema>
  </edmx:DataServices>
</edmx:Edmx>

UPDATE 2014-07-20:
There is a gotcha I just ran into which I want to share with you. While in this example I showed how to create an ODATA REST WebAPI endpoint as a separate project you can do the same also from within your main LightSwitch project.
While I was generating a Controller for a class named ‘Task’ I got an error as the class ‘Task’ already exists someehere within the ‘System.Threading.Tasks’ namespace. That means three things:
0. You certainly have to insert using directives for all your required namespace to the appropriate places (the main namespace, e.g. ‘LightSwitchApplication’, and also for your ‘Models’ and the ‘Controllers’).
1. The default scaffolded controller must be updated because of the name clashing (I did not try to move the using directive for ‘System.Threading.Tasks’ below the project specific using directive.
2. And the most important thing: you have to update ‘GetModel()’ method within the ‘WebApiConfig.cs’ when you define the EntitySet to use the full name for your model class:

private static IEdmModel GetModel()
{
  ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
  builder.Namespace = "LightSwitchApplication.Controllers";
  builder.EntitySet<LightSwitchApplication.Models.Note>("Notes");
  // instead of the default:
  // builder.EntitySet<Note>("Notes");
  return builder.GetEdmModel();
}

When you leave the default everything compiles just fine but when browsing the ODATA endpoint’s EntitySet via a screen of the LightSwitch application you will get an error similar to this:


{"odata.error":{"code":"1","message":{"lang":"en-US","value":"<?xml version=\"1.0\" encoding=\"utf-16\"?><ExceptionInfo><Message>An error occurred while processing this request.
Inner exception message:
{
  \"odata.error\":{
    \"code\":\"\",\"message\":{
      \"lang\":\"en-US\",\"value\":\"An error has occurred.\"
    },\"innererror\":{
      \"message\":\"The given model does not contain the type 'LightSwitchApplication.Models.Task'.
Parameter name: elementClrType\",\"type\":\"System.ArgumentException\",\"stacktrace\":\"   at System.Web.Http.OData.ODataQueryContext..ctor(IEdmModel model, Type elementClrType)
   at System.Web.Http.OData.ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
   at System.Web.Http.Controllers.HttpActionBinding.&lt;ExecuteBindingAsyncCore&gt;d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at System.Web.Http.Controllers.ActionFilterResult.&lt;ExecuteAsync&gt;d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at System.Web.Http.Dispatcher.HttpControllerDispatcher.&lt;SendAsync&gt;d__0.MoveNext()\"
    }
  }
}</Message><StackTrace>   at System.Data.Services.Client.DataServiceRequest.Execute[TElement](DataServiceContext context, QueryComponents queryComponents)
   at System.Data.Services.Client.DataServiceQuery`1.Execute()
   at System.Data.Services.Client.DataServiceQuery`1.ExecuteInternal()
   at System.Data.Services.Client.DataServiceQuery.Execute()
   at Microsoft.LightSwitch.ServerGenerated.Implementation.DataProvider.AstoriaDataServiceDataProvider.DataServiceOuterQuery`1.&lt;&gt;c__DisplayClass1e`1.&lt;Execute&gt;b__1d()
   at Microsoft.LightSwitch.ServerGenerated.Implementation.DataProvider.AstoriaDataServiceDataProvider.InvokeAstoriaOperation(DataServiceContext context, Action astoriaOperation)
   at Microsoft.LightSwitch.ServerGenerated.Implementation.DataProvider.AstoriaDataServiceDataProvider.DataServiceOuterQuery`1.Execute[TElement](IQueryable innerQuery, Expression outerExpression)
   at Microsoft.LightSwitch.ServerGenerated.Implementation.DataProvider.OuterQuery`1.ExecuteInternal[TElement](IQueryable innerQuery, Expression outerExpression)
   at Microsoft.LightSwitch.ServerGenerated.Implementation.DataProvider.OuterQueryExecutor`1.Execute[TElement](Expression expression)
   at Microsoft.LightSwitch.ServerGenerated.Implementation.DataProvider.OuterQuery`1.GetEnumerator()
   at System.Collections.Generic.List`1.InsertRange(Int32 index, IEnumerable`1 collection)
   at System.Collections.Generic.List`1.AddRange(IEnumerable`1 collection)
   at Microsoft.LightSwitch.ServerGenerated.Implementation.DataServiceQueryProvider.QueryableWrapper`1.&lt;&gt;c__DisplayClass10.&lt;GetEnumerator&gt;b__f()
   at Microsoft.LightSwitch.Threading.DispatcherExtensions.Invoke(IDispatcher dispatcher, Action action)
   at Microsoft.LightSwitch.ServerGenerated.Implementation.DataService`1.LogicInvoke(Action a)
   at Microsoft.LightSwitch.ServerGenerated.Implementation.DataService`1.Microsoft.LightSwitch.ServerGenerated.Implementation.IODataService.LogicInvoke(Action a)
   at Microsoft.LightSwitch.ServerGenerated.Implementation.DataServiceQueryProvider.QueryableWrapper`1.GetEnumerator()
   at Microsoft.LightSwitch.ServerGenerated.Implementation.DataServiceQueryProvider.QueryableWrapper`1.System.Collections.IEnumerable.GetEnumerator()
   at System.Data.Services.WebUtil.GetRequestEnumerator(IEnumerable enumerable)
   at System.Data.Services.QueryResultInfo.MoveNext()
   at System.Data.Services.DataService`1.SerializeResponseBody(RequestDescription description, IDataService dataService, IODataResponseMessage responseMessage)
   at System.Data.Services.DataService`1.HandleNonBatchRequest(RequestDescription description)
   at System.Data.Services.DataService`1.HandleRequest()</StackTrace><ErrorInfo /></ExceptionInfo>"}}}

UPDATE 2014-07-21:
And another update on this issue. Whenever you use LightSwitch to consume (wrap) an ODATA endpoint from within your namespace and you name it the same as the wrapped endpoint you effectively have to specify the full name of the class even within the actual endpoint you are working in (because the search order of the namespaces seems to find the “outer” class definition first). This was not obvious for my untrained eye, but you might certainly have been aware of it already.

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

%d bloggers like this: