Entity Relations in OData v4 Using ASP.NET Web API 2.2
by Mike Wasson
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 OData v4 using ASP.NET Web API. The tutorial builds on the tutorial Create an OData v4 Endpoint Using ASP.NET Web API 2.
Software versions used in the tutorial
- Web API 2.1
- OData v4
- Visual Studio 2017 (download Visual Studio 2017 here)
- Entity Framework 6
- .NET 4.5
Tutorial versions
For the OData Version 3, see Supporting Entity Relations in OData v3.
Add a Supplier Entity
Note
The tutorial builds on the tutorial Create an OData v4 Endpoint Using ASP.NET Web API 2.
First, we need a related entity. Add a class named Supplier
in the Models folder.
using System.Collections.Generic;
namespace ProductService.Models
{
public class Supplier
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Product> Products { get; set; }
}
}
Add a navigation property to the Product
class:
using System.ComponentModel.DataAnnotations.Schema;
namespace ProductService.Models
{
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 int? SupplierId { get; set; }
public virtual Supplier Supplier { get; set; }
}
}
Add a new DbSet to the ProductsContext
class, so that Entity Framework will include the Supplier table in the database.
public class ProductsContext : DbContext
{
static ProductsContext()
{
Database.SetInitializer(new ProductInitializer());
}
public DbSet<Product> Products { get; set; }
// New code:
public DbSet<Supplier> Suppliers { get; set; }
}
In WebApiConfig.cs, add a "Suppliers" entity set to the entity data model:
public static void Register(HttpConfiguration config)
{
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
// New code:
builder.EntitySet<Supplier>("Suppliers");
config.MapODataServiceRoute("ODataRoute", null, builder.GetEdmModel());
}
Add a Suppliers Controller
Add a SuppliersController
class to the Controllers folder.
using ProductService.Models;
using System.Linq;
using System.Web.OData;
namespace ProductService.Controllers
{
public class SuppliersController : ODataController
{
ProductsContext db = new ProductsContext();
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
I won't show how to add CRUD operations for this controller. The steps are the same as for the Products controller (see Create an OData v4 Endpoint).
Getting Related Entities
To get the supplier for a product, the client sends a GET request:
GET /Products(1)/Supplier
To support this request, add the following method to the ProductsController
class:
public class ProductsController : ODataController
{
// GET /Products(1)/Supplier
[EnableQuery]
public SingleResult<Supplier> GetSupplier([FromODataUri] int key)
{
var result = db.Products.Where(m => m.Id == key).Select(m => m.Supplier);
return SingleResult.Create(result);
}
// Other controller methods not shown.
}
This method uses a default naming convention
- Method name: GetX, where X is the navigation property.
- Parameter name: key
If you follow this naming convention, Web API automatically maps the HTTP request to the controller method.
Example HTTP request:
GET http://myproductservice.example.com/Products(1)/Supplier HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com
Example HTTP response:
HTTP/1.1 200 OK
Content-Length: 125
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 00:44:27 GMT
{
"@odata.context":"http://myproductservice.example.com/$metadata#Suppliers/$entity","Id":2,"Name":"Wingtip Toys"
}
Getting a related collection
In the previous example, a product has one supplier. A navigation property can also return a collection. The following code gets the products for a supplier:
public class SuppliersController : ODataController
{
// GET /Suppliers(1)/Products
[EnableQuery]
public IQueryable<Product> GetProducts([FromODataUri] int key)
{
return db.Suppliers.Where(m => m.Id.Equals(key)).SelectMany(m => m.Products);
}
// Other controller methods not shown.
}
In this case, the method returns an IQueryable instead of a SingleResult<T>
Example HTTP request:
GET http://myproductservice.example.com/Suppliers(2)/Products HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com
Example HTTP response:
HTTP/1.1 200 OK
Content-Length: 372
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 01:06:54 GMT
{
"@odata.context":"http://myproductservice.example.com/$metadata#Products","value":[
{
"Id":1,"Name":"Hat","Price":14.95,"Category":"Clothing","SupplierId":2
},{
"Id":2,"Name":"Socks","Price":6.95,"Category":"Clothing","SupplierId":2
},{
"Id":4,"Name":"Pogo Stick","Price":29.99,"Category":"Toys","SupplierId":2
}
]
}
Creating a Relationship Between Entities
OData supports creating or removing relationships between two existing entities. In OData v4 terminology, the relationship is a "reference". (In OData v3, the relationship was called a link. The protocol differences don't matter for this tutorial.)
A reference has its own URI, with the form /Entity/NavigationProperty/$ref
. For example, here is the URI to address the reference between a product and its supplier:
http:/host/Products(1)/Supplier/$ref
To add a relationship, the client sends a POST or PUT request to this address.
- PUT if the navigation property is a single entity, such as
Product.Supplier
. - POST if the navigation property is a collection, such as
Supplier.Products
.
The body of the request contains the URI of the other entity in the relation. Here is an example request:
PUT http://myproductservice.example.com/Products(6)/Supplier/$ref HTTP/1.1
OData-Version: 4.0;NetFx
OData-MaxVersion: 4.0;NetFx
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Content-Type: application/json;odata.metadata=minimal
User-Agent: Microsoft ADO.NET Data Services
Host: myproductservice.example.com
Content-Length: 70
Expect: 100-continue
{"@odata.id":"http://myproductservice.example.com/Suppliers(4)"}
In this example, the client sends a PUT request to /Products(6)/Supplier/$ref
, which is the $ref URI for the Supplier
of the product with ID = 6. If the request succeeds, the server sends a 204 (No Content) response:
HTTP/1.1 204 No Content
Server: Microsoft-IIS/8.0
Date: Tue, 08 Jul 2014 06:35:59 GMT
Here is the controller method to add a relationship to a Product
:
public class ProductsController : ODataController
{
[AcceptVerbs("POST", "PUT")]
public async Task<IHttpActionResult> CreateRef([FromODataUri] int key,
string navigationProperty, [FromBody] Uri link)
{
var product = await db.Products.SingleOrDefaultAsync(p => p.Id == key);
if (product == null)
{
return NotFound();
}
switch (navigationProperty)
{
case "Supplier":
// Note: The code for GetKeyFromUri is shown later in this topic.
var relatedKey = Helpers.GetKeyFromUri<int>(Request, link);
var supplier = await db.Suppliers.SingleOrDefaultAsync(f => f.Id == relatedKey);
if (supplier == null)
{
return NotFound();
}
product.Supplier = supplier;
break;
default:
return StatusCode(HttpStatusCode.NotImplemented);
}
await db.SaveChangesAsync();
return StatusCode(HttpStatusCode.NoContent);
}
// Other controller methods not shown.
}
The navigationProperty parameter specifies which relationship to set. (If there is more than one navigation property on the entity, you can add more case
statements.)
The link parameter contains the URI of the supplier. Web API automatically parses the request body to get the value for this parameter.
To look up the supplier, we need the ID (or key), which is part of the link parameter. To do this, use the following helper method:
using Microsoft.OData.Core;
using Microsoft.OData.Core.UriParser;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Routing;
using System.Web.OData.Extensions;
using System.Web.OData.Routing;
namespace ProductService
{
public static class Helpers
{
public static TKey GetKeyFromUri<TKey>(HttpRequestMessage request, Uri uri)
{
if (uri == null)
{
throw new ArgumentNullException("uri");
}
var urlHelper = request.GetUrlHelper() ?? new UrlHelper(request);
string serviceRoot = urlHelper.CreateODataLink(
request.ODataProperties().RouteName,
request.ODataProperties().PathHandler, new List<ODataPathSegment>());
var odataPath = request.ODataProperties().PathHandler.Parse(
request.ODataProperties().Model,
serviceRoot, uri.LocalPath);
var keySegment = odataPath.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
if (keySegment == null)
{
throw new InvalidOperationException("The link does not contain a key.");
}
var value = ODataUriUtils.ConvertFromUriLiteral(keySegment.Value, ODataVersion.V4);
return (TKey)value;
}
}
}
Basically, this method uses the OData library to split the URI path into segments, find the segment that contains the key, and convert the key into the correct type.
Deleting a Relationship Between Entities
To delete a relationship, the client sends an HTTP DELETE request to the $ref URI:
DELETE http://host/Products(1)/Supplier/$ref
Here is the controller method to delete the relationship between a Product and a Supplier:
public class ProductsController : ODataController
{
public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key,
string navigationProperty, [FromBody] Uri link)
{
var product = db.Products.SingleOrDefault(p => p.Id == key);
if (product == null)
{
return NotFound();
}
switch (navigationProperty)
{
case "Supplier":
product.Supplier = null;
break;
default:
return StatusCode(HttpStatusCode.NotImplemented);
}
await db.SaveChangesAsync();
return StatusCode(HttpStatusCode.NoContent);
}
// Other controller methods not shown.
}
In this case, Product.Supplier
is the "1" end of a 1-to-many relation, so you can remove the relationship just by setting Product.Supplier
to null
.
In the "many" end of a relationship, the client must specify which related entity to remove. To do so, the client sends the URI of the related entity in the query string of the request. For example, to remove "Product 1" from "Supplier 1":
DELETE http://host/Suppliers(1)/Products/$ref?$id=http://host/Products(1)
To support this in Web API, we need to include an extra parameter in the DeleteRef
method. Here is the controller method to remove a product from the Supplier.Products
relation.
public class SuppliersController : ODataController
{
public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key,
[FromODataUri] string relatedKey, string navigationProperty)
{
var supplier = await db.Suppliers.SingleOrDefaultAsync(p => p.Id == key);
if (supplier == null)
{
return StatusCode(HttpStatusCode.NotFound);
}
switch (navigationProperty)
{
case "Products":
var productId = Convert.ToInt32(relatedKey);
var product = await db.Products.SingleOrDefaultAsync(p => p.Id == productId);
if (product == null)
{
return NotFound();
}
product.Supplier = null;
break;
default:
return StatusCode(HttpStatusCode.NotImplemented);
}
await db.SaveChangesAsync();
return StatusCode(HttpStatusCode.NoContent);
}
// Other controller methods not shown.
}
The key parameter is the key for the supplier, and the relatedKey parameter is the key for the product to remove from the Products
relationship. Note that Web API automatically gets the key from the query string.