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().ToArray(); + var actualItemsArray = actualCollectionValue.Items.OfType().ToArray(); + + Assert.Equal(expectedItemsArray.Length, actualItemsArray.Length); + + for (int i = 0; i < expectedItemsArray.Length; i++) + { + if (expectedItemsArray[i] is ODataValue expectedOdataValue && actualItemsArray[i] is ODataValue actualOdataValue) + { + AssertODataValueEqual(expectedOdataValue, actualOdataValue); + } + else + { + Assert.Equal(expectedItemsArray[i], actualItemsArray[i]); + } + } + } + + public static void AssertODataPropertiesEqual(IEnumerable expectedProperties, IEnumerable actualProperties) + { + if (expectedProperties == null && actualProperties == null) + { + return; + } + + Assert.NotNull(expectedProperties); + Assert.NotNull(actualProperties); + + var expectedPropertyArray = expectedProperties.ToArray(); + var actualPropertyArray = actualProperties.ToArray(); + Assert.Equal(expectedPropertyArray.Length, actualPropertyArray.Length); + + for (int i = 0; i < expectedPropertyArray.Length; i++) + { + AssertODataPropertyEqual(expectedPropertyArray[i], actualPropertyArray[i]); + } + } + + public static void AssertODataPropertyAndResourceEqual(ODataResource expectedOdataProperty, ODataResource actualOdataProperty) + { + Assert.NotNull(expectedOdataProperty); + Assert.NotNull(actualOdataProperty); + AssertODataValueAndResourceEqual(expectedOdataProperty, actualOdataProperty); + } + + public static void AssertODataValueAndResourceEqual(ODataResource expected, ODataResource actual) + { + Assert.Equal(expected.TypeName, actual.TypeName); + AssertODataPropertiesEqual(expected.Properties, actual.Properties); + } + + public static void AssertODataPropertyEqual(ODataPropertyInfo expectedPropertyInfo, ODataPropertyInfo actualPropertyInfo) + { + Assert.NotNull(expectedPropertyInfo); + Assert.NotNull(actualPropertyInfo); + Assert.Equal(expectedPropertyInfo.Name, actualPropertyInfo.Name); + + var expectedProperty = expectedPropertyInfo as ODataProperty; + var actualProperty = actualPropertyInfo as ODataProperty; + + Assert.NotNull(expectedProperty); + Assert.NotNull(actualProperty); + + AssertODataValueEqual(ToODataValue(expectedProperty.Value), ToODataValue(actualProperty.Value)); + } + + private static ODataValue ToODataValue(object value) + { + if (value == null) + { + return new ODataNullValue(); + } + + if (value is ODataValue odataValue) + { + return odataValue; + } + + return new ODataPrimitiveValue(value); + } + + private static void AssertODataPrimitiveValueEqual(ODataPrimitiveValue expectedPrimitiveValue, ODataPrimitiveValue actualPrimitiveValue) + { + Assert.NotNull(expectedPrimitiveValue); + Assert.NotNull(actualPrimitiveValue); + Assert.Equal(expectedPrimitiveValue.Value, actualPrimitiveValue.Value); + } + + private static void AssertODataEnumValueEqual(ODataEnumValue expectedEnumValue, ODataEnumValue actualEnumValue) + { + Assert.NotNull(expectedEnumValue); + Assert.NotNull(actualEnumValue); + Assert.Equal(expectedEnumValue.Value, actualEnumValue.Value); + Assert.Equal(expectedEnumValue.TypeName, actualEnumValue.TypeName); + } + + #endregion Util methods to AssertEqual ODataValues +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonQueryTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonQueryTests.cs new file mode 100644 index 0000000000..9f0cdf0968 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonQueryTests.cs @@ -0,0 +1,450 @@ +//--------------------------------------------------------------------- +// +// 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.TestCommon.Common; +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 Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OData.Client.E2E.Tests.SingletonTests.Tests; + +public class SingletonQueryTests : EndToEndTestBase +{ + private readonly Uri _baseUri; + private readonly Container _context; + private readonly IEdmModel _model; + + public class TestsStartup : TestStartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(SingletonTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", DefaultEdmModel.GetEdmModel())); + } + } + + public SingletonQueryTests(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 + }; + + _model = DefaultEdmModel.GetEdmModel(); + ResetDefaultDataSource(); + } + + // Constants + private const string MimeTypeODataParameterFullMetadata = MimeTypes.ApplicationJson + MimeTypes.ODataParameterFullMetadata; + private const string MimeTypeODataParameterMinimalMetadata = MimeTypes.ApplicationJson + MimeTypes.ODataParameterMinimalMetadata; + + #region Query singleton entity + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingleton(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceEntriesAsync("VipCustomer", mimeType); + var entry = entries.SingleOrDefault(e => e != null && e.TypeName.EndsWith("Customer")); + + // Assert + Assert.NotNull(entry); + + var personIDProperty = entry.Properties.SingleOrDefault(p => p.Name == "PersonID") as ODataProperty; + var firstNameProperty = entry.Properties.SingleOrDefault(p => p.Name == "FirstName") as ODataProperty; + var lastNameProperty = entry.Properties.SingleOrDefault(p => p.Name == "LastName") as ODataProperty; + var cityProperty = entry.Properties.SingleOrDefault(p => p.Name == "City") as ODataProperty; + var emailsProperty = entry.Properties.SingleOrDefault(p => p.Name == "Emails") as ODataProperty; + Assert.NotNull(personIDProperty); + Assert.NotNull(firstNameProperty); + Assert.NotNull(lastNameProperty); + Assert.NotNull(cityProperty); + + Assert.Equal(1, personIDProperty.Value); + Assert.Equal("Bob", firstNameProperty.Value); + Assert.Equal("Cat", lastNameProperty.Value); + Assert.Equal("London", cityProperty.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingletonWhichIsOpenType(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceEntriesAsync("Company", mimeType); + var entry = entries.SingleOrDefault(e => e != null && e.TypeName.EndsWith("Company")); + + // Assert + Assert.NotNull(entry); + + var companyIDProperty = entry.Properties.SingleOrDefault(p => p.Name == "CompanyID") as ODataProperty; + var companyCategoryProperty = entry.Properties.SingleOrDefault(p => p.Name == "CompanyCategory") as ODataProperty; + var nameProperty = entry.Properties.SingleOrDefault(p => p.Name == "Name") as ODataProperty; + Assert.NotNull(companyIDProperty); + Assert.NotNull(companyCategoryProperty); + Assert.NotNull(nameProperty); + + Assert.Equal(0, companyIDProperty.Value); + Assert.Equal("IT", companyCategoryProperty.Value.ToString()); + Assert.Equal("MS", nameProperty.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QueryDerivedSingletonWithTypeCast(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceEntriesAsync("Boss/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.Customer", mimeType); + var entry = entries.SingleOrDefault(e => e != null && e.TypeName.EndsWith("Customer")); + + // Assert + Assert.NotNull(entry); + + var personIDProperty = entry.Properties.SingleOrDefault(p => p.Name == "PersonID") as ODataProperty; + var firstNameProperty = entry.Properties.SingleOrDefault(p => p.Name == "FirstName") as ODataProperty; + var lastNameProperty = entry.Properties.SingleOrDefault(p => p.Name == "LastName") as ODataProperty; + var cityProperty = entry.Properties.SingleOrDefault(p => p.Name == "City") as ODataProperty; + Assert.NotNull(personIDProperty); + Assert.NotNull(firstNameProperty); + Assert.NotNull(lastNameProperty); + Assert.NotNull(cityProperty); + + Assert.Equal(2, personIDProperty.Value); + Assert.Equal("Jill", firstNameProperty.Value); + Assert.Equal("Jones", lastNameProperty.Value); + Assert.Equal("Sydney", cityProperty.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QueryDerivedSingletonWithoutTypeCast(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceEntriesAsync("Boss", mimeType); + var entry = entries.SingleOrDefault(e => e != null && e.TypeName.EndsWith("Customer")); + + // Assert + Assert.NotNull(entry); + + var personIDProperty = entry.Properties.SingleOrDefault(p => p.Name == "PersonID") as ODataProperty; + var firstNameProperty = entry.Properties.SingleOrDefault(p => p.Name == "FirstName") as ODataProperty; + var cityProperty = entry.Properties.SingleOrDefault(p => p.Name == "City") as ODataProperty; + Assert.NotNull(personIDProperty); + Assert.NotNull(firstNameProperty); + Assert.NotNull(cityProperty); + + Assert.Equal(2, personIDProperty.Value); + Assert.Equal("Jill", firstNameProperty.Value); + Assert.Equal("Sydney", cityProperty.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingletonWithExpand(string mimeType) + { + // Arrange & Act + var resources = await this.TestsHelper.QueryResourceEntriesAsync("VipCustomer?$expand=Orders", mimeType); + var entries = resources.Where(r => r != null && r.Id != null).ToList(); + + // Assert + var orders = entries.FindAll(e => e.Id.AbsoluteUri.Contains("Orders")); + Assert.Equal(2, orders.Count); + + var customer = entries.SingleOrDefault(e => e.Id.AbsoluteUri.Contains("VipCustomer")); + Assert.NotNull(customer); + + var personIDProperty = customer.Properties.SingleOrDefault(p => p.Name == "PersonID") as ODataProperty; + var firstNameProperty = customer.Properties.SingleOrDefault(p => p.Name == "FirstName") as ODataProperty; + var cityProperty = customer.Properties.SingleOrDefault(p => p.Name == "City") as ODataProperty; + Assert.NotNull(personIDProperty); + Assert.NotNull(firstNameProperty); + Assert.NotNull(cityProperty); + + Assert.Equal(1, personIDProperty.Value); + Assert.Equal("Bob", firstNameProperty.Value); + Assert.Equal("London", cityProperty.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingletonWithSelect(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceEntriesAsync("VipCustomer?$select=PersonID,FirstName", mimeType); + var customer = entries.SingleOrDefault(e => e != null && e.TypeName.EndsWith("Customer")); + + // Assert + Assert.NotNull(customer); + Assert.Equal(2, customer.Properties.Count()); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QueryDerivedSingletonWithTypeCastAndSelect(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceEntriesAsync("Boss/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.Customer?$select=City", mimeType); + var customer = entries.SingleOrDefault(e => e != null && e.TypeName.EndsWith("Customer")); + + // Assert + Assert.NotNull(customer); + Assert.Single(customer.Properties); + var cityProperty = customer.Properties.SingleOrDefault(p => p.Name == "City") as ODataProperty; + Assert.NotNull(cityProperty); + Assert.Equal("Sydney", cityProperty.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingletonWithSelectUnderExpand(string mimeType) + { + // Arrange & Act + var resources = await this.TestsHelper.QueryResourceEntriesAsync("VipCustomer?$expand=Orders($select=OrderID,OrderDate)", mimeType); + var entries = resources.Where(r => r != null && r.Id != null).ToList(); + + // Assert + var orders = entries.FindAll(e => e != null && e.Id.AbsoluteUri.Contains("Orders")); + Assert.Equal(2, orders.Count); + + foreach (var order in orders) + { + Assert.Equal(2, order.Properties.Count()); + Assert.Contains(order.Properties, p => p.Name == "OrderID"); + Assert.Contains(order.Properties, p => p.Name == "OrderDate"); + } + + var customer = entries.SingleOrDefault(e => e != null && e.Id.AbsoluteUri.Contains("VipCustomer")); + Assert.NotNull(customer); + + var personIDProperty = customer.Properties.SingleOrDefault(p => p.Name == "PersonID") as ODataProperty; + var firstNameProperty = customer.Properties.SingleOrDefault(p => p.Name == "FirstName") as ODataProperty; + var cityProperty = customer.Properties.SingleOrDefault(p => p.Name == "City") as ODataProperty; + Assert.NotNull(personIDProperty); + Assert.NotNull(firstNameProperty); + Assert.NotNull(cityProperty); + + Assert.Equal(1, personIDProperty.Value); + Assert.Equal("Bob", firstNameProperty.Value); + Assert.Equal("London", cityProperty.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingletonWithMiscQueryOptions(string mimeType) + { + // Arrange & Act + var resources = await this.TestsHelper.QueryResourceEntriesAsync("VipCustomer?$select=FirstName,HomeAddress&$expand=Orders", mimeType); + var entries = resources.Where(r => r != null && r.Id != null).ToList(); + + // Assert + var orders = entries.FindAll(e => e.Id.AbsoluteUri.Contains("Orders")); + Assert.Equal(2, orders.Count); + + var customer = entries.SingleOrDefault(e => e.Id.AbsoluteUri.Contains("VipCustomer")); + Assert.NotNull(customer); + Assert.Single(customer.Properties); + + var firstNameProperty = customer.Properties.SingleOrDefault(p => p.Name == "FirstName") as ODataProperty; + Assert.NotNull(firstNameProperty); + Assert.Equal("Bob", firstNameProperty.Value); + + var homeAddress = resources.SingleOrDefault(r => r != null && r.TypeName.EndsWith("Address")); + Assert.NotNull(homeAddress); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task SelectDerivedPropertyWithoutTypeCastShouldFail(string mimeType) + { + // Arrange + ODataMessageReaderSettings readerSettings = new() { BaseUri = _baseUri }; + var requestUrl = new Uri(_baseUri.AbsoluteUri + "Boss?$select=City", UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", mimeType); + if (mimeType == MimeTypes.ApplicationAtomXml) + { + requestMessage.SetHeader("Accept", "text/html, application/xhtml+xml, */*"); + } + + // Act + var responseMessage = await requestMessage.GetResponseAsync(); + + // Assert + Assert.Equal(400, responseMessage.StatusCode); + } + + #endregion + + #region Query singleton property + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingletonProperty(string mimeType) + { + // Arrange & Act + var property = await this.TestsHelper.QueryPropertyAsync("VipCustomer/PersonID", mimeType); + + // Assert + Assert.NotNull(property); + Assert.Equal(1, property.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingletonPropertyUnderComplexProperty(string mimeType) + { + ResetDefaultDataSource(); + + // Arrange & Act + var property = await this.TestsHelper.QueryPropertyAsync("VipCustomer/HomeAddress/City", mimeType); + + // Assert + Assert.NotNull(property); + Assert.Equal("London", property.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingletonEnumProperty(string mimeType) + { + // Arrange & Act + var property = await this.TestsHelper.QueryPropertyAsync("Company/CompanyCategory", mimeType); + + // Assert + Assert.NotNull(property); + Assert.Equal("IT", property.Value.ToString()); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingletonNavigationProperty(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceSetsAsync("VipCustomer/Orders", mimeType); + var entry = entries.FirstOrDefault(); + + // Assert + Assert.NotNull(entry); + var orderIdProperty = entry.Properties.Single(p => p.Name == "OrderID") as ODataProperty; + Assert.NotNull(orderIdProperty); + Assert.Equal(7, orderIdProperty.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingletonPropertyUnderNavigationProperty(string mimeType) + { + // Arrange & Act + var property = await this.TestsHelper.QueryPropertyAsync("VipCustomer/Orders(8)/OrderDate", mimeType); + + // Assert + Assert.NotNull(property); + Assert.Equal(new DateTimeOffset(2011, 3, 4, 16, 3, 57, TimeSpan.FromHours(-8)), property.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QueryDerivedSingletonPropertyWithTypeCast(string mimeType) + { + // Arrange & Act + var property = await this.TestsHelper.QueryPropertyAsync("Boss/Microsoft.OData.Client.E2E.Tests.Common.Server.Default.Customer/City", mimeType); + + // Assert + Assert.NotNull(property); + Assert.Equal("Sydney", property.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QuerySingletonNavigationPropertyWithFilter(string mimeType) + { + // Arrange & Act + var entries = await this.TestsHelper.QueryResourceSetsAsync("VipCustomer/Orders?$filter=OrderID eq 8", mimeType); + var entry = entries.FirstOrDefault(); + + // Assert + Assert.NotNull(entry); + var orderDateProperty = entry.Properties.Single(p => p.Name == "OrderDate") as ODataProperty; + Assert.NotNull(orderDateProperty); + Assert.Equal(new DateTimeOffset(2011, 3, 4, 16, 3, 57, TimeSpan.FromHours(-8)), orderDateProperty.Value); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task QueryDerivedPropertyWithoutTypeCastShouldFail(string mimeType) + { + // Arrange + ODataMessageReaderSettings readerSettings = new() { BaseUri = _baseUri }; + var requestUrl = new Uri(_baseUri.AbsoluteUri + "Boss/City", UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", mimeType); + if (mimeType == MimeTypes.ApplicationAtomXml) + { + requestMessage.SetHeader("Accept", "text/html, application/xhtml+xml, */*"); + } + + var responseMessage = await requestMessage.GetResponseAsync(); + + // Assert + Assert.Equal(404, responseMessage.StatusCode); + } + + #endregion + + #region Private methods + + private SingletonTestsHelper TestsHelper + { + get + { + return new SingletonTestsHelper(_baseUri, _model, Client); + } + } + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "singletontests/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonTestsHelper.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonTestsHelper.cs new file mode 100644 index 0000000000..b96a54e060 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonTestsHelper.cs @@ -0,0 +1,139 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using Microsoft.OData.Client.E2E.TestCommon.Common; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OData.Client.E2E.Tests.SingletonTests.Tests; + +public class SingletonTestsHelper +{ + private readonly Uri BaseUri; + private readonly IEdmModel Model; + private readonly HttpClient Client; + private const string IncludeAnnotation = "odata.include-annotations"; + + public SingletonTestsHelper(Uri baseUri, IEdmModel model, HttpClient client) + { + this.BaseUri = baseUri; + this.Model = model; + this.Client = client; + } + + /// + /// Queries resource entries asynchronously based on the provided query text and MIME type. + /// + /// The query text to append to the base URI. + /// The MIME type to set in the request header. + /// A task that represents the asynchronous operation. The task result contains a list of . + public async Task> QueryResourceEntriesAsync(string queryText, string mimeType) + { + ODataMessageReaderSettings readerSettings = new() { BaseUri = BaseUri }; + var requestUrl = new Uri(BaseUri.AbsoluteUri + queryText, UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", mimeType); + requestMessage.SetHeader("Prefer", string.Format("{0}={1}", IncludeAnnotation, "*")); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(200, responseMessage.StatusCode); + + var entries = new List(); + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + using (var messageReader = new ODataMessageReader(responseMessage, readerSettings, Model)) + { + var reader = await messageReader.CreateODataResourceReaderAsync(); + while (await reader.ReadAsync()) + { + if (reader.State == ODataReaderState.ResourceEnd && reader.Item is ODataResource odataResource) + { + entries.Add(odataResource); + } + } + Assert.Equal(ODataReaderState.Completed, reader.State); + } + } + + return entries; + } + + /// + /// Queries resource sets asynchronously based on the provided query text and MIME type. + /// + /// The query text to append to the base URI. + /// The MIME type to set in the request header. + /// A task that represents the asynchronous operation. The task result contains a list of . + public async Task> QueryResourceSetsAsync(string queryText, string mimeType) + + { + ODataMessageReaderSettings readerSettings = new() { BaseUri = BaseUri }; + var requestUrl = new Uri(BaseUri.AbsoluteUri + queryText, UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", mimeType); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(200, responseMessage.StatusCode); + + var entries = new List(); + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + using (var messageReader = new ODataMessageReader(responseMessage, readerSettings, Model)) + { + var reader = await messageReader.CreateODataResourceSetReaderAsync(); + while (await reader.ReadAsync()) + { + if (reader.State == ODataReaderState.ResourceEnd) + { + if (reader.Item is ODataResource odataResource) + { + entries.Add(odataResource); + } + } + } + Assert.Equal(ODataReaderState.Completed, reader.State); + } + } + + return entries; + } + + /// + /// Queries a property asynchronously based on the provided request URI and MIME type. + /// + /// The request URI to append to the base URI. + /// The MIME type to set in the request header. + /// A task that represents the asynchronous operation. The task result contains an if found; otherwise, null. + public async Task QueryPropertyAsync(string requestUri, string mimeType) + { + var readerSettings = new ODataMessageReaderSettings() { BaseUri = BaseUri }; + + var uri = new Uri(BaseUri.AbsoluteUri + requestUri, UriKind.Absolute); + var requestMessage = new TestHttpClientRequestMessage(uri, Client); + + requestMessage.SetHeader("Accept", mimeType); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(200, responseMessage.StatusCode); + + ODataProperty? property = null; + + if (!mimeType.Contains(MimeTypes.ODataParameterNoMetadata)) + { + using (var messageReader = new ODataMessageReader(responseMessage, readerSettings, Model)) + { + property = messageReader.ReadProperty(); + } + } + + return property; + } +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonUpdateTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonUpdateTests.cs new file mode 100644 index 0000000000..c297f29742 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/SingletonTests/Tests/SingletonUpdateTests.cs @@ -0,0 +1,233 @@ +//--------------------------------------------------------------------- +// +// 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.TestCommon.Common; +using Microsoft.OData.Client.E2E.TestCommon.Helpers; +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 Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OData.Client.E2E.Tests.SingletonTests.Tests; + +public class SingletonUpdateTests : EndToEndTestBase +{ + private const string NameSpacePrefix = "Microsoft.OData.Client.E2E.Tests.Common.Server.Default"; + + private readonly Uri _baseUri; + private readonly Container _context; + private readonly IEdmModel _model; + + public class TestsStartup : TestStartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(SingletonUpdateTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", DefaultEdmModel.GetEdmModel())); + } + } + + public SingletonUpdateTests(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 + }; + + _model = DefaultEdmModel.GetEdmModel(); + ResetDefaultDataSource(); + } + + // Constants + private const string MimeTypeODataParameterFullMetadata = MimeTypes.ApplicationJson + MimeTypes.ODataParameterFullMetadata; + private const string MimeTypeODataParameterMinimalMetadata = MimeTypes.ApplicationJson + MimeTypes.ODataParameterMinimalMetadata; + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task UpdateSingletonProperty(string mimeType) + { + // Arrange + var cities = new Dictionary + { + { MimeTypes.ApplicationJson + MimeTypes.ODataParameterFullMetadata, "Seattle" }, + { MimeTypes.ApplicationJson + MimeTypes.ODataParameterMinimalMetadata, "Paris" } + }; + + // Act (Query) + var entries = await TestsHelper.QueryResourceEntriesAsync("VipCustomer", mimeType); + var customerEntry = entries.SingleOrDefault(e => e != null && e.TypeName.EndsWith("Customer")); + + // Assert + Assert.NotNull(customerEntry); + var cityProperty0 = customerEntry.Properties.Single(p => p.Name == "City") as ODataProperty; + Assert.NotNull(cityProperty0); + Assert.Equal("London", cityProperty0.Value); + + // Arrange + var properties = new[] { new ODataProperty { Name = "City", Value = cities[mimeType] } }; + + // Act (Update) + await this.UpdateEntryAsync("Customer", "VipCustomer", mimeType, properties); + var updatedEntries = await TestsHelper.QueryResourceEntriesAsync("VipCustomer", mimeType); + + // Assert + var updatedCustomerEntry = updatedEntries.SingleOrDefault(e => e != null && e.TypeName.EndsWith("Customer")); + Assert.NotNull(updatedCustomerEntry); + + var cityProperty1 = updatedCustomerEntry.Properties.Single(p => p.Name == "City") as ODataProperty; + Assert.NotNull(cityProperty1); + Assert.Equal(cities[mimeType], cityProperty1.Value); + + ResetDefaultDataSource(); + } + + [Theory] + [InlineData(MimeTypeODataParameterFullMetadata)] + [InlineData(MimeTypeODataParameterMinimalMetadata)] + public async Task UpdateSingletonComplexProperty(string mimeType) + { + // Arrange + var complex0 = new ODataResource() + { + TypeName = $"{NameSpacePrefix}.Address", + Properties = + [ + new ODataProperty() {Name = "Street", Value = "1 Microsoft Way"}, + new ODataProperty() {Name = "City", Value = "London"}, + new ODataProperty() {Name = "PostalCode", Value = "98052"}, + new ODataProperty() {Name = "UpdatedTime", Value = DateTimeOffset.Parse("1/1/0001 12:00:00 AM +00:00")} + ] + }; + + var homeAddress0 = new ODataNestedResourceInfo() { Name = "HomeAddress", IsCollection = false }; + homeAddress0.SetAnnotation(complex0); + + var complex1 = new ODataResource() + { + TypeName = $"{NameSpacePrefix}.Address", + Properties = + [ + new ODataProperty() {Name = "Street", Value = "Zixing 999"}, + new ODataProperty() {Name = "City", Value = "Seattle"}, + new ODataProperty() {Name = "PostalCode", Value = "1111"}, + new ODataProperty() {Name = "UpdatedTime", Value = DateTimeOffset.Parse("1/1/0001 12:00:00 AM +00:00")} + ] + }; + + var homeAddress1 = new ODataNestedResourceInfo() { Name = "HomeAddress", IsCollection = false }; + homeAddress1.SetAnnotation(complex1); + + var currentHomeAddress = complex0; + var updatedHomeAddress = complex1; + var properties = new[] { homeAddress1 }; + + if(mimeType.Equals(MimeTypes.ApplicationJson + MimeTypes.ODataParameterMinimalMetadata)) + { + currentHomeAddress = complex0; + updatedHomeAddress = complex0; + properties = new[] { homeAddress0 }; + } + + // Act + var entries = await TestsHelper.QueryResourceEntriesAsync("VipCustomer", mimeType); + + // Assert + SingletonODataValueAssertEqualHelper.AssertODataPropertyAndResourceEqual(currentHomeAddress, entries[0]); + + // Act + await this.UpdateEntryAsync("Customer", "VipCustomer", mimeType, properties); + + var updatedEntries = await TestsHelper.QueryResourceEntriesAsync("VipCustomer", mimeType); + + // Assert + SingletonODataValueAssertEqualHelper.AssertODataPropertyAndResourceEqual(updatedHomeAddress, updatedEntries[0]); + + ResetDefaultDataSource(); + } + + #region Private methods + + private SingletonTestsHelper TestsHelper + { + get + { + return new SingletonTestsHelper(_baseUri, _model, Client); + } + } + + private async Task UpdateEntryAsync(string singletonType, string singletonName, string mimeType, IEnumerable properties) + { + var entry = new ODataResource() { TypeName = $"{NameSpacePrefix}.{singletonType}" }; + var elementType = properties != null && properties.Any() ? properties.ElementAt(0).GetType() : null; + + Assert.NotNull(properties); + + if (elementType == typeof(ODataProperty)) + { + entry.Properties = properties.Cast(); + } + + var settings = new ODataMessageWriterSettings + { + BaseUri = _baseUri, + EnableMessageStreamDisposal = false, // Ensure the stream is not disposed of prematurely + }; + + var customerType = _model.FindDeclaredType(NameSpacePrefix + singletonType) as IEdmEntityType; + var customerSet = _model.EntityContainer.FindSingleton(singletonName); + + var requestMessage = new TestHttpClientRequestMessage(new Uri(_baseUri + singletonName), Client); + requestMessage.SetHeader("Content-Type", mimeType); + requestMessage.SetHeader("Accept", mimeType); + requestMessage.Method = "PATCH"; + + using (var messageWriter = new ODataMessageWriter(requestMessage, settings)) + { + var odataWriter = await messageWriter.CreateODataResourceWriterAsync(customerSet, customerType); + await odataWriter.WriteStartAsync(entry); + if (elementType == typeof(ODataNestedResourceInfo)) + { + foreach (var p in properties) + { + var nestedInfo = (ODataNestedResourceInfo)p; + await odataWriter.WriteStartAsync(nestedInfo); + await odataWriter.WriteStartAsync(nestedInfo.GetAnnotation()); + await odataWriter.WriteEndAsync(); + await odataWriter.WriteEndAsync(); + } + } + await odataWriter.WriteEndAsync(); + } + + var responseMessage = await requestMessage.GetResponseAsync(); + + // verify the update + Assert.Equal(204, responseMessage.StatusCode); + } + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "singletonupdatetests/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +}