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
- namespace WebAPITest
- {
- using System.Collections.Generic;
- using System.ComponentModel.DataAnnotations;
- using System.ComponentModel.DataAnnotations.Schema;
- using System.Web.OData.Builder;
- public partial class Employee
- {
- [Key]
- public int Id { get; set; }
- public string Name { get; set; }
- public int DepartmentId { get; set; }
- public decimal? Salary { get; set; }
- [Contained]
- public IList<EmployeeAddress> Addresses { get; set; }
- }
- public partial class EmployeeAddress
- {
- [Key]
- public int AddressId { get; set; }
- public string Address { get; set; }
- }
- }
- ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
- builder.Namespace = "WebAPITest";
- builder.ContainerName = "DefaultContainer";
- builder.EntitySet<Employee>("Employee");
- var employeeAddressType = builder.EntityType<EmployeeAddress>();
- var functionConfiguration =
- employeeAddressType.Collection.Function("GetCount");
- functionConfiguration.Parameter<string>("NameContains");
- functionConfiguration.Returns<int>();

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).
- namespace WebAPITest.Controllers
- {
- using System.Collections.Generic;
- using System.Linq;
- using System.Net;
- using System.Net.Http;
- using System.Web.Http;
- using System.Web.OData;
- using System.Web.OData.Routing;
- public class EmployeeController : ODataController
- {
- IList<Employee> employees = null;
- public EmployeeController()
- {
- if(employees == null)
- {
- employees = InitEmployee();
- }
- }
- [EnableQuery]
- public IQueryable<Employee> Get()
- {
- return employees.AsQueryable();
- }
- /// <summary>
- /// Read Operation
- /// </summary>
- /// <param name="key">Key</param>
- /// <returns></returns>
- public HttpResponseMessage Get([FromODataUri]int key)
- {
- Employee data = employees.Where(k => k.Id == key).FirstOrDefault();
- if (data == null)
- {
- return Request.CreateResponse(HttpStatusCode.NotFound);
- }
- return Request.CreateResponse(HttpStatusCode.OK, data);
- }
- /// <summary>
- /// Get Employee Address by employeeId
- /// </summary>
- /// <param name="key">employeeId</param>
- /// <returns></returns>
- [EnableQuery]
- [ODataRoute("Employee({employeeId})/Addresses")]
- public IHttpActionResult GetAddresses(int employeeId)
- {
- var address = employees.Single(a => a.Id == employeeId).Addresses;
- return Ok(address);
- }
- /// <summary>
- /// Get Employee Address by employee id and address id
- /// </summary>
- /// <param name="employeeId">employeeId</param>
- /// <param name="addressId">addressId</param>
- /// <returns></returns>
- [EnableQuery]
- [ODataRoute("Employee({employeeId})/Addresses({addressId})")]
- public IHttpActionResult GetSingleAddress(int employeeId, int addressId)
- {
- var addresses = employees.Single(a => a.Id == employeeId).Addresses;
- var address = addresses.Single(s => s.AddressId == addressId);
- return Ok(address);
- }
- /// <summary>
- /// Update Employee Address based on employee id and address id
- /// PUT ~/Employee(1)/Addresses(101)
- /// </summary>
- /// <param name="employeeId">employee id</param>
- /// <param name="addressId">address id</param>
- /// <param name="employeeAddress">Address entity</param>
- /// <returns></returns>
- [ODataRoute("Employee({employeeId})/Addresses({addressId})")]
- public IHttpActionResult PutToAddress(int employeeId, int addressId, [FromBody]EmployeeAddress employeeAddress)
- {
- var employee = employees.Single(a =>a.Id == employeeId);
- var empAddress = employee.Addresses.Single(p => p.AddressId == addressId);
- empAddress.Address = employeeAddress.Address;
- return Ok(employeeAddress);
- }
- /// <summary>
- /// Delete Employee address
- /// DELETE ~/Employee(1)/Addresses(101)
- /// </summary>
- /// <param name="employeeId"><employeeId</param>
- /// <param name="addressId">addressId</param>
- /// <returns></returns>
- [ODataRoute("Employee({employeeId})/Addresses({addressId})")]
- public IHttpActionResult DeleteAddressFromEmployee(int employeeId, int addressId)
- {
- var emp = employees.Single(a =>a.Id == employeeId);
- var address = emp.Addresses.Single(p => p.AddressId == addressId);
- if (emp.Addresses.Remove(address))
- {
- return StatusCode(HttpStatusCode.NoContent);
- }
- else
- {
- return StatusCode(HttpStatusCode.InternalServerError);
- }
- }
- /// <summary>
- /// Get count of employee address by employeeId and address
- /// GET ~/Employee(1)/Addresses/Namespace.GetCount()
- /// </summary>
- /// <param name="employeeId"></param>
- /// <param name="name"></param>
- /// <returns></returns>
- [ODataRoute("Employee({employeeId})/Addresses/WebAPITest.GetCount(NameContains={partialAddress})")]
- public IHttpActionResult GetAddressCountWhoseNameContainsGivenValue(int employeeId, [FromODataUri]string partialAddress)
- {
- var account = employees.Single(a =>a.Id == employeeId);
- var count = account.Addresses.Where(s => s.Address.Contains(partialAddress)).Count();
- return Ok(count);
- }
- /// <summary>
- /// Static Data source.
- /// </summary>
- /// <returns>IList<Employee></returns>
- private static IList<Employee> InitEmployee()
- {
- var Employees = new List<Employee>()
- {
- new Employee()
- {
- Id = 1,
- Name="Jignesh Trivedi",
- Addresses = new List<EmployeeAddress>()
- {
- new EmployeeAddress()
- {
- AddressId = 101,
- Address = "Test Address",
- },
- new EmployeeAddress()
- {
- AddressId = 102,
- Address = "Test Address",
- },
- },
- },
- new Employee()
- {
- Id = 2,
- Name="Rakesh Trivedi",
- Addresses = new List<EmployeeAddress>()
- {
- new EmployeeAddress()
- {
- AddressId = 103,
- Address = "Test Address",
- },
- new EmployeeAddress()
- {
- AddressId = 104,
- Address = "Test Address",
- },
- },
- },
- };
- return Employees;
- }
- }
- }
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.

Shimmy WeitzhandlerPosted Mar 29, 2020, 4:11 PM
Thanks for sharing!
Kuppurasu NagarajPosted May 17, 2016, 11:33 AM
Nice sharing..
Prasanna MuraliPosted May 16, 2016, 10:05 AM
Nice one...
Debasis SahaPosted May 16, 2016, 1:00 AM
Nice one..