Relationship links


Problem Statement


OData (the protocol used by WCF Data Services) enables you to address the relationships between Entries. This functionality is required to be able to create or change a relationship between two instances, such as an Order_Detail that is related to a given Order. Currently the OData protocol requires clients and servers to agree up front on how to address relationships. For example, most OData implementations today follow the optional URL conventions for addressing links stated in the main OData spec. This convention states that a “$links” URI path segment be used to distinguish the relationship between Entries as opposed to an entry itself. For example, the following URI addresses the relationship between Order 1 and one or more OrderDetail Entries: http://myserver/myODataService/Orders(1)/$links/OrderDetails . Currently many of the OData client libraries rely on this $links convention when manipulating relationships.


In an effort to make the protocol (OData) used by odata services more “hypermedia friendly” and reduce the coupling between clients and servers (by allowing a client to be fully response payload driven) we would like to remove the need for a URL convention and have the server state (in the response payload) the URIs which represent the relationships for the Entry represented in a given response.


Design (state: draft)


The OData protocol already has the constructs necessary to express navigation property URIs in the form of standard <atom:link> elements. For example, the following is the atom representation for an Order Entry which has a navigation property Order_Details expressed as an <atom:link>.





<entry>
  <category term=”SampleModel.Order”
              scheme=”http://schemas.microsoft.com/ado/2007/08/dataservices/scheme”/>

  <id>http://host/service.svc/Orders(1)</id>
  <title type=”text” />
  <updated>2008-03-30T21:52:45Z</updated>
  <author>
    <name />
  </author>
  <link rel=”edit” title=”Orders” href=”Orders(1)” mce_href=”Orders(1)” />

  <link
   rel=”
http://schemas.microsoft.com/ado/2007/08/dataservices/related/Order_Details”
    type=”application/atom+xml;type=feed” title=”Order_Details”
    href=”
http://host/service.svc/Orders(1)/Order_Details” />

  <content type=”application/xml”>
    <m:properties>
      <d:OrderID m:type=”Edm.Int32″>1</d:OrderID>
      <d:ShippedDate m:type=”Edm.DateTime”>1997-08-25T00:00:00</d:ShippedDate>
    </m:properties>
  </content>
</entry>


Following this pattern, the URI needed to address the relationship between entities can also be expressed as a link element in the following form:
<link  rel=”http://schemas.microsoft.com/ado/2007/08/dataservices/relatedlinks/<relationshipPropertyName>”   type=”application/ xml” Title=”< relationshipPropertyName >”  ” href=” relatedLinksURI” />


Where:
relationshipPropertyName is a navigation property name as defined in CSDL associated with the datat service.
relatedLinksURI is URI which identifies the relationship between the Entry represented by the parent <entry> and another Entry (or group of Entries) as identified by the navigation property


The example below shows an Order entry with a link element (as described above) that represents the orders’ relationship with OrderDetails as a response to the following GET query ”http://host/service.svc/Orders(1)”.





<entry>
  <category term=”SampleModel.Order”
    scheme=”http://schemas.microsoft.com/ado/2007/08/dataservices/scheme”/>
  <id>http://host/service.svc/Orders(1)</id>
  <title type=”text” />
  <updated>2008-03-30T21:52:45Z</updated>
  <author>
    <name />
  </author>
  <link rel=”edit” title=”Orders” href=”Orders(1)” mce_href=”Orders(1)” /> 
  <link
    rel=”http://schemas.microsoft.com/ado/2007/08/dataservices/related/Order_Details”
    type=”application/atom+xml;type=feed” title=”Order_Details”
    href=” http://host/service.svc/Orders(1)/Order_Details” />

  <link                rel=”http://schemas.microsoft.com/ado/2007/08/dataservices/relatedlinks/Order_Details”
    type=”application/xml” Title=”Order_Details””
    href=”
http://host2/Orders(1)/$links/Order_Details”/>

  <content type=”application/xml”>
    <m:properties>
      <d:OrderID m:type=”Edm.Int32″>1</d:OrderID>
      <d:ShippedDate m:type=”Edm.DateTime”>1997-08-25T00:00:00</d:ShippedDate>
    </m:properties>
  </content>
</entry>


For JSON we would use “associationuri” to hold the relationship URI. As shown in the example below the associationuri would be a sibling of the uri property on a navigation property.





DataServiceVersion: 3.0;
{
  d : {
    __metadata: {
      uri: http://host/service.svc/Orders(1),
      type: ” SampleModel.Order”,
    },
    OrderId: 1,
    ShippedDate: “1997-08-25T00:00:00”,
    Order_Details: {
      __deferred: {
           uri: “http://host/service.svc/Orders(1)/Order_Details/” ,
           associationuri: “http://host2/Orders(1)/$links/Order_Details”
      }
    }
  }
}


In the case of expanded relationships (using data services $expand operator) the associationuri would be placed before the “results” of the expanded related entities. For example the following GET query request “http://host/service.svc /Orders(1)?$expand=Order_Details&$format=json” would produce the following server payload:





DataServiceVersion: 3.0;
{
  d : {
  __metadata: {
      uri: http://host/service.svc/Orders(1),
      type: ” SampleModel.Order”,
    },
  OrderId: 1,
  ShippedDate: “1997-08-25T00:00:00”,
  Order_Details:{
    __linkInfo : {
      associationuri: “
http://host2/Orders(1)/$links/Order_Details”
    }
,
    results:[
    {
      __metadata: {
        uri: http://host/service.svc/Order_Details(OrderID=10643,ProductID=28),
        type: “SampleModel.Order_Details”
        },
    OrderID: 1,
    ProductID: 28,
    UnitPrice: “45.6000”,
    Quantity: 15,
    Discount: 0.25,
    Orders: {
      __deferred: {
        uri: “http://host/service.svc/Order_Details(OrderID=1,ProductID=28)/Orders” ,
        associationuri: “http://host2/Order_Details(OrderID=1,ProductID=28)/$links/Orders
      }
   },
   Products: {
    __deferred: {
      uri: “http://host/service.svc/Order_Details(OrderID=10643,ProductID=28)/Products”,
      associationuri: “http://host2/Order_Details(OrderID=1,ProductID=28)/$links/Products ”
}}}]}
}
}


Server Rules:

– The server MAY return URI that represents the relationship between two Entries.


Client Rules:

– Client MUST use relationship URIs obtained from the server payload when addressing a relationship between two Entries, if present in the server response


– If the relationship URI is not present in the server response the client runtime MAY choose to use convention, for URI to construction, to address the relationship.


Backwards Compatibility:

Existing WCF Data Services clients that rely on convention, for URI construction, to address the relationship between two Entries will continue to work as long as the server supports the convention.


We look forward to hearing what you think of this approach…


Ahmed Moustafa


Program Manager, WCF Data Services


This post is part of the transparent design exercise in the Astoria Team. To understand how it works and how your feedback will be used please look at this post.


Technorati Tags:

Comments (3)

  1. I like the idea of encoding the relationship information w/o resorting to URI convention. But the suggested use of the rel and title attributes is, I suspect, going to cause trouble for state machine clients.

    I’d like to see something like:

    <link rel="relationship" href="{uri-to-details}" />

    IOW, offer a <link /> that points the user agent to a resource that contains the details you are trying to encode into your REL and TITLE attributes.

    This will give the most flexibility, prevent OData from encoding resource-specific information into relation links, and prevent OData from overriding the use of TITLE.

  2. I am just looking at the Data Service Provider toolkit and I notice several things.

    1.) You need a huge amount of code to expose the most simple classes like

    public Class B

    {

      public string NoIdProperty {get;set;}

    }

    public Class A

    {

      public B MyB { get;set; }

      public int MyIDProperty { get; set; }

    }

    2.) You do really messy things like in GetQueryRootForResourceSet(ResourceSet) function

    3.) The whole System.Data.Services.Providers Namespace is as good as undocumented

    4.) The whole namespace thing smells like entity framework to me

    5.) The whole error reporting/debugging is not even close to real life requirements. I definately need more details that just "Something blew up somewhere".

    I really love the idea of a WCF Service that exposes objects and I love the whole basic concept behind data services.

    Maybe I am just too stupid, but so far for me it looks the current ADO .NET Data Service implementation will probably never match my expectations.

    Maybe you should think about dumping the current implementation and start a rewrite that is focused on exposing objects (not just entity framework) from the scratch. Trying to fix the current code base seems to be a big waste of your and my time. But at least you should do your homework in regards to documentation before thinking about extensions.

    Cheers,

    Tobias

  3. Alex D James says:

    Tobias,

    Answering your points in turn…

    1) There is no doubt that the most simple path – the one we originally optimized for – is EF.

    However the reflection provider is pretty simple too, and is designed to make it easy for you to take custom classes and expose then as a Data Service.

    The first step is to create a ‘Context’ class to represent the model of you Data Service, something like this for example:

    public class Context
    {
      private List<A> _As; // TODO: Initialize some how.
      private List<B> _Bs;
      public IQueryable<A> As {
        return _As.AsQueryable();
      }
      public IQueryable<B> Bs {
        return _Bs.AsQueryable();
      }
    }

    And then on your classes Identify the Key properties using the [DataServiceKeyAttribute] like this:

    [DataServiceKeyAttribute(“NoIdProperty”)]
    public Class B
    {
     public string NoIdProperty {get;set;}
    }

    At this point you can expose your classes as a DataService simply by using

    public MyDataService: DataService<Context> {…}

    The service will be readonly, but you if your Context class also implements IDataServiceUpdateProvider (like the L2S example we posted on CodeGallery

    http://code.msdn.microsoft.com/IUpdateableLinqToSql) then you have a readwrite service.

    Finally if the reflection provider doesn’t give you enough flexibility then you need to implement a full Data Service Provider, which is admittedly not trivial. But then it is a very advanced API, which we don’t expect many developer to need to implement.

    2) See above

    3) Both My Blog [http://blogs.msdn.com/alexj/archive/2010/01/07/data-service-providers-getting-started.aspx] and the OData SDK – DSP toolkit [http://odata.org/developers/odata-sdk] cover writing custom providers. I hear your frustration though, it is clear we need more guidance around the reflection provider, we’ll work towards that soon, in the meantime feel free to contact us again if you need help.

    4) Namespace is a pretty platform and technology agnostic thing, it is simply a way of avoiding naming conflicts while keeping simple names. The web is built on the same principle. There must be millions of about.html pages out there – simple name – disambiguated by the rest of URL.

    5) Have you seen DataService<>.HandleException(..) & DataServiceConfiguration.UseVerboseErrors which you can turn on in your InitializeService method? Both of these simplify tracking down issues in your DataService.

    Hopefully my answers to both (1) and (2) cause you to rethink your assessment. I personally have spent a lot of time writing DSPs etc, and can say that there exist a lot of opportuntities to make things even better, and the beautiful thing is that most of the interfaces and APIs are public so you / others don’t necessarily have to wait for Microsoft to take the lead.

    Please let me know if you have any more questions

    Alex James