다음을 통해 공유


Data Services Expressions – Part 7 – Navigation

Series: This post is the seventh part of the Data Services Expressions Series which describes expressions generated by WCF Data Services.

In this post we will examine how navigations are expressed by the WCF Data Services and what the provider needs to do to correctly support them.

What is navigation

In the OData model, entities can have so called “navigation” properties. These properties are references to other entities in the model. There are two types of navigation properties. Resource reference property references a single resource (entity) and can have a null value. Resource reference property has cardinality of 0..1. Resource set reference property references a collection of entities, must not be null but can evaluate to an empty collection. Resource set reference property has cardinality 0..N.

In the URL, navigations are expressed as traversal of the specified property in the path. For samples in this post we will use the Product entity we have from previous posts. A class-like description of the entities is here:

 public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public double Price { get; set; }
    public DateTime ReleaseDate { get; set; }
    public DateTime? DiscontinueDate { get; set; }
    public int Rating { get; set; }
    public Category Category { get; set; }
}

public class Category
{
    public int ID { get; set; }
    public string Name { get; set; }
    public List<Product> Products { get; set; }
}

We added a resource reference property (0..1) Category on the Product and then a resource set reference property (0..N) Products on Category. So a sample query with navigation is for example:

https://host/service.svc/Products(1)/Category

This query returns the Category for Product with ID 1. The other way round navigation like this:

https://host/services.svc/Categories(1)/Products

returns all products which belong to category with ID 1. Navigations can be chained (just like directory traversal), but it is only possible to navigate on properties of a single result. So right after a navigation over resource reference property, we can put another navigation like this:

https://host/services.svc/Products(1)/Category/Products

But if the navigation is over a resource set reference property a key lookup must be inserted to specify which entity to use and then another navigation can be used, like this:

https://host/services.svc/Categories(1)/Products(2)/Category

Resource reference property navigation

Navigating over a resource reference property means to traverse from one entity instance to another over the navigation property. As we’ve discussed in the key lookup description post, a single entity is expressed by appending a call to Where method which filters based on the key properties to the query root. So in order to navigate over a resource reference property, we need to project the value of the navigation property on the entity in question. Since the query is constructed to return multiple results (even though it never will), the projection needs to be done through a call to the Select method. So a query for /Products(1)/Category will look like:

System.Collections.Generic.List`1[TypedService.Product]

  .Where(element => (element.ID == 1))

.Select(element => element.Category)

Note that the body of the Select method is a property access to the resource reference property Category. This property access follows all the same rules as other property accesses, as described in this post. The Select method is in fact an extension method on the class Queryable. It takes two parameters, the implicit IQueryable<Product> which is the source query to project from and a lambda expression which for each Product returns the respective Category entity. All of this can be seen in the detailed view of the expression:

.Call System.Linq.Queryable.Select(

.Call System.Linq.Queryable.Where(

.Constant<System.Linq.EnumerableQuery`1[TypedService.Product]>(System.Collections.Generic.List`1[TypedService.Product]),

'(.Lambda #Lambda1<System.Func`2[TypedService.Product,System.Boolean]>)),

'(.Lambda #Lambda2<System.Func`2[TypedService.Product,TypedService.Category]>))

.Lambda #Lambda1<System.Func`2[TypedService.Product,System.Boolean]>(TypedService.Product $element) {

$element.ID == 1

}

.Lambda #Lambda2<System.Func`2[TypedService.Product,TypedService.Category]>(TypedService.Product $element) {

$element.Category

}

The provider is expected to return enumeration of Category entities from such query with a single result. If the above query returns no result or more than one result, the WCF Data Services will fail to process the request (with 500 status code). The single instance returned can be null though (the case where product has no category), in which case the response for such query will be 404 Resource Not Found. The 404 status code is the equivalent of null in the HTTP/URL world.

Resource set reference property navigation

Navigating over a resource set reference property is similar in that it must start on a single entity instance. But it returns a collection of entities as its result. Since the expression tree for the single entity is in fact an enumeration, the way to return another set of results is to perform a join. Join queries are expressed by a call to method SelectMany. So a query for /Categories(1)/Products will look like:

System.Collections.Generic.List`1[TypedService.Category]

.Where(element => (element.ID == 1))

.SelectMany(element => Convert(element.Products))

The SelectMany is again in fact an extension method on class Queryable. It takes two parameters, the first implicit one is the source query and the second is a lambda which takes the category entity and returns an enumeration of products. The result of the SelectMany call is enumeration of products, which is a concatenation of all the enumerations of products returned for all the categories in the source query. In our case the source query will always return exactly one result, so the result of the whole query is a list of products which belong to the category of ID 1.

Note that the Convert expression in the lambda converts the value of Products (which in our case is of type List<Product>) to the instance type of the navigation property which in case of resource set reference property is always IEnumerable<T>. In this particular case it is IEnumerable<Product>. Again this is in fact a property access expression.

For completeness here is the detailed view of the query:

.Call System.Linq.Queryable.SelectMany(

.Call System.Linq.Queryable.Where(

.Constant<System.Linq.EnumerableQuery`1[TypedService.Category]>(System.Collections.Generic.List`1[TypedService.Category]),

'(.Lambda #Lambda1<System.Func`2[TypedService.Category,System.Boolean]>)),

'(.Lambda #Lambda2<System.Func`2[TypedService.Category,System.Collections.Generic.IEnumerable`1[TypedService.Product]]>))

.Lambda #Lambda1<System.Func`2[TypedService.Category,System.Boolean]>(TypedService.Category $element) {

$element.ID == 1

}

.Lambda #Lambda2<System.Func`2[TypedService.Category,System.Collections.Generic.IEnumerable`1[TypedService.Product]]>(TypedService.Category $element)

{

(System.Collections.Generic.IEnumerable`1[TypedService.Product])$element.Products

}

A result of such query is expected to return enumeration of Product entities. Such enumeration can be empty or can contain multiple results.

Multiple navigations

Navigations can be chained and the expression generated for these operations are quite predictable. They are basically also results of chaining the single navigations together. So instead of describing again all the details, an example is much easier to understand. A URL query like /Products(1)/Category/Products yields an expression:

System.Collections.Generic.List`1[TypedService.Product]

.Where(element => (element.ID == 1))

.Select(element => element.Category)

.SelectMany(element => Convert(element.Products))

In this case since the first navigation is over resource reference property, the second navigation immediately follows the first one. And the second example with key lookup in the middle with URL query like /Categories(1)/Products(2)/Category yields an expression:

System.Collections.Generic.List`1[TypedService.Category]

.Where(element => (element.ID == 1))

.SelectMany(element => Convert(element.Products))

.Where(element => (element.ID == 2))

.Select(element => element.Category)

Here since the first navigation is over a resource set reference property, it must be followed by a key lookup (a filter expressed by a call to Where method) which then can be followed by the second navigation.

And that’s all there is to navigations. Since the queries in the URL can compose navigations with other operations like sorting, the expression tree generated may also contain such parts. The important thing to remember is that all the query options are applied to the end result of all the navigations.