Containment In OData V4 Using Web API

Introduction

Up to OData V3, Entity sets must be accessed from the top level even if they are in a containment relationship with other entity sets.

For example I have two entity sets: Employee and EmployeeAddress, Employee has employeeId that is a key member and EmployeeAddress has also EmployeeAddress. There are two issues in this design.

  • Container has redundant information e.g. employeeId is repeated.
  • Entity is accessible through top level entity set.

To resolve this issue, containment property is introduced in OData V4. We can define an implicit entity set for every instance of its declaring entity type using containment. The redundant information issue is resolved now. In this article, we will learn how to define containment in an OData endpoint in Web API 2.2. We can define Containment navigation property using the "Contained" attribute.

An employee may have many addresses but it is not required to define entity set for the Address. The Address can only be accessed through an employee.

Definition of data model

  1. namespace WebAPITest  
  2. {  
  3. using System.Collections.Generic;  
  4. using System.ComponentModel.DataAnnotations;  
  5. using System.ComponentModel.DataAnnotations.Schema;  
  6. using System.Web.OData.Builder;  
  7.   
  8. public partial class Employee  
  9. {  
  10. [Key]  
  11. public int Id { getset; }  
  12.   
  13. public string Name { getset; }  
  14.   
  15. public int DepartmentId { getset; }  
  16.   
  17. public decimal? Salary { getset; }  
  18. [Contained]  
  19. public IList<EmployeeAddress> Addresses { getset; }   
  20. }  
  21.   
  22. public partial class EmployeeAddress  
  23. {  
  24. [Key]  
  25. public int AddressId { getset; }  
  26.   
  27. public string Address { getset; }  
  28. }  
  29. }  
Following is a web API configuration. If the Contained attribute found ODataConventionModelBuilder is assed with corresponding navigation property to entity model. Here I have created GetCount method because employee address is collection type.
  1. ODataConventionModelBuilder builder = new ODataConventionModelBuilder();  
  2. builder.Namespace = "WebAPITest";  
  3. builder.ContainerName = "DefaultContainer";  
  4. builder.EntitySet<Employee>("Employee");  
  5.   
  6. var employeeAddressType = builder.EntityType<EmployeeAddress>();  
  7. var functionConfiguration =  
  8. employeeAddressType.Collection.Function("GetCount");  
  9. functionConfiguration.Parameter<string>("NameContains");  
  10. functionConfiguration.Returns<int>();   
Following is a snap of Meta data. The employee entity type contains the ContainsTarget attribute that indicates navigation property is containment.

1

Entity which is marked with “Contained” attribute does not have its own controller. It can only be accessible from the containing entity set controller. In this example, I have created EmployeeController but not created EmployeeAddressController.

In this example, I have defined EmployeeController which contains the methods to perform the CRUD operation on “Contained” entity (EmployeeAddress).
  1. namespace WebAPITest.Controllers  
  2. {  
  3. using System.Collections.Generic;  
  4. using System.Linq;  
  5. using System.Net;  
  6. using System.Net.Http;  
  7. using System.Web.Http;  
  8. using System.Web.OData;  
  9. using System.Web.OData.Routing;  
  10. public class EmployeeController : ODataController  
  11. {  
  12. IList<Employee> employees = null;  
  13. public EmployeeController()  
  14. {  
  15. if(employees == null)  
  16. {  
  17. employees = InitEmployee();  
  18. }  
  19. }  
  20.   
  21. [EnableQuery]  
  22. public IQueryable<Employee> Get()  
  23. {  
  24. return employees.AsQueryable();  
  25. }  
  26.   
  27. /// <summary>  
  28. /// Read Operation  
  29. /// </summary>  
  30. /// <param name="key">Key</param>  
  31. /// <returns></returns>  
  32. public HttpResponseMessage Get([FromODataUri]int key)  
  33. {  
  34. Employee data = employees.Where(k => k.Id == key).FirstOrDefault();  
  35. if (data == null)  
  36. {  
  37. return Request.CreateResponse(HttpStatusCode.NotFound);  
  38. }  
  39.   
  40. return Request.CreateResponse(HttpStatusCode.OK, data);  
  41. }  
  42.   
  43. /// <summary>  
  44. /// Get Employee Address by employeeId  
  45. /// </summary>  
  46. /// <param name="key">employeeId</param>  
  47. /// <returns></returns>  
  48. [EnableQuery]  
  49. [ODataRoute("Employee({employeeId})/Addresses")]  
  50. public IHttpActionResult GetAddresses(int employeeId)  
  51. {  
  52. var address = employees.Single(a => a.Id == employeeId).Addresses;  
  53. return Ok(address);  
  54. }  
  55.   
  56. /// <summary>  
  57. /// Get Employee Address by employee id and address id  
  58. /// </summary>  
  59. /// <param name="employeeId">employeeId</param>  
  60. /// <param name="addressId">addressId</param>  
  61. /// <returns></returns>  
  62. [EnableQuery]  
  63. [ODataRoute("Employee({employeeId})/Addresses({addressId})")]  
  64. public IHttpActionResult GetSingleAddress(int employeeId, int addressId)  
  65. {  
  66. var addresses = employees.Single(a => a.Id == employeeId).Addresses;  
  67. var address = addresses.Single(s => s.AddressId == addressId);  
  68. return Ok(address);  
  69. }  
  70.   
  71. /// <summary>  
  72. /// Update Employee Address based on employee id and address id  
  73. /// PUT ~/Employee(1)/Addresses(101)  
  74. /// </summary>  
  75. /// <param name="employeeId">employee id</param>  
  76. /// <param name="addressId">address id</param>  
  77. /// <param name="employeeAddress">Address entity</param>  
  78. /// <returns></returns>  
  79. [ODataRoute("Employee({employeeId})/Addresses({addressId})")]  
  80. public IHttpActionResult PutToAddress(int employeeId, int addressId, [FromBody]EmployeeAddress employeeAddress)  
  81. {  
  82. var employee = employees.Single(a =>a.Id == employeeId);  
  83. var empAddress = employee.Addresses.Single(p => p.AddressId == addressId);  
  84. empAddress.Address = employeeAddress.Address;  
  85. return Ok(employeeAddress);  
  86. }  
  87.   
  88. /// <summary>  
  89. /// Delete Employee address  
  90. /// DELETE ~/Employee(1)/Addresses(101)   
  91. /// </summary>  
  92. /// <param name="employeeId"><employeeId</param>  
  93. /// <param name="addressId">addressId</param>  
  94. /// <returns></returns>  
  95. [ODataRoute("Employee({employeeId})/Addresses({addressId})")]  
  96. public IHttpActionResult DeleteAddressFromEmployee(int employeeId, int addressId)  
  97. {  
  98. var emp = employees.Single(a =>a.Id == employeeId);  
  99. var address = emp.Addresses.Single(p => p.AddressId == addressId);  
  100. if (emp.Addresses.Remove(address))  
  101. {  
  102. return StatusCode(HttpStatusCode.NoContent);  
  103. }  
  104. else  
  105. {  
  106. return StatusCode(HttpStatusCode.InternalServerError);  
  107. }  
  108. }  
  109.   
  110. /// <summary>  
  111. /// Get count of employee address by employeeId and address  
  112. /// GET ~/Employee(1)/Addresses/Namespace.GetCount()   
  113. /// </summary>  
  114. /// <param name="employeeId"></param>  
  115. /// <param name="name"></param>  
  116. /// <returns></returns>  
  117. [ODataRoute("Employee({employeeId})/Addresses/WebAPITest.GetCount(NameContains={partialAddress})")]  
  118. public IHttpActionResult GetAddressCountWhoseNameContainsGivenValue(int employeeId, [FromODataUri]string partialAddress)  
  119. {  
  120. var account = employees.Single(a =>a.Id == employeeId);  
  121. var count = account.Addresses.Where(s => s.Address.Contains(partialAddress)).Count();  
  122. return Ok(count);  
  123. }   
  124.   
  125. /// <summary>  
  126. /// Static Data source.  
  127. /// </summary>  
  128. /// <returns>IList<Employee></returns>  
  129. private static IList<Employee> InitEmployee()  
  130. {  
  131. var Employees = new List<Employee>()   
  132. {   
  133. new Employee()   
  134. {   
  135. Id = 1,   
  136. Name="Jignesh Trivedi",   
  137. Addresses = new List<EmployeeAddress>()   
  138. {   
  139. new EmployeeAddress()   
  140. {   
  141. AddressId = 101,   
  142. Address = "Test Address",   
  143. },   
  144. new EmployeeAddress()   
  145. {   
  146. AddressId = 102,   
  147. Address = "Test Address",   
  148. },   
  149. },   
  150. },  
  151. new Employee()   
  152. {   
  153. Id = 2,   
  154. Name="Rakesh Trivedi",   
  155. Addresses = new List<EmployeeAddress>()   
  156. {   
  157. new EmployeeAddress()   
  158. {   
  159. AddressId = 103,   
  160. Address = "Test Address",   
  161. },   
  162. new EmployeeAddress()   
  163. {   
  164. AddressId = 104,   
  165. Address = "Test Address",   
  166. },   
  167. },   
  168. },   
  169. };  
  170. return Employees;  
  171. }   
  172. }  
  173. }  
Here, I have defined route using attribute. The reason is the OData path only works up to 4 segments -- for attribute and conventional routing, both are worked with for a small URI.

Summary

Containment allows the client to access the child entity by using parent entity. The client does not direct access of the child entity. Query parameters support querying both parent and child records.

Up Next
    Ebook Download
    View all
    Learn
    View all