This will be a quick post about WebAPI and ODATA controllers, and a strange “bug” or behaviour I found. I was not aware of this, so maybe it is also of some interest for you. When you create an ODATA service via WebAPI and want to connect two or more entity sets via a relation the standard example will tell you to define your models somehow similar to this (see also Mike Wasson and his post Supporting Entity Relations in OData v3 with Web API 2 :

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

namespace bizdfchApplication.Models
{
  public class Task
  {
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public DateTimeOffset Created { get; set; }
    public DateTimeOffset Modified { get; set; }
    public DateTimeOffset Due { get; set; }

    public int TenantId { get; set; }
    public Tenant Tenant { get; set; }
  }

  public class Tenant
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    public List<Task> Tasks { get; set; }
  }
}

This would and will define a relationship where a Tenant can have multiple Tasks and a Task can only have one Tenant. When you look at the generated $metadata you will notice that your relationship (or navigationproperty in ODATA terms) is expressed not via one Association but actually via _two_ distinct Associations. One Association into each direction. This is strange, as both Associations express the correc relationship (from their point of view). This is actually nuisance as this would mean you would have to code the relationship manually twice when creating, updating and deleting your entities.

Further investigation on ASP.net and stackoverflow showed that this is actually known and will not be fixed, as you can read in the following posts:

  1. Two distinct associations instead of one
    http://stackoverflow.com/questions/13319792/two-distinct-associations-instead-of-one
  2. OData model builder should reuse association for bi-direction navigation properties
    http://aspnetwebstack.codeplex.com/workitem/623
  3. Missing inverse property in asp.net webapi odata $metadata
    http://stackoverflow.com/questions/15828643/missing-inverse-property-in-asp-net-webapi-odata-metadata

The story behind is that in ODATA v4 the navigation properties will not have associations any more (as far as I understood the examples in the preliminary ODATA v4 spec). Still a nusiance when dealing with WebAPI ODATA v3 – much more manual coding and much space for inconsistencies as you have no atomic way to update both sides of the relationship.

8 Comments »

  1. Since Lightswitch only accepts v3 for now, this behavior breaks Lightswitch. Even though you create v3 libraries, the ODataModelBuilder produces two associations. One of them is “0..1 to 0..1” and Lightswitch will throw those out. Thus, any screens in LS treat the data model has having one-way relationships only…and this is corraborated by the lsml files.

    Thanks so much for writing this up. I’m sure you felt “nobody cares” but I’ve been beating my head against this for the past two weeks. I’m frustrated that the same exact Edm model will will work for LS to database but not LS to OData.

  2. I once had a quick look on how the generate the EDM manually but voted against it, as it first did not seem to be supported, plus I would have to override it on every rebuild and I then still would not be sure if the generated javascript model would support this.
    As LightSwitch does not seem to get much attention from Microsoft lately I instead opted to use ODATAv4 and the jaydata client library (without LightSwitch generated entities).
    I am a bit disappointed by the evolution of LightSwitch and will not invest into too much LightSwitch specific stuff. Sorry I am of no help here.

    • Ronald, Thanks for your reply. Agree – LS evolution – lack of attention – is a shame. I still find LS useful and it’s liberating to separate database and backend from LS project tooling using WebAPI OData. I was able to fix the ODatav3 association problem and now WebAPI OData is working quite well with LS. More Info for your readers: http://joshuabooker.com/Blog/Post/22/WebAPI-ODAta-Part-6—Hook-it-up-to-LightSwitch—Solved Code: .gist table { margin-bottom: 0; } This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using ODataSample.Models; namespace ODataSample.Controllers { using Microsoft.Restier.WebApi; public class AdventureWorksController : ODataDomainController<AdventureWorksDomain> { private AdventureWorksContext DbContext { get { return Domain.Context;} } } } view raw AdventureWorksController.cs hosted with ❤ by GitHub This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters using System; using System.Collections.Generic; using System.Linq; using System.Web; using ODataSample.Models; namespace ODataSample.Controllers { using Microsoft.Restier.EntityFramework; public class AdventureWorksDomain : DbDomain<AdventureWorksContext> { public AdventureWorksContext Context { get { return DbContext; } } } } view raw AdventureWorksDomain.cs hosted with ❤ by GitHub This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters using System; using System.Collections.Generic; using System.Linq; using System.Web.Http; namespace ODataSample { using Microsoft.Restier.WebApi; using Microsoft.Restier.WebApi.Batch; using Controllers; public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); RegisterAdventureWorks(config, GlobalConfiguration.DefaultServer); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } public static async void RegisterAdventureWorks(HttpConfiguration config, HttpServer server) { await config.MapODataDomainRoute<AdventureWorksController>( "AdventureWorksV4", "AdventureWorks/V4", new ODataDomainBatchHandler(server)); } } } view raw Part3-WebAPIConfig.cs hosted with ❤ by GitHub This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters using System; using System.Collections.Generic; using System.Linq; using System.Web.Http; using System.Web.Http.OData.Builder; using System.Web.Http.OData.Batch; using System.Web.Http.OData.Extensions; using ODataSample.Models; namespace ODataSample { using Microsoft.Restier.WebApi; using Microsoft.Restier.WebApi.Batch; using Controllers; public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); RegisterAdventureWorksV4(config, GlobalConfiguration.DefaultServer); RegisterAdventureWorksV3(config, GlobalConfiguration.DefaultServer); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } public static async void RegisterAdventureWorksV4(HttpConfiguration config, HttpServer server) { await config.MapODataDomainRoute<AdventureWorksController>( "AdventureWorksV4", "AdventureWorks/v4", new ODataDomainBatchHandler(server)); } /**/ public static void RegisterAdventureWorksV3(HttpConfiguration config, HttpServer server) { ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); builder.EntitySet<Address>("Addresses"); builder.EntitySet<AddressType>("AddressTypes"); builder.EntitySet<BusinessEntity>("BusinessEntities"); builder.EntitySet<BusinessEntityContact>("BusinessEntityContacts"); builder.EntitySet<BusinessEntityAddress>("BusinessEntityAddresses"); builder.EntitySet<EmailAddress>("EmailAddresses"); builder.EntitySet<Person>("People"); builder.EntitySet<PersonPhone>("PersonPhones"); builder.EntitySet<PhoneNumberType>("PhoneNumberTypes"); builder.EntitySet<Password>("Passwords"); builder.EntitySet<StateProvince>("StateProvinces"); builder.EntitySet<CountryRegion>("CountryRegions"); builder.EntitySet<ContactType>("ContactTypes"); //HttpServer server = new HttpServer(config); config.Routes.MapODataServiceRoute("AdventureWorksV3", "AdventureWorks/v3", builder.GetEdmModel(), new DefaultODataBatchHandler(server)); //config.Routes.MapODataServiceRoute("odata", "odata", builder.GetEdmModel()); } } } view raw Part4-WebAPIConfig.cs hosted with ❤ by GitHub This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters using System; using System.Collections.Generic; using System.Linq; using System.Web.Http; using System.Web.Http.OData.Builder; using System.Web.Http.OData.Batch; using System.Web.Http.OData.Extensions; using ODataSample.Models; using System.IO; using System.Xml; using System.Data.Entity; using System.Data.Entity.Infrastructure; using Microsoft.Data.Edm; //using Microsoft.Data.Edm.Csdl; using Microsoft.Data.Edm.Validation; using Microsoft.Data.OData; namespace ODataSample { using Microsoft.Restier.WebApi; using Microsoft.Restier.WebApi.Batch; using Controllers; public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); RegisterAdventureWorksV4(config, GlobalConfiguration.DefaultServer); RegisterAdventureWorksV3(config, GlobalConfiguration.DefaultServer); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } public static async void RegisterAdventureWorksV4(HttpConfiguration config, HttpServer server) { await config.MapODataDomainRoute<AdventureWorksController>( "AdventureWorksV4", "AdventureWorks/v4", new ODataDomainBatchHandler(server)); } /**/ public static void RegisterAdventureWorksV3(HttpConfiguration config, HttpServer server) { /* ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); builder.EntitySet<Address>("Addresses"); builder.EntitySet<AddressType>("AddressTypes"); builder.EntitySet<BusinessEntity>("BusinessEntities"); builder.EntitySet<BusinessEntityContact>("BusinessEntityContacts"); builder.EntitySet<BusinessEntityAddress>("BusinessEntityAddresses"); builder.EntitySet<EmailAddress>("EmailAddresses"); builder.EntitySet<Person>("People"); builder.EntitySet<PersonPhone>("PersonPhones"); builder.EntitySet<PhoneNumberType>("PhoneNumberTypes"); builder.EntitySet<Password>("Passwords"); builder.EntitySet<StateProvince>("StateProvinces"); builder.EntitySet<CountryRegion>("CountryRegions"); builder.EntitySet<ContactType>("ContactTypes"); //HttpServer server = new HttpServer(config); config.Routes.MapODataServiceRoute("AdventureWorksV3", "AdventureWorks/v3", builder.GetEdmModel(), new DefaultODataBatchHandler(server)); */ IEdmModel model = GetEdmModel<AdventureWorksContext>(); config.Routes.MapODataServiceRoute("AdventureWorksV3", "AdventureWorks/v3", model, new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer)); } public static IEdmModel GetEdmModel<T>() where T : DbContext, new() { IEdmModel model; DbContext context = new T(); string contextName = new Stack<string>(context.ToString().Split('.')).Pop(); using (MemoryStream stream = new MemoryStream()) { using (XmlWriter writer = XmlWriter.Create(stream)) { EdmxWriter.WriteEdmx(context, writer); writer.Close(); stream.Seek(0, SeekOrigin.Begin); using (XmlReader reader = XmlReader.Create(stream)) { model = Microsoft.Data.Edm.Csdl.EdmxReader.Parse(reader); //LS pukes when there's no m:IsDefaultEntityContainer="true" attribute on the EntityContainer node IEdmEntityContainer defaultContainer = model.FindDeclaredEntityContainer(contextName); ODataUtils.SetIsDefaultEntityContainer(model,defaultContainer , true); } } } return model; } } } view raw Part5-WebAPIConfig.cs hosted with ❤ by GitHub

Leave a Reply to Ronald Rink Cancel 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.