Language

Working with Entity Relations in OData

By |

Most data sets define relations between entities: Customers have orders; books have authors; products have suppliers. Using OData, clients can navigate over entity relations. Given a product, you can find the supplier. You can also create or remove relationships. For example, you can set the supplier for a product.

This tutorial shows how to support these operations in ASP.NET Web API. The tutorial builds on the previous tutorial, Supporting OData CRUD Operations in ASP.NET Web API.

Download the completed project.

Add a Supplier Entity

First we need to add a new entity type to our OData feed. We'll add a Supplier class.

public class Supplier
{
    [Key]
    public string Key { get; set; }
    public string Name { get; set; }
}

This class uses a string for the entity key. In practice, that might be less common than using an integer key. But it’s worth seeing how OData handles other key types besides integers.

Next, we'll create a relation by adding a Supplier property to the Product class:

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }

    // New code
    [ForeignKey("Supplier")]
    public string SupplierId { get; set; }
    public virtual Supplier Supplier { get; set; }
}

Then add a new DbSet to the ProductsContext class, so that Entity Framework will include the Supplier table in the database.

public class ProductsContext : DbContext
{
    // ....

    public DbSet<Product> Products { get; set; }
    // New code
    public DbSet<Supplier> Suppliers { get; set; }
}

You can also add code in ProductsContextInitializer to seed the data base with some suppliers. I won’t show all of the code for that.

In WebApiConfig.cs, add a "Suppliers" entity to the EDM model:

ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Product>("Products");
// New code:
modelBuilder.EntitySet<Product>("Suppliers");

Navigation Properties

To get the supplier for a product, the client sends a GET request:

GET /Products(1)/Supplier

Here “Supplier” is a navigation property on the Product type. In this case, Supplier refers to a single item, but a navigation property can also return a collection (one-to-many or many-to-many relation).

To support this request, add the following method to the ProductsController class:

// GET /Products(1)/Supplier
public Supplier GetSupplier([FromODataUri] int key)
{
    Product product = _context.Products.FirstOrDefault(p => p.ID == key);
    if (product == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    return product.Supplier;
}

The key parameter is the key of the product. The method returns the related entity—in this case, a Supplier instance. The method name and parameter name are both important. In general, if the navigation property is named “X”, you need to add a method named “GetX”. The method must take a parameter named “key” that matches the data type of the parent’s key.

It is also important to include the [FromOdataUri] attribute in the key parameter. This attribute tells Web API to use OData syntax rules when it parses the key from the request URI.

Creating and Deleting Links

OData supports creating or removing relationships between two entities. In OData terminology, the relationship is a “link.” Each link has a URI with the form entity/$links/entity. For example, the link from product to supplier looks like this:

/Products(1)/$links/Supplier

To create a new link, the client sends a POST request to the link URI. The body of the request is the URI of the target entity. For example, suppose there is a supplier with the key “CTSO”. To create a link from “Product(1)” to “Supplier('CTSO')”, the client sends a request like the following:

POST http://localhost/odata/Products(1)/$links/Supplier
Content-Type: application/json
Content-Length: 50

{"url":"http://localhost/odata/Suppliers('CTSO')"}

To delete a link, the client sends a DELETE request to the link URI.

Creating Links

To enable a client to create product-supplier links, add the following code to the ProductsController class:

public override void CreateLink([FromODataUri] int key, string navigationProperty, 
    [FromBody] Uri link)
{
    Product product = _context.Products.FirstOrDefault(p => p.ID == key);
    if (product == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    switch (navigationProperty)
    {
        case "Supplier":
            string supplierKey = GetKeyFromLinkUri<string>(link);
            Supplier supplier = _context.Suppliers.
                SingleOrDefault(s => s.Key == supplierKey);
            if (supplier == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            product.Supplier = supplier;
            _context.SaveChanges();
            break;

        default:
            base.CreateLink(key, navigationProperty, link);
            break;
    }
}

This code overrides the EntitySetController.CreateLink method. The method takes three parameters:

  • key: The key to the parent entity (the product)
  • navigationProperty: The name of the navigation property. In this example, the only valid navigation property is “Supplier”.
  • link: The OData URI of the related entity. This value is taken from the request body. For example, the link URI might be “http://localhost/odata/Suppliers('CTSO'), meaning the supplier with ID = ‘CTSO’.

The method uses the link to look up the supplier. If the matching supplier is found, the method sets the Product.Supplier property and saves the result to the database.

The hardest part is parsing the link URI. Basically, you need to simulate the result of sending a GET request to that URI. The following helper method shows how to do this. The method invokes the Web API routing process and gets back an ODataPath instance that represents the parsed OData path. For a link URI, one of the segments should be the entity key. (If not, the client sent a bad URI.)

// Helper method to extract the key from an OData link URI.
private TKey GetKeyFromLinkUri<TKey>(Uri link)
{
    TKey key = default(TKey);

    // Get the route that was used for this request.
    IHttpRoute route = Request.GetRouteData().Route;

    // Create an equivalent self-hosted route. 
    IHttpRoute newRoute = new HttpRoute(route.RouteTemplate, 
        new HttpRouteValueDictionary(route.Defaults), 
        new HttpRouteValueDictionary(route.Constraints),
        new HttpRouteValueDictionary(route.DataTokens), route.Handler);

    // Create a fake GET request for the link URI.
    var tmpRequest = new HttpRequestMessage(HttpMethod.Get, link);

    // Send this request through the routing process.
    var routeData = newRoute.GetRouteData(
        Request.GetConfiguration().VirtualPathRoot, tmpRequest);

    // If the GET request matches the route, use the path segments to find the key.
    if (routeData != null)
    {
        ODataPath path = tmpRequest.GetODataPath();
        var segment = path.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
        if (segment != null)
        {
            // Convert the segment into the key type.
            key = (TKey)ODataUriUtils.ConvertFromUriLiteral(
                segment.Value, ODataVersion.V3);
        }
    }
    return key;
}

Deleting Links

To delete a link, override the EntitySetController.DeleteLink method:

public override void DeleteLink([FromODataUri] int key, string navigationProperty, 
    [FromBody] Uri link)
{
    Product product = _context.Products.FirstOrDefault(p => p.ID == key);
    if (product == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    switch (navigationProperty)
    {
        case "Supplier":
            product.Supplier = null;
            break;

        default:
            base.DeleteLink(key, navigationProperty, link);
            break;

    }
    _context.SaveChanges();
}

The DeleteLink method has two overloads:

void DeleteLink([FromODataUri] int key, string navigationProperty, [FromBody] Uri link);

void DeleteLink([FromODataUri] int key, string relatedKey, string navigationProperty);

The first version is used when the navigation property points to a single entity. For example: /odata/Products(1)/$links/Supplier.

The second version is used when the navigation property points to a collection. In that case, the link URI includes a key that selects the related entity. For example: /odata/Customers(1)/$links/Orders(1).

Mike Wasson

By Mike Wasson, Mike Wasson is a programmer-writer at Microsoft.

Table of Contents

Getting Started with ASP.NET Web API

Creating Web APIs

Web API Clients

Web API Routing and Actions

Working with HTTP

Formats and Model Binding

OData Support in ASP.NET Web API

Security

Hosting ASP.NET Web API

Testing and Debugging

Extensibility

Resources