diff --git a/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Common/ODataAnnotatableExtensions.cs b/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Common/ODataAnnotatableExtensions.cs
new file mode 100644
index 0000000000..77a31202a1
--- /dev/null
+++ b/test/EndToEndTests/Common/Microsoft.OData.Client.E2E.TestCommon/Common/ODataAnnotatableExtensions.cs
@@ -0,0 +1,51 @@
+//---------------------------------------------------------------------
+//
+// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
+//
+//---------------------------------------------------------------------
+
+using System.Collections.Concurrent;
+using System.Diagnostics;
+
+namespace Microsoft.OData.Client.E2E.TestCommon.Common;
+
+public static class ODataAnnotatableExtensions
+{
+ public static void SetAnnotation(this ODataAnnotatable annotatable, T annotation)
+ where T : class
+ {
+ Debug.Assert(annotatable != null, "annotatable != null");
+ Debug.Assert(annotation != null, "annotation != null");
+
+ InternalDictionary.SetAnnotation(annotatable, annotation);
+ }
+
+ public static T GetAnnotation(this ODataAnnotatable annotatable)
+ where T : class
+ {
+ Debug.Assert(annotatable != null, "annotatable != null");
+
+ return InternalDictionary.GetAnnotation(annotatable);
+ }
+
+ private static class InternalDictionary where T : class
+ {
+ private static readonly ConcurrentDictionary Dictionary =
+ new ConcurrentDictionary();
+
+ public static void SetAnnotation(ODataAnnotatable annotatable, T annotation)
+ {
+ Dictionary[annotatable] = annotation;
+ }
+
+ public static T GetAnnotation(ODataAnnotatable annotatable)
+ {
+ if (Dictionary.TryGetValue(annotatable, out T? annotation))
+ {
+ return annotation;
+ }
+
+ return default(T);
+ }
+ }
+}
diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/Default/DefaultContainer.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/Default/DefaultContainer.cs
index 0389db9d40..a297bdba77 100644
--- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/Default/DefaultContainer.cs
+++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/Default/DefaultContainer.cs
@@ -7305,7 +7305,11 @@ private abstract class GeneratedEdmModel
try
{
var assembly = global::System.Reflection.Assembly.GetExecutingAssembly();
- var resourcePath = global::System.Linq.Enumerable.Single(assembly.GetManifestResourceNames(), str => str.EndsWith(filePath));
+ // If multiple resource names end with the file name, select the shortest one.
+ var resourcePath = global::System.Linq.Enumerable.First(
+ global::System.Linq.Enumerable.OrderBy(
+ global::System.Linq.Enumerable.Where(assembly.GetManifestResourceNames(), name => name.EndsWith(filePath)),
+ filteredName => filteredName.Length));
global::System.IO.Stream stream = assembly.GetManifestResourceStream(resourcePath);
return global::System.Xml.XmlReader.Create(new global::System.IO.StreamReader(stream));
}
@@ -7402,6 +7406,20 @@ private abstract class GeneratedEdmModel
///
public static class ExtensionMethods
{
+ ///
+ /// There are no comments for GetEmployeesCount in the schema.
+ ///
+ [global::Microsoft.OData.Client.OriginalNameAttribute("GetEmployeesCount")]
+ public static global::Microsoft.OData.Client.DataServiceQuerySingle GetEmployeesCount(this global::Microsoft.OData.Client.DataServiceQuerySingle _source)
+ {
+ if (!_source.IsComposable)
+ {
+ throw new global::System.NotSupportedException("The previous function is not composable.");
+ }
+
+ return _source.CreateFunctionQuerySingle("Default.GetEmployeesCount", false);
+ }
+
///
/// There are no comments for GetProductDetails in the schema.
///
diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/Default/DefaultServiceCsdl.xml b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/Default/DefaultServiceCsdl.xml
index e01225d970..7180d19f66 100644
--- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/Default/DefaultServiceCsdl.xml
+++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/Default/DefaultServiceCsdl.xml
@@ -355,6 +355,10 @@
+
+
+
+
@@ -492,7 +496,8 @@
-
+
+
diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/Default/DefaultEdmModel.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/Default/DefaultEdmModel.cs
index 87a6e805f2..1e825e4387 100644
--- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/Default/DefaultEdmModel.cs
+++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/Default/DefaultEdmModel.cs
@@ -29,8 +29,8 @@ public static IEdmModel GetEdmModel()
builder.EntitySet("OrderDetails");
builder.EntitySet("Departments");
builder.Singleton("Company");
- builder.Singleton("PublicCompany");
- builder.Singleton("LabourUnion");
+ builder.Singleton("PublicCompany").HasSingletonBinding((PublicCompany p) => p.LabourUnion, "LabourUnion");
+ // builder.Singleton("LabourUnion");
builder.EntitySet("Accounts");
builder.EntitySet("Orders");
builder.EntitySet("PaymentInstruments");
@@ -95,6 +95,10 @@ public static IEdmModel GetEdmModel()
builder.Action("ResetDefaultDataSource");
+ builder.EntityType()
+ .Function("GetEmployeesCount")
+ .Returns();
+
builder.EntityType()
.Function("GetProductDetails")
.ReturnsCollectionViaEntitySetPath("bindingParameter/Details")
diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Server/SingletonClientTestsController.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Server/SingletonClientTestsController.cs
new file mode 100644
index 0000000000..b9322ca5e4
--- /dev/null
+++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Server/SingletonClientTestsController.cs
@@ -0,0 +1,610 @@
+//---------------------------------------------------------------------
+//
+// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
+//
+//---------------------------------------------------------------------
+
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.OData.Deltas;
+using Microsoft.AspNetCore.OData.Formatter;
+using Microsoft.AspNetCore.OData.Query;
+using Microsoft.AspNetCore.OData.Routing.Controllers;
+using Microsoft.OData.Client.E2E.Tests.Common.Server.Default;
+
+namespace Microsoft.OData.Client.E2E.Tests.SingletonTests.Server;
+
+public class SingletonClientTestsController : ODataController
+{
+ private static DefaultDataSource _dataSource;
+
+
+ #region odata/VipCustomer
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer")]
+ public IActionResult GetVipCustomer()
+ {
+ var result = _dataSource.VipCustomer;
+
+ return Ok(result);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer/PersonID")]
+ public IActionResult GetVipCustomerPersonID()
+ {
+ var result = _dataSource.VipCustomer;
+
+ return Ok(result?.PersonID);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer/Orders")]
+ public IActionResult GetVipCustomerOrders()
+ {
+ var result = _dataSource.VipCustomer;
+
+ return Ok(result?.Orders);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer/Orders({key})/OrderDate")]
+ public IActionResult GetVipCustomerOrderOrderDate([FromRoute] int key)
+ {
+ var result = _dataSource.VipCustomer?.Orders?.SingleOrDefault(a => a.OrderID == key);
+
+ return Ok(result?.OrderDate);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer/HomeAddress")]
+ public IActionResult GetVipCustomerHomeAddress()
+ {
+ var result = _dataSource.VipCustomer;
+
+ return Ok(result?.HomeAddress);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer/HomeAddress/City")]
+ public IActionResult GetVipCustomerHomeAddressCity()
+ {
+ var result = _dataSource.VipCustomer;
+
+ return Ok(result?.HomeAddress?.City);
+ }
+
+ [HttpPatch("odata/VipCustomer")]
+ public IActionResult UpdateVipCustomer([FromBody] Delta delta)
+ {
+ var customer = _dataSource.VipCustomer;
+ if (customer == null)
+ {
+ return NotFound();
+ }
+
+ var updatedResult = delta.Patch(customer);
+ return Updated(updatedResult);
+ }
+
+ #endregion
+
+ #region odata/Company
+
+ [EnableQuery]
+ [HttpGet("odata/Company")]
+ public IActionResult GetCompany()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/Name")]
+ public IActionResult GetCompanyName()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.Name);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/CompanyCategory")]
+ public IActionResult GetCompanyCompanyCategory()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.CompanyCategory);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/VipCustomer")]
+ public IActionResult GetCompanyVipCustomer()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.VipCustomer);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/Departments")]
+ public IActionResult GetCompanyDepartments()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.Departments);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/Revenue")]
+ public IActionResult GetRevenue()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.Revenue);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/CoreDepartment")]
+ public IActionResult GetCompanyCoreDepartment()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.CoreDepartment);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/Address/City")]
+ public IActionResult GetCompanyAddressCity()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.Address?.City);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/Default.GetEmployeesCount")]
+ public IActionResult GetEmployeesCount()
+ {
+ var result = _dataSource.Company;
+
+ if (result == null)
+ {
+ return NotFound();
+ }
+
+ return Ok(result.Employees?.Count);
+ }
+
+ [EnableQuery]
+ [HttpPost("odata/Company/Default.IncreaseRevenue")]
+ public IActionResult IncreaseRevenue([FromODataBody] int IncreaseValue)
+ {
+ var result = _dataSource.Company;
+
+ if (result == null)
+ {
+ return NotFound();
+ }
+
+ result.Revenue += IncreaseValue;
+
+ return Ok(result.Revenue);
+ }
+
+ [HttpPost("odata/Company/Departments/$ref")]
+ public IActionResult AddDepartmentRefToCompany([FromBody] Uri departmentUri)
+ {
+ if (departmentUri == null)
+ {
+ return BadRequest();
+ }
+
+ // Extract the department ID from the URI
+ var lastSegment = departmentUri.Segments.Last();
+ var departmentId = int.Parse(Regex.Match(lastSegment, @"\d+").Value);
+
+ // Find the department by ID
+ var department = _dataSource.Departments?.SingleOrDefault(d => d.DepartmentID == departmentId);
+ if (department == null)
+ {
+ return NotFound();
+ }
+
+ // Add the department reference to the company
+ var company = _dataSource.Company;
+ if (company == null)
+ {
+ return NotFound();
+ }
+
+ company.Departments ??= [];
+ company.Departments.Add(department);
+
+ return Ok(department);
+ }
+
+ [HttpPut("odata/company")]
+ public IActionResult UpdateCompany([FromBody] Company company)
+ {
+ var companyToUpdate = _dataSource.Company;
+ if (companyToUpdate == null)
+ {
+ return NotFound();
+ }
+
+ companyToUpdate.CompanyID = company.CompanyID == 0 ? companyToUpdate.CompanyID : company.CompanyID;
+ companyToUpdate.Address = company.Address ?? companyToUpdate.Address;
+ companyToUpdate.CompanyCategory = company.CompanyCategory;
+ companyToUpdate.Name = company.Name ?? companyToUpdate.Name;
+ companyToUpdate.Employees = company.Employees ?? companyToUpdate.Employees;
+ companyToUpdate.Revenue = company.Revenue;
+ companyToUpdate.CoreDepartment = company.CoreDepartment ?? companyToUpdate.CoreDepartment;
+ companyToUpdate.VipCustomer = company.VipCustomer ?? companyToUpdate.VipCustomer;
+ companyToUpdate.Departments = company.Departments ?? companyToUpdate.Departments;
+
+ return Updated(companyToUpdate);
+ }
+
+ [HttpPatch("odata/company")]
+ public IActionResult PatchCompany([FromBody] Delta delta)
+ {
+ var company = _dataSource.Company;
+ if (company == null)
+ {
+ return NotFound();
+ }
+
+ var updatedResult = delta.Patch(company);
+ return Updated(updatedResult);
+ }
+
+ [HttpPut("odata/Company/CoreDepartment/$ref")]
+ public IActionResult UpdateCompanyCoreDepartmentRef([FromBody] Uri departmentUri)
+ {
+ if (departmentUri == null)
+ {
+ return BadRequest();
+ }
+
+ // Extract the department ID from the URI
+ var lastSegment = departmentUri.Segments.Last();
+ var departmentId = int.Parse(Regex.Match(lastSegment, @"\d+").Value);
+
+ // Find the department by ID
+ var department = _dataSource.Departments?.SingleOrDefault(d => d.DepartmentID == departmentId);
+ if (department == null)
+ {
+ return NotFound();
+ }
+
+ // Update the core department reference in the company
+ var company = _dataSource.Company;
+ if (company == null)
+ {
+ return NotFound();
+ }
+
+ company.CoreDepartment = department;
+
+ return NoContent();
+ }
+
+ [HttpPut("odata/Company/VipCustomer/$ref")]
+ public IActionResult UpdateCompanyVipCustomerRef([FromBody] Uri vipCustomerUri)
+ {
+ var vipCustomer = _dataSource.VipCustomer;
+ if (vipCustomer == null)
+ {
+ return NotFound();
+ }
+
+ // Update the vipCustomer reference in the company
+ var company = _dataSource.Company;
+ if (company == null)
+ {
+ return NotFound();
+ }
+
+ company.VipCustomer = vipCustomer;
+
+ return NoContent();
+ }
+
+ [HttpDelete("odata/Company/Departments/$ref")]
+ public IActionResult DeleteDepartmentRefFromCompany([FromODataUri] string id)
+ {
+ id ??= Request.Query["$id"].ToString();
+
+ var uriId = new Uri(id);
+
+ // Extract the department ID from the URI
+ var lastSegment = uriId.Segments.Last();
+ var departmentId = int.Parse(Regex.Match(lastSegment, @"\d+").Value);
+
+ // Find the department by ID
+ var department = _dataSource.Departments?.SingleOrDefault(d => d.DepartmentID == departmentId);
+ if (department == null)
+ {
+ return NotFound();
+ }
+
+ // Remove the department reference from the company
+ var company = _dataSource.Company;
+ if (company == null)
+ {
+ return NotFound();
+ }
+
+ company.Departments?.Remove(department);
+
+ return NoContent();
+ }
+
+ [HttpDelete("odata/Company/VipCustomer/$ref")]
+ public IActionResult DeleteVipCustomerRefFromCompany()
+ {
+ // Remove the VipCustomer reference from the company
+ var company = _dataSource.Company;
+ if (company == null)
+ {
+ return NotFound();
+ }
+
+ company.VipCustomer = null;
+
+ return NoContent();
+ }
+
+ #endregion
+
+ #region odata/Boss
+
+ [EnableQuery]
+ [HttpGet("odata/Boss")]
+ public IActionResult GetBoss()
+ {
+ var result = _dataSource.Boss;
+
+ return Ok(result);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Boss/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.Customer")]
+ public IActionResult GetBossFromDerivedType()
+ {
+ var result = _dataSource.Boss;
+
+ if (result is not Customer customer)
+ {
+ return NotFound();
+ }
+
+ return Ok(customer);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Boss/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.Customer/City")]
+ public IActionResult GetBossCityFromDerivedType()
+ {
+ var result = _dataSource.Boss;
+
+ if (result is not Customer customer)
+ {
+ return NotFound();
+ }
+
+ return Ok(customer.City);
+ }
+
+ #endregion
+
+ #region odata/Departments
+
+ [EnableQuery]
+ [HttpGet("odata/Departments")]
+ public IActionResult GetDepartments()
+ {
+ var result = _dataSource.Departments;
+
+ return Ok(result);
+ }
+
+ [HttpPut("odata/Departments({key})/Company/$ref")]
+ public IActionResult UpdateDepartmentCompanyRef([FromRoute] int key, [FromBody] Uri companyUri)
+ {
+ if (companyUri == null)
+ {
+ return BadRequest();
+ }
+
+ // Find the company by ID
+ var company = _dataSource.Company;
+ if (company == null)
+ {
+ return NotFound();
+ }
+
+ // Find the department by ID
+ var department = _dataSource.Departments?.SingleOrDefault(d => d.DepartmentID == key);
+ if (department == null)
+ {
+ return NotFound();
+ }
+
+ // Update the company reference in the department
+ department.Company = company;
+
+ return NoContent();
+ }
+
+ [HttpPost("odata/Departments")]
+ public IActionResult CreateDepartment([FromBody] Department department)
+ {
+ if (department == null)
+ {
+ return BadRequest();
+ }
+
+ _dataSource.Departments?.Add(department);
+ return Created(department);
+ }
+
+ #endregion
+
+ #region odata/PublicCompany
+
+ [EnableQuery]
+ [HttpGet("odata/PublicCompany")]
+ public IActionResult GetPublicCompany()
+ {
+ var result = _dataSource.PublicCompany;
+
+ return Ok(result);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/PublicCompany/Name")]
+ public IActionResult GetPublicCompanyName()
+ {
+ var result = _dataSource.PublicCompany;
+
+ return Ok(result?.Name);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/PublicCompany/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.PublicCompany/StockExchange")]
+ public IActionResult GetPublicCompanyStockExchange()
+ {
+ var result = _dataSource.PublicCompany;
+
+ return Ok(result?.StockExchange);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/PublicCompany/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.PublicCompany/Assets")]
+ public IActionResult GetPublicCompanyAssets()
+ {
+ var result = _dataSource.PublicCompany;
+
+ return Ok(result?.Assets);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/PublicCompany/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.PublicCompany/Club")]
+ public IActionResult GetPublicCompanyClub()
+ {
+ var result = _dataSource.PublicCompany;
+
+ return Ok(result?.Club);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/PublicCompany/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.PublicCompany/LabourUnion")]
+ public IActionResult GetPublicCompanyLabourUnion()
+ {
+ var result = _dataSource.PublicCompany;
+
+ return Ok(result?.LabourUnion);
+ }
+
+ [HttpPost("odata/PublicCompany/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.PublicCompany/Assets")]
+ public IActionResult AddAssetToPublicCompanyAssets([FromBody] Asset asset)
+ {
+ var result = _dataSource.PublicCompany;
+ if (result == null)
+ {
+ return NotFound();
+ }
+
+ result.Assets ??= [];
+ result.Assets.Add(asset);
+
+ return Created(asset);
+ }
+
+ [HttpPut("odata/PublicCompany/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.PublicCompany")]
+ public IActionResult UpdatePublicCompany([FromBody] PublicCompany company)
+ {
+ var companyToUpdate = _dataSource.PublicCompany;
+
+ if (companyToUpdate == null)
+ {
+ return NotFound();
+ }
+
+ companyToUpdate.CompanyID = company.CompanyID == 0 ? companyToUpdate.CompanyID : company.CompanyID;
+ companyToUpdate.Address = company.Address ?? companyToUpdate.Address;
+ companyToUpdate.CompanyCategory = company.CompanyCategory;
+ companyToUpdate.Name = company.Name ?? companyToUpdate.Name;
+ companyToUpdate.Employees = company.Employees ?? companyToUpdate.Employees;
+ companyToUpdate.Revenue = company.Revenue;
+ companyToUpdate.CoreDepartment = company.CoreDepartment ?? companyToUpdate.CoreDepartment;
+ companyToUpdate.VipCustomer = company.VipCustomer ?? companyToUpdate.VipCustomer;
+ companyToUpdate.Departments = company.Departments ?? companyToUpdate.Departments;
+ companyToUpdate.StockExchange = company.StockExchange ?? companyToUpdate.StockExchange;
+ companyToUpdate.Assets = company.Assets ?? companyToUpdate.Assets;
+ companyToUpdate.Club = company.Club ?? companyToUpdate.Club;
+ companyToUpdate.LabourUnion = company.LabourUnion ?? companyToUpdate.LabourUnion;
+
+ return Updated(companyToUpdate);
+ }
+
+ [HttpPatch("odata/PublicCompany/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.PublicCompany/Club")]
+ public IActionResult PatchPublicCompanyClub([FromBody] Delta delta)
+ {
+ var club = _dataSource.PublicCompany?.Club;
+ if (club == null)
+ {
+ return NotFound();
+ }
+
+ var updatedResult = delta.Patch(club);
+ return Updated(updatedResult);
+ }
+
+ [HttpPatch("odata/LabourUnion")]
+ public IActionResult PatchPublicCompanyLabourUnion([FromBody] Delta delta)
+ {
+ var labourUnion = _dataSource.LabourUnion;
+ if (labourUnion == null)
+ {
+ return NotFound();
+ }
+
+ var updatedResult = delta.Patch(labourUnion);
+ return Updated(updatedResult);
+ }
+
+ [HttpDelete("odata/PublicCompany/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.PublicCompany/Assets({key})")]
+ public IActionResult RemoveAssetFromPublicCompanyAssets([FromRoute] int key)
+ {
+ var result = _dataSource.PublicCompany;
+ if (result == null)
+ {
+ return NotFound();
+ }
+
+ var asset = result.Assets?.SingleOrDefault(a => a.AssetID == key);
+ if (asset == null)
+ {
+ return NotFound();
+ }
+
+ result.Assets?.Remove(asset);
+
+ return NoContent();
+ }
+
+ #endregion
+
+ [HttpPost("odata/singletonclienttests/Default.ResetDefaultDataSource")]
+ public IActionResult ResetDefaultDataSource()
+ {
+ _dataSource = DefaultDataSource.CreateInstance();
+
+ return Ok();
+ }
+}
diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Server/SingletonTestsController.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Server/SingletonTestsController.cs
new file mode 100644
index 0000000000..35718db9fe
--- /dev/null
+++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Server/SingletonTestsController.cs
@@ -0,0 +1,228 @@
+//---------------------------------------------------------------------
+//
+// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
+//
+//---------------------------------------------------------------------
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.OData.Deltas;
+using Microsoft.AspNetCore.OData.Query;
+using Microsoft.AspNetCore.OData.Routing.Controllers;
+using Microsoft.OData.Client.E2E.Tests.Common.Server.Default;
+
+namespace Microsoft.OData.Client.E2E.Tests.SingletonTests.Server;
+
+public class SingletonTestsController : ODataController
+{
+ private static DefaultDataSource _dataSource;
+
+
+ #region odata/VipCustomer
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer")]
+ public IActionResult GetVipCustomer()
+ {
+ var result = _dataSource.VipCustomer;
+
+ return Ok(result);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer/PersonID")]
+ public IActionResult GetVipCustomerPersonID()
+ {
+ var result = _dataSource.VipCustomer;
+
+ return Ok(result?.PersonID);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer/Orders")]
+ public IActionResult GetVipCustomerOrders()
+ {
+ var result = _dataSource.VipCustomer;
+
+ return Ok(result?.Orders);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer/Orders({key})/OrderDate")]
+ public IActionResult GetVipCustomerOrderOrderDate([FromRoute] int key)
+ {
+ var result = _dataSource.VipCustomer?.Orders?.SingleOrDefault(a => a.OrderID == key);
+
+ return Ok(result?.OrderDate);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer/HomeAddress")]
+ public IActionResult GetVipCustomerHomeAddress()
+ {
+ var result = _dataSource.VipCustomer;
+
+ return Ok(result?.HomeAddress);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer/HomeAddress/City")]
+ public IActionResult GetVipCustomerHomeAddressCity()
+ {
+ var result = _dataSource.VipCustomer;
+
+ return Ok(result?.HomeAddress?.City);
+ }
+
+ [HttpPatch("odata/VipCustomer")]
+ public IActionResult UpdateVipCustomer([FromBody] Delta delta)
+ {
+ var customer = _dataSource.VipCustomer;
+ if (customer == null)
+ {
+ return NotFound();
+ }
+
+ var updatedResult = delta.Patch(customer);
+ return Updated(updatedResult);
+ }
+
+ #endregion
+
+ #region odata/Company
+
+ [EnableQuery]
+ [HttpGet("odata/Company")]
+ public IActionResult GetCompany()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/Name")]
+ public IActionResult GetCompanyName()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.Name);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/CompanyCategory")]
+ public IActionResult GetCompanyCompanyCategory()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.CompanyCategory);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/VipCustomer")]
+ public IActionResult GetCompanyVipCustomer()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.VipCustomer);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/Departments")]
+ public IActionResult GetCompanyDepartments()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.Departments);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/Revenue")]
+ public IActionResult GetRevenue()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.Revenue);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/CoreDepartment")]
+ public IActionResult GetCompanyCoreDepartment()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.CoreDepartment);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Company/Address/City")]
+ public IActionResult GetCompanyAddressCity()
+ {
+ var result = _dataSource.Company;
+
+ return Ok(result?.Address?.City);
+ }
+
+ #endregion
+
+ #region odata/Boss
+
+ [EnableQuery]
+ [HttpGet("odata/Boss")]
+ public IActionResult GetBoss()
+ {
+ var result = _dataSource.Boss;
+
+ return Ok(result);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Boss/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.Customer")]
+ public IActionResult GetBossFromDerivedType()
+ {
+ var result = _dataSource.Boss;
+
+ if (result is not Customer customer)
+ {
+ return NotFound();
+ }
+
+ return Ok(customer);
+ }
+
+ [EnableQuery]
+ [HttpGet("odata/Boss/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.Customer/City")]
+ public IActionResult GetBossCityFromDerivedType()
+ {
+ var result = _dataSource.Boss;
+
+ if (result is not Customer customer)
+ {
+ return NotFound();
+ }
+
+ return Ok(customer.City);
+ }
+
+ #endregion
+
+ #region odata/Departments
+
+ [EnableQuery]
+ [HttpGet("odata/Departments")]
+ public IActionResult GetDepartments()
+ {
+ var result = _dataSource.Departments;
+
+ return Ok(result);
+ }
+
+ #endregion
+
+ [HttpPost("odata/singletontests/Default.ResetDefaultDataSource")]
+ public IActionResult ResetDefaultDataSource()
+ {
+ _dataSource = DefaultDataSource.CreateInstance();
+
+ return Ok();
+ }
+}
diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Server/SingletonUpdateTestsController.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Server/SingletonUpdateTestsController.cs
new file mode 100644
index 0000000000..e34e7c4019
--- /dev/null
+++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Server/SingletonUpdateTestsController.cs
@@ -0,0 +1,53 @@
+//---------------------------------------------------------------------
+//
+// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
+//
+//---------------------------------------------------------------------
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.OData.Deltas;
+using Microsoft.AspNetCore.OData.Query;
+using Microsoft.AspNetCore.OData.Routing.Controllers;
+using Microsoft.OData.Client.E2E.Tests.Common.Server.Default;
+
+namespace Microsoft.OData.Client.E2E.Tests.SingletonTests.Server;
+
+public class SingletonUpdateTestsController : ODataController
+{
+ private static DefaultDataSource _dataSource;
+
+
+ #region odata/VipCustomer
+
+ [EnableQuery]
+ [HttpGet("odata/VipCustomer")]
+ public IActionResult GetVipCustomer()
+ {
+ var result = _dataSource.VipCustomer;
+
+ return Ok(result);
+ }
+
+ [HttpPatch("odata/VipCustomer")]
+ public IActionResult UpdateVipCustomer([FromBody] Delta delta)
+ {
+ var customer = _dataSource.VipCustomer;
+ if (customer == null)
+ {
+ return NotFound();
+ }
+
+ var updatedResult = delta.Patch(customer);
+ return Updated(updatedResult);
+ }
+
+ #endregion
+
+ [HttpPost("odata/singletonupdatetests/Default.ResetDefaultDataSource")]
+ public IActionResult ResetDefaultDataSource()
+ {
+ _dataSource = DefaultDataSource.CreateInstance();
+
+ return Ok();
+ }
+}
diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonClientTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonClientTests.cs
new file mode 100644
index 0000000000..05b5958f10
--- /dev/null
+++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonClientTests.cs
@@ -0,0 +1,512 @@
+//---------------------------------------------------------------------
+//
+// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
+//
+//---------------------------------------------------------------------
+
+using Microsoft.AspNetCore.OData;
+using Microsoft.AspNetCore.OData.Routing.Controllers;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.OData.Client.E2E.TestCommon;
+using Microsoft.OData.Client.E2E.Tests.Common.Client.Default.Default;
+using Microsoft.OData.Client.E2E.Tests.Common.Server.Default;
+using Microsoft.OData.Client.E2E.Tests.SingletonTests.Server;
+using Xunit;
+using Asset = Microsoft.OData.Client.E2E.Tests.Common.Client.Default.Asset;
+using Company = Microsoft.OData.Client.E2E.Tests.Common.Client.Default.Company;
+using CompanyCategory = Microsoft.OData.Client.E2E.Tests.Common.Client.Default.CompanyCategory;
+using Customer = Microsoft.OData.Client.E2E.Tests.Common.Client.Default.Customer;
+using Department = Microsoft.OData.Client.E2E.Tests.Common.Client.Default.Department;
+using PublicCompany = Microsoft.OData.Client.E2E.Tests.Common.Client.Default.PublicCompany;
+
+namespace Microsoft.OData.Client.E2E.Tests.SingletonTests.Tests;
+
+public class SingletonClientTests : EndToEndTestBase
+{
+ private readonly Uri _baseUri;
+ private readonly Container _context;
+
+ public class TestsStartup : TestStartupBase
+ {
+ public override void ConfigureServices(IServiceCollection services)
+ {
+ services.ConfigureControllers(typeof(SingletonClientTestsController), typeof(MetadataController));
+
+ services.AddControllers().AddOData(opt =>
+ {
+ opt.EnableQueryFeatures().AddRouteComponents("odata", DefaultEdmModel.GetEdmModel());
+ opt.RouteOptions.EnableNonParenthesisForEmptyParameterFunction = true;
+ });
+ }
+ }
+
+ public SingletonClientTests(TestWebApplicationFactory fixture) : base(fixture)
+ {
+ if (Client.BaseAddress == null)
+ {
+ throw new ArgumentNullException(nameof(Client.BaseAddress), "Base address cannot be null");
+ }
+
+ _baseUri = new Uri(Client.BaseAddress, "odata/");
+
+ _context = new Container(_baseUri)
+ {
+ HttpClientFactory = HttpClientFactory
+ };
+
+ ResetDefaultDataSource();
+ }
+
+ #region Singleton Client Tests
+
+ [Fact]
+ public async Task SingletonClientTestAsync()
+ {
+ // Arrange
+ var rand = new Random();
+ var format = ODataFormat.Json;
+
+ //Query Singleton
+ _context.MergeOption = MergeOption.OverwriteChanges;
+ var company = await _context.Company.GetValueAsync();
+ Assert.NotNull(company);
+
+ //Update Singleton Property and Verify
+ company.CompanyCategory = CompanyCategory.Communication;
+ company.Name = "UpdatedName";
+ company.Address.City = "UpdatedCity";
+ _context.UpdateObject(company);
+ await _context.SaveChangesAsync(SaveChangesOptions.ReplaceOnUpdate);
+
+ //Query Singleton Property - Select
+ var companyCategory = await _context.Company.Select(c => c.CompanyCategory).GetValueAsync();
+ Assert.Equal(CompanyCategory.Communication, companyCategory);
+
+ var cities = await _context.CreateSingletonQuery("Company/Address/City").ExecuteAsync();
+ var city = cities.Single();
+ Assert.Equal("UpdatedCity", city);
+
+ var names = await _context.ExecuteAsync(new Uri("Company/Name", UriKind.Relative));
+ var name = names.Single();
+ Assert.Equal("UpdatedName", name);
+
+ //Projection with properties - Select
+ company = await _context.Company.Select(c => new Company { CompanyID = c.CompanyID, Address = c.Address, Name = c.Name }).GetValueAsync();
+ Assert.NotNull(company);
+ Assert.Equal("UpdatedName", company.Name);
+
+ //Load Navigation Property
+ //Singleton
+ _context.LoadProperty(company, "VipCustomer");
+ Assert.NotNull(company.VipCustomer);
+
+ //Collection
+ await _context.LoadPropertyAsync(company, "Departments");
+ Assert.NotNull(company.Departments);
+ Assert.True(company.Departments.Count > 0);
+
+ //Single Entity
+ await _context.LoadPropertyAsync(company, "CoreDepartment");
+ Assert.NotNull(company.CoreDepartment);
+
+ //Add Navigation Property - Collection
+ company = await _context.Company.GetValueAsync();
+ int tmpDepartmentID = rand.Next();
+ int tmpCoreDepartmentID = rand.Next();
+ var department = new Department()
+ {
+ DepartmentID = tmpDepartmentID,
+ Name = "ID" + tmpDepartmentID,
+ };
+ var coreDepartment = new Department()
+ {
+ DepartmentID = tmpCoreDepartmentID,
+ Name = "ID" + tmpCoreDepartmentID,
+ };
+ _context.AddToDepartments(department);
+ _context.AddLink(company, "Departments", department);
+ await _context.SaveChangesAsync();
+
+ _context.AddToDepartments(coreDepartment);
+ _context.AddLink(company, "Departments", coreDepartment);
+ await _context.SaveChangesAsync();
+
+ _context.Departments.ToList();
+
+ //Projection with Navigation properties - Select
+ company = await _context.Company.Select(c => new Company { CompanyID = c.CompanyID, Departments = c.Departments }).GetValueAsync();
+ Assert.NotNull(company);
+ Assert.Contains(company.Departments, c => c.DepartmentID == tmpDepartmentID);
+ Assert.Contains(company.Departments, c => c.DepartmentID == tmpCoreDepartmentID);
+
+ //Update Navigation Property - Single Entity
+ _context.SetLink(company, "CoreDepartment", coreDepartment);
+ await _context.SaveChangesAsync();
+
+ //Projection with Navigation properties - Select
+ company = await _context.Company.Select(c => new Company { CompanyID = c.CompanyID, CoreDepartment = c.CoreDepartment }).GetValueAsync();
+ Assert.NotNull(company);
+ Assert.Equal(company.CoreDepartment.DepartmentID, tmpCoreDepartmentID);
+
+ //Update EntitySet's Navigation Property - Singleton
+ _context.SetLink(department, "Company", company);
+ await _context.SaveChangesAsync(SaveChangesOptions.ReplaceOnUpdate);
+
+ //Query(Expand) EntitySet's Navigation Property - Singleton
+ department = _context.Departments.Expand(d => d.Company).Where(d => d.DepartmentID == tmpDepartmentID).Single();
+ Assert.NotNull(department);
+ Assert.Equal(department.Company.CompanyID, company.CompanyID);
+
+ //Delete Navigation Property - EntitySet
+ _context.DeleteLink(company, "Departments", department);
+ await _context.SaveChangesAsync();
+
+ //Projection with Navigation Property - EntitySet
+ company.Departments = null;
+ company = await _context.Company.Select(c => new Company { CompanyID = c.CompanyID, Departments = c.Departments }).GetValueAsync();
+ Assert.NotNull(company);
+ Assert.DoesNotContain(company.Departments, c => c.DepartmentID == tmpDepartmentID);
+
+ //Query Singleton's Navigation Property - Singleton
+ company = await _context.Company.Select(c => new Company { CompanyID = c.CompanyID, VipCustomer = c.VipCustomer }).GetValueAsync();
+ Assert.NotNull(company);
+ Assert.NotNull(company.VipCustomer);
+
+ //Query Singleton again with Execute
+ var vipCustomers = await _context.ExecuteAsync(new Uri("VipCustomer", UriKind.Relative));
+ var vipCustomer = vipCustomers.Single();
+
+ //Update Singleton's Navigation property - Singleton
+ vipCustomer.LastName = "UpdatedLastName";
+ _context.UpdateRelatedObject(company, "VipCustomer", vipCustomer);
+ await _context.SaveChangesAsync();
+
+ company.VipCustomer = null;
+ company = await _context.Company.Expand(c => c.VipCustomer).GetValueAsync();
+ Assert.NotNull(company);
+ Assert.NotNull(company.VipCustomer);
+ Assert.Equal("UpdatedLastName", company.VipCustomer.LastName);
+
+ //Update Navigation Property - Delete the Singleton navigation
+ _context.SetLink(company, "VipCustomer", null);
+ await _context.SaveChangesAsync();
+
+ //Expand Navigation Property - Singleton
+ company.VipCustomer = null;
+ company = await _context.Company.Expand(c => c.VipCustomer).GetValueAsync();
+ Assert.NotNull(company);
+ Assert.Null(company.VipCustomer);
+
+ //Update Navigation Property - Singleton
+ var anotherVipCustomer = await _context.VipCustomer.GetValueAsync();
+ _context.SetLink(company, "VipCustomer", anotherVipCustomer);
+ await _context.SaveChangesAsync();
+ company = await _context.Company.Select(c => new Company { CompanyID = c.CompanyID, VipCustomer = c.VipCustomer }).GetValueAsync();
+ Assert.NotNull(company);
+ Assert.NotNull(company.VipCustomer);
+
+ ResetDefaultDataSource();
+ }
+
+ [Fact]
+ public void SingletonClientTest()
+ {
+ // Arrange
+ var rand = new Random();
+ var format = ODataFormat.Json;
+
+ //Query Singleton
+ _context.MergeOption = MergeOption.OverwriteChanges;
+ var company = _context.Company.GetValue();
+ Assert.NotNull(company);
+
+ //Update Singleton Property and Verify
+ company.CompanyCategory = CompanyCategory.Communication;
+ company.Name = "UpdatedName";
+ company.Address.City = "UpdatedCity";
+ _context.UpdateObject(company);
+ var result = _context.SaveChanges(SaveChangesOptions.ReplaceOnUpdate);
+
+ //Query Singleton Property - Select
+ var companyCategory = _context.Company.Select(c => c.CompanyCategory).GetValue();
+ Assert.Equal(CompanyCategory.Communication, companyCategory);
+
+ var cities = _context.CreateSingletonQuery("Company/Address/City").Execute();
+ var city = cities.Single();
+ Assert.Equal("UpdatedCity", city);
+
+ var name = _context.Execute(new Uri("Company/Name", UriKind.Relative)).Single();
+ Assert.Equal("UpdatedName", name);
+
+ //Projection with properties - Select
+ company = _context.Company.Select(c => new Company { CompanyID = c.CompanyID, Address = c.Address, Name = c.Name }).GetValue();
+ Assert.NotNull(company);
+ Assert.Equal("UpdatedName", company.Name);
+
+ //Load Navigation Property
+ //Singleton
+ _context.LoadProperty(company, "VipCustomer");
+ Assert.NotNull(company.VipCustomer);
+
+ //Collection
+ _context.LoadProperty(company, "Departments");
+ Assert.NotNull(company.Departments);
+ Assert.True(company.Departments.Count > 0);
+
+ //Single Entity
+ _context.LoadProperty(company, "CoreDepartment");
+ Assert.NotNull(company.CoreDepartment);
+
+ //Add Navigation Property - Collection
+ company = _context.Company.GetValue();
+ int tmpDepartmentID = rand.Next();
+ int tmpCoreDepartmentID = rand.Next();
+ var department = new Department()
+ {
+ DepartmentID = tmpDepartmentID,
+ Name = "ID" + tmpDepartmentID,
+ };
+ var coreDepartment = new Department()
+ {
+ DepartmentID = tmpCoreDepartmentID,
+ Name = "ID" + tmpCoreDepartmentID,
+ };
+ _context.AddToDepartments(department);
+ _context.AddLink(company, "Departments", department);
+ result = _context.SaveChanges();
+
+ _context.AddToDepartments(coreDepartment);
+ _context.AddLink(company, "Departments", coreDepartment);
+ result = _context.SaveChanges();
+
+ _context.Departments.ToList();
+
+ //Projection with Navigation properties - Select
+ company = _context.Company.Select(c => new Company { CompanyID = c.CompanyID, Departments = c.Departments }).GetValue();
+ Assert.NotNull(company);
+ Assert.Contains(company.Departments, c => c.DepartmentID == tmpDepartmentID);
+ Assert.Contains(company.Departments, c => c.DepartmentID == tmpCoreDepartmentID);
+
+ //Update Navigation Property - Single Entity
+ _context.SetLink(company, "CoreDepartment", coreDepartment);
+ result = _context.SaveChanges();
+
+ //Projection with Navigation properties - Select
+ company = _context.Company.Select(c => new Company { CompanyID = c.CompanyID, CoreDepartment = c.CoreDepartment }).GetValue();
+ Assert.NotNull(company);
+ Assert.Equal(company.CoreDepartment.DepartmentID, tmpCoreDepartmentID);
+
+ //Update EntitySet's Navigation Property - Singleton
+ _context.SetLink(department, "Company", company);
+ result = _context.SaveChanges(SaveChangesOptions.ReplaceOnUpdate);
+
+ //Query(Expand) EntitySet's Navigation Property - Singleton
+ department = _context.Departments.Expand(d => d.Company).Where(d => d.DepartmentID == tmpDepartmentID).Single();
+ Assert.NotNull(department);
+ Assert.Equal(department.Company.CompanyID, company.CompanyID);
+
+ //Delete Navigation Property - EntitySet
+ _context.DeleteLink(company, "Departments", department);
+ result = _context.SaveChanges();
+
+ //Projection with Navigation Property - EntitySet
+ company.Departments = null;
+ company = _context.Company.Select(c => new Company { CompanyID = c.CompanyID, Departments = c.Departments }).GetValue();
+ Assert.NotNull(company);
+ Assert.DoesNotContain(company.Departments, c => c.DepartmentID == tmpDepartmentID);
+
+ //Query Singleton's Navigation Property - Singleton
+ company = _context.Company.Select(c => new Company { CompanyID = c.CompanyID, VipCustomer = c.VipCustomer }).GetValue();
+ Assert.NotNull(company);
+ Assert.NotNull(company.VipCustomer);
+
+ //Query Singleton again with Execute
+ var vipCustomer = _context.Execute(new Uri("VipCustomer", UriKind.Relative)).Single();
+
+ //Update Singleton's Navigation property - Singleton
+ vipCustomer.LastName = "UpdatedLastName";
+ _context.UpdateRelatedObject(company, "VipCustomer", vipCustomer);
+ result = _context.SaveChanges();
+
+ company.VipCustomer = null;
+ company = _context.Company.Expand(c => c.VipCustomer).GetValue();
+ Assert.NotNull(company);
+ Assert.NotNull(company.VipCustomer);
+ Assert.Equal("UpdatedLastName", company.VipCustomer.LastName);
+
+ //Update Navigation Property - Delete the Singleton navigation
+ _context.SetLink(company, "VipCustomer", null);
+ result = _context.SaveChanges();
+
+ //Expand Navigation Property - Singleton
+ company.VipCustomer = null;
+ company = _context.Company.Expand(c => c.VipCustomer).GetValue();
+ Assert.NotNull(company);
+ Assert.Null(company.VipCustomer);
+
+ //Update Navigation Property - Singleton
+ var anotherVipCustomer = _context.VipCustomer.GetValue();
+ _context.SetLink(company, "VipCustomer", anotherVipCustomer);
+ result = _context.SaveChanges();
+ company = _context.Company.Select(c => new Company { CompanyID = c.CompanyID, VipCustomer = c.VipCustomer }).GetValue();
+ Assert.NotNull(company);
+ Assert.NotNull(company.VipCustomer);
+
+ ResetDefaultDataSource();
+ }
+
+ #endregion
+
+ #region DerivedType Singleton Tests
+
+ [Fact]
+ public void DerivedTypeSingletonClientTest()
+ {
+ // Query Singleton
+ _context.MergeOption = MergeOption.OverwriteChanges;
+ var company = _context.PublicCompany.GetValue();
+ Assert.NotNull(company);
+
+ // Update DerivedType Property and Verify
+ var publicCompany = company as PublicCompany;
+ Assert.NotNull(publicCompany);
+ publicCompany.Name = "UpdatedName";
+ publicCompany.StockExchange = "Updated StockExchange";
+ _context.UpdateObject(publicCompany);
+ _context.SaveChanges(SaveChangesOptions.ReplaceOnUpdate);
+
+ // Query Singleton Property - Select
+ var name = _context.PublicCompany.Select(c => c.Name).GetValue();
+ Assert.Equal("UpdatedName", name);
+ company = _context.CreateSingletonQuery("PublicCompany").Single();
+ Assert.Equal("Updated StockExchange", (company as PublicCompany).StockExchange);
+
+ // Query properties of DerivedType
+ var stockExchange = _context.PublicCompany.Select(c => (c as PublicCompany).StockExchange).GetValue();
+ Assert.Equal("Updated StockExchange", stockExchange);
+
+ // Projection with properties - Select
+ publicCompany = _context.PublicCompany.Select(c =>
+ new PublicCompany { CompanyID = c.CompanyID, Name = c.Name, StockExchange = (c as PublicCompany).StockExchange }).GetValue();
+ Assert.NotNull(publicCompany);
+ Assert.Equal("UpdatedName", publicCompany.Name);
+ Assert.Equal("Updated StockExchange", publicCompany.StockExchange);
+
+ company = _context.CreateSingletonQuery("PublicCompany").Single();
+
+ // Load Navigation Property
+ // Collection
+ _context.LoadProperty(company, "Assets");
+ Assert.NotNull(((PublicCompany)company).Assets);
+ Assert.Equal(2, ((PublicCompany)company).Assets.Count);
+
+ // Single Entity
+ _context.LoadProperty(company, "Club");
+ Assert.NotNull(((PublicCompany)company).Club);
+
+ // Singleton
+ _context.LoadProperty(publicCompany, "LabourUnion");
+ Assert.NotNull(((PublicCompany)company).LabourUnion);
+
+ //Add Contained Navigation Property - Collection of derived type
+ var random = new Random();
+ int tmpAssertId = random.Next();
+ Asset tmpAssert = new()
+ {
+ AssetID = tmpAssertId,
+ Name = tmpAssertId + "Name",
+ Number = tmpAssertId
+ };
+
+ _context.AddRelatedObject(publicCompany, "Assets", tmpAssert);
+ _context.SaveChanges();
+
+ // Query contained Navigation Property - Collection of derived type
+ company = _context.PublicCompany.Expand(c => (c as PublicCompany).Assets).GetValue();
+ Assert.NotNull(company);
+ Assert.Contains(((PublicCompany)company).Assets, a => a.AssetID == tmpAssertId);
+
+ _context.DeleteObject(tmpAssert);
+ _context.SaveChanges();
+
+ company = _context.PublicCompany.Expand(c => (c as PublicCompany).Assets).GetValue();
+ Assert.NotNull(company);
+ Assert.DoesNotContain(((PublicCompany)company).Assets, a => a.AssetID == tmpAssertId);
+
+ // Updated contained Navigation Property - SingleEntity of derived type
+ var club = ((PublicCompany)company).Club;
+ club.Name = "UpdatedClubName";
+ _context.UpdateRelatedObject(company, "Club", club);
+ _context.SaveChanges();
+
+ // Query Contained Navigation Property - Single Entity of derived type
+ publicCompany = _context.PublicCompany.Select(c => new PublicCompany { CompanyID = c.CompanyID, Club = (c as PublicCompany).Club }).GetValue();
+ Assert.NotNull(publicCompany);
+ Assert.NotNull(publicCompany.Club);
+ Assert.Equal("UpdatedClubName", publicCompany.Club.Name);
+
+ company = _context.PublicCompany.Expand(c => (c as PublicCompany).Club).GetValue();
+ Assert.NotNull(company);
+ Assert.NotNull(((PublicCompany)company).Club);
+
+ // Projection with Navigation property of derived type - Singleton
+ company = _context.PublicCompany.Expand(c => (c as PublicCompany).LabourUnion).GetValue();
+
+ // Update Navigation property of derived Type - Singleton
+ var labourUnion = ((PublicCompany)company).LabourUnion;
+ labourUnion.Name = "UpdatedLabourUnionName";
+ _context.UpdateRelatedObject(publicCompany, "LabourUnion", labourUnion);
+ _context.SaveChanges();
+
+ //Query singleton of derived type.
+ publicCompany = _context.PublicCompany.Select(c => new PublicCompany { CompanyID = c.CompanyID, LabourUnion = (c as PublicCompany).LabourUnion }).GetValue();
+ Assert.NotNull(publicCompany);
+ Assert.NotNull(publicCompany.LabourUnion);
+
+ ResetDefaultDataSource();
+ }
+
+ #endregion
+
+ #region Action/Function
+
+ [Fact]
+ public void InvokeFunctionBoundedToSingleton()
+ {
+ // Arrange & Act & Assert
+ var employeeCount = _context.Execute(new Uri(_baseUri.AbsoluteUri + "Company/Default.GetEmployeesCount", UriKind.Absolute)).Single();
+ Assert.Equal(2, employeeCount);
+
+ var company = _context.Company.GetValue();
+ var descriptor = _context.GetEntityDescriptor(company).OperationDescriptors.Single(e => e.Title == "Default.GetEmployeesCount");
+ employeeCount = _context.Execute(descriptor.Target, "GET", true).Single();
+ Assert.Equal(2, employeeCount);
+ }
+
+ [Fact]
+ public void InvokeActionBoundedToSingleton()
+ {
+ var company = _context.Company.GetValue();
+ _context.LoadProperty(company, "Revenue");
+
+ var newValue = _context.Execute(
+ new Uri(_baseUri.AbsoluteUri + "Company/Default.IncreaseRevenue"), "POST", true, new BodyOperationParameter("IncreaseValue", 20000));
+ Assert.Equal(newValue.Single(), company.Revenue + 20000);
+
+ OperationDescriptor descriptor = _context.GetEntityDescriptor(company).OperationDescriptors.Single(e => e.Title == "Default.IncreaseRevenue");
+ newValue = _context.Execute(descriptor.Target, "POST", new BodyOperationParameter("IncreaseValue", 40000));
+ Assert.Equal(newValue.Single(), company.Revenue + 60000);
+ }
+
+ #endregion
+
+ #region Private methods
+
+ private void ResetDefaultDataSource()
+ {
+ var actionUri = new Uri(_baseUri + "singletonclienttests/Default.ResetDefaultDataSource", UriKind.Absolute);
+ _context.Execute(actionUri, "POST");
+ }
+
+ #endregion
+}
diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonODataValueAssertEqualHelper.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonODataValueAssertEqualHelper.cs
new file mode 100644
index 0000000000..151493f803
--- /dev/null
+++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonODataValueAssertEqualHelper.cs
@@ -0,0 +1,139 @@
+//---------------------------------------------------------------------
+//
+// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
+//
+//---------------------------------------------------------------------
+
+using Xunit;
+
+namespace Microsoft.OData.Client.E2E.Tests.SingletonTests.Tests;
+
+public static class SingletonODataValueAssertEqualHelper
+{
+ #region Util methods to AssertEqual ODataValues
+
+ public static void AssertODataValueEqual(ODataValue expected, ODataValue actual)
+ {
+ if (expected is ODataPrimitiveValue expectedPrimitiveValue && actual is ODataPrimitiveValue actualPrimitiveValue)
+ {
+ AssertODataPrimitiveValueEqual(expectedPrimitiveValue, actualPrimitiveValue);
+ }
+ else
+ {
+ if (expected is ODataEnumValue expectedEnumValue && actual is ODataEnumValue actualEnumValue)
+ {
+ AssertODataEnumValueEqual(expectedEnumValue, actualEnumValue);
+ }
+ else
+ {
+ ODataCollectionValue expectedCollectionValue = (ODataCollectionValue)expected;
+ ODataCollectionValue actualCollectionValue = (ODataCollectionValue)actual;
+ AssertODataCollectionValueEqual(expectedCollectionValue, actualCollectionValue);
+ }
+ }
+ }
+
+ private static void AssertODataCollectionValueEqual(ODataCollectionValue expectedCollectionValue, ODataCollectionValue actualCollectionValue)
+ {
+ Assert.NotNull(expectedCollectionValue);
+ Assert.NotNull(actualCollectionValue);
+ Assert.Equal(expectedCollectionValue.TypeName, actualCollectionValue.TypeName);
+
+ var expectedItemsArray = expectedCollectionValue.Items.OfType