diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/EndToEnd/DefaultContainer.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/EndToEnd/DefaultContainer.cs index 503d453f2a..c29d6c31e7 100644 --- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/EndToEnd/DefaultContainer.cs +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/EndToEnd/DefaultContainer.cs @@ -4225,6 +4225,7 @@ public CustomerInfoSingle(global::Microsoft.OData.Client.DataServiceQuerySingle< /// CustomerInfoId /// [global::Microsoft.OData.Client.Key("CustomerInfoId")] + [global::Microsoft.OData.Client.HasStream()] [global::Microsoft.OData.Client.OriginalNameAttribute("CustomerInfo")] public partial class CustomerInfo : global::Microsoft.OData.Client.BaseEntityType, global::System.ComponentModel.INotifyPropertyChanged { @@ -5690,6 +5691,7 @@ public CarSingle(global::Microsoft.OData.Client.DataServiceQuerySingle quer /// VIN /// [global::Microsoft.OData.Client.Key("VIN")] + [global::Microsoft.OData.Client.HasStream()] [global::Microsoft.OData.Client.OriginalNameAttribute("Car")] public partial class Car : global::Microsoft.OData.Client.BaseEntityType, global::System.ComponentModel.INotifyPropertyChanged { diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/EndToEnd/EndToEndServiceCsdl.xml b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/EndToEnd/EndToEndServiceCsdl.xml index 2e96664e92..8bee34dba4 100644 --- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/EndToEnd/EndToEndServiceCsdl.xml +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Client/EndToEnd/EndToEndServiceCsdl.xml @@ -215,7 +215,7 @@ - + @@ -287,7 +287,7 @@ - + diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/EndToEnd/CommonEndToEndDataModel.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/EndToEnd/CommonEndToEndDataModel.cs index 3b6a6c66e1..ee94cd6c8c 100644 --- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/EndToEnd/CommonEndToEndDataModel.cs +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/EndToEnd/CommonEndToEndDataModel.cs @@ -5,6 +5,7 @@ // //------------------------------------------------------------------------------ +using Microsoft.OData.ModelBuilder; using Microsoft.Spatial; using EfKey = System.ComponentModel.DataAnnotations.KeyAttribute; @@ -182,6 +183,7 @@ public class ProductPhoto public byte[]? Photo { get; set; } } + [MediaType] public class CustomerInfo { public int CustomerInfoId { get; set; } @@ -250,6 +252,7 @@ public class MappedEntityType public ContactDetails? ComplexContactDetails { get; set; } } + [MediaType] public class Car { [EfKey] diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/DollarSegmentTestsController.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/DollarSegmentTestsController.cs new file mode 100644 index 0000000000..db32cb4880 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/DollarSegmentTestsController.cs @@ -0,0 +1,141 @@ +//--------------------------------------------------------------------- +// +// 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.Formatter; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd; + +namespace Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Server; + +public class DollarSegmentTestsController : ODataController +{ + private static CommonEndToEndDataSource _dataSource; + + [EnableQuery] + [HttpGet("odata/People/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee")] + public IActionResult GetPeopleOfTypeEmployee() + { + var people = _dataSource.People?.OfType(); + return Ok(people); + } + + [EnableQuery] + [HttpGet("odata/People/{key}/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/$/Salary")] + public IActionResult GetEmployeeSalary([FromRoute] int key) + { + var employee = _dataSource.People?.OfType().SingleOrDefault(a => a.PersonId == key); + if (employee == null) + { + return NotFound(); + } + return Ok(employee.Salary); + } + + [EnableQuery] + [HttpGet("odata/Products/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct/{key}/RelatedProducts/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct/{relatedKey}/Photos")] + public IActionResult GetProductRelatedPhotos([FromRoute] int key, [FromRoute] int relatedKey) + { + var product = _dataSource.Products?.SingleOrDefault(a => a.ProductId == key); + if (product == null) + { + return NotFound(); + } + + var relatedProduct = product.RelatedProducts?.SingleOrDefault(a => a.ProductId == relatedKey); + if (relatedProduct == null) + { + return NotFound(); + } + + return Ok(relatedProduct.Photos); + } + + [EnableQuery] + [HttpGet("odata/Products/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct")] + public IActionResult GetDiscontinuedProducts() + { + var discontinuedProducts = _dataSource.Products?.OfType(); + + return Ok(discontinuedProducts); + } + + [EnableQuery] + [HttpGet("odata/Products/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct/$")] + public IActionResult GetAnotherDiscontinuedProducts() + { + var discontinuedProducts = _dataSource.Products?.OfType(); + + return Ok(discontinuedProducts); + } + + [EnableQuery] + [HttpGet("odata/Products/$/$/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct")] + public IActionResult GetDiscontinuedProductsMultipleDollarSegments() + { + var discontinuedProducts = _dataSource.Products?.OfType(); + + return Ok(discontinuedProducts); + } + + [EnableQuery] + [HttpGet("odata/Login")] + public IActionResult GetLogin() + { + var login = _dataSource.Logins; + return Ok(login); + } + + [HttpPost("odata/People/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/$/Default.IncreaseSalaries")] + public IActionResult IncreaseSalaries([FromODataBody] int n) + { + _dataSource.People?.OfType().ToList().ForEach(e => e.Salary += n); + return Ok(); + } + + [HttpPost("odata/People/$/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/$/$/Default.IncreaseSalaries")] + public IActionResult IncreaseSalariesMultipleDollarSegments([FromODataBody] int n) + { + _dataSource.People?.OfType().ToList().ForEach(e => e.Salary += n); + return Ok(); + } + + [HttpPost("odata/People/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/$/Default.IncreaseSalaries/$")] + public IActionResult IncreaseSalariesDollarSegmentAtUriEnd([FromODataBody] int n) + { + _dataSource.People?.OfType().ToList().ForEach(e => e.Salary += n); + return Ok(); + } + + [HttpPost("odata/People/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/{key}/$/Default.Sack")] + public IActionResult Sack([FromODataUri] int key) + { + var employee = _dataSource.People?.OfType().SingleOrDefault(a => a.PersonId == key); + if (employee == null) + { + return NotFound(); + } + + _dataSource.People?.Remove(employee); + return NoContent(); + } + + [HttpPost("odata/Login")] + public IActionResult PostLogin([FromBody] Login login) + { + _dataSource.Logins?.Add(login); + return Created(login); + } + + [HttpPost("odata/dollarsegmenttests/Default.ResetDefaultDataSource")] + public IActionResult ResetDefaultDataSource() + { + _dataSource = CommonEndToEndDataSource.CreateInstance(); + + return Ok(); + } +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/KeyAsSegmentTestsController.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/KeyAsSegmentTestsController.cs new file mode 100644 index 0000000000..c984ad7512 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/KeyAsSegmentTestsController.cs @@ -0,0 +1,203 @@ +//--------------------------------------------------------------------- +// +// 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.Formatter; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd; + +namespace Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Server; + +public class KeyAsSegmentTestsController : ODataController +{ + private static CommonEndToEndDataSource _dataSource; + + [EnableQuery] + [HttpGet("odata/People")] + public IActionResult GetPeople() + { + var people = _dataSource.People; + return Ok(people); + } + + [EnableQuery] + [HttpGet("odata/People/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee")] + public IActionResult GetPeopleOfTypeEmployee() + { + var people = _dataSource.People?.OfType(); + return Ok(people); + } + + [EnableQuery] + [HttpGet("odata/People({key})")] + public IActionResult GetPerson([FromRoute] int key) + { + var person = _dataSource.People?.SingleOrDefault(a => a.PersonId == key); + if (person == null) + { + return NotFound(); + } + return Ok(person); + } + + [EnableQuery] + [HttpGet("odata/People/{key}")] + public IActionResult GetPersonById([FromRoute] int key) + { + var person = _dataSource.People?.SingleOrDefault(a => a.PersonId == key); + if (person == null) + { + return NotFound(); + } + return Ok(person); + } + + [EnableQuery] + [HttpGet("odata/People/{key}/PersonMetadata")] + public IActionResult GetPersonMetadatada([FromRoute] int key) + { + var person = _dataSource.People?.SingleOrDefault(a => a.PersonId == key); + if (person == null) + { + return NotFound(); + } + return Ok(person.PersonMetadata); + } + + [EnableQuery] + [HttpGet("odata/Products/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct/{key}/RelatedProducts/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct/{relatedKey}/Photos")] + public IActionResult GetProductRelatedPhotos([FromRoute] int key, [FromRoute] int relatedKey) + { + var product = _dataSource.Products?.SingleOrDefault(a => a.ProductId == key); + if (product == null) + { + return NotFound(); + } + + var relatedProduct = product.RelatedProducts?.SingleOrDefault(a => a.ProductId == relatedKey); + if (relatedProduct == null) + { + return NotFound(); + } + + return Ok(relatedProduct.Photos); + } + + [EnableQuery] + [HttpGet("odata/Orders({key})")] + public IActionResult GetProduct([FromRoute] int key) + { + var order = _dataSource.Orders?.SingleOrDefault(a => a.OrderId == key); + if (order == null) + { + return NotFound(); + } + return Ok(order); + } + + [EnableQuery] + [HttpGet("odata/Customers")] + public IActionResult GetCustomers() + { + var customers = _dataSource.Customers; + return Ok(customers); + } + + [EnableQuery] + [HttpGet("odata/Customers/{key}")] + public IActionResult GetCustomer([FromODataUri] int key) + { + var customer = _dataSource.Customers?.SingleOrDefault(c => c.CustomerId == key); + if (customer == null) + { + return NotFound(); + } + + return Ok(customer); + } + + [EnableQuery] + [HttpGet("odata/CustomerInfos")] + public IActionResult GetCustomerInfos() + { + var customerInfos = _dataSource.CustomerInfos; + return Ok(customerInfos); + } + + [EnableQuery] + [HttpGet("odata/CustomerInfos({key})/$value")] + public IActionResult GetCustomerInfosValue([FromODataUri] int key) + { + var customerInfo = _dataSource.CustomerInfos?.SingleOrDefault(c => c.CustomerInfoId == key); + if (customerInfo == null) + { + return NotFound(); + } + + return Ok(customerInfo); + } + + [EnableQuery] + [HttpGet("odata/Cars")] + public IActionResult GetCars() + { + var cars = _dataSource.Cars; + return Ok(cars); + } + + [EnableQuery] + [HttpGet("odata/Cars({key})/Photo")] + public IActionResult GetCarPhoto([FromODataUri] int key) + { + var car = _dataSource.Cars?.SingleOrDefault(c => c.VIN == key); + if (car == null) + { + return NotFound(); + } + + car.Photo = new MemoryStream([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]); + + return Ok(car.Photo); + } + + [HttpPut("odata/Cars({key})/Video")] + public IActionResult AddVideoToCar([FromODataUri] int key) + { + var car = _dataSource.Cars?.SingleOrDefault(c => c.VIN == key); + if (car == null) + { + return NotFound(); + } + + var video = Request.Body; + car.Video = video; + return Updated(car); + } + + [EnableQuery] + [HttpPut("odata/CustomerInfos({key})/$value")] + public IActionResult AddStreamValueToCustomerInfo([FromODataUri] int key) + { + var customerInfo = _dataSource.CustomerInfos?.SingleOrDefault(c => c.CustomerInfoId == key); + if (customerInfo == null) + { + return NotFound(); + } + + var stream = Request.Body; + + return Updated(customerInfo); + } + + [HttpPost("odata/keyassegmenttests/Default.ResetDefaultDataSource")] + public IActionResult ResetDefaultDataSource() + { + _dataSource = CommonEndToEndDataSource.CreateInstance(); + + return Ok(); + } +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/LiteralFormatTestsController.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/LiteralFormatTestsController.cs new file mode 100644 index 0000000000..72b21b362c --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/LiteralFormatTestsController.cs @@ -0,0 +1,108 @@ +//--------------------------------------------------------------------- +// +// 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.Formatter; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd; + +namespace Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Server; + +public class LiteralFormatTestsController : ODataController +{ + private static CommonEndToEndDataSource _dataSource; + + [EnableQuery] + [HttpGet("odata/Customers")] + public IActionResult GetCustomers() + { + var customers = _dataSource.Customers; + return Ok(customers); + } + + [EnableQuery] + [HttpGet("odata/Customers/{key}")] + public IActionResult GetCustomer([FromODataUri] int key) + { + var customer = _dataSource.Customers?.SingleOrDefault(c => c.CustomerId == key); + if (customer == null) + { + return NotFound(); + } + + return Ok(customer); + } + + [EnableQuery] + [HttpGet("odata/Login")] + public IActionResult GetLogin() + { + var login = _dataSource.Logins; + return Ok(login); + } + + [EnableQuery] + [HttpGet("odata/Login/{key}/Customer")] + public IActionResult GetLoginCustomers([FromODataUri] string key) + { + var login = _dataSource.Logins?.SingleOrDefault(l => l.Username == key); + if (login == null) + { + return NotFound(); + } + + return Ok(login.Customer); + } + + [HttpPost("odata/Login")] + public IActionResult AddLogin([FromBody] Login login) + { + _dataSource.Logins?.Add(login); + return Created(login); + } + + [HttpPost("odata/Customers({key})/Logins/$ref")] + public IActionResult AddLoginsRefToCustomer([FromODataUri] int key, [FromBody] Uri loginUri) + { + if (loginUri == null) + { + return BadRequest(); + } + + // Extract the login ID from the URI + var lastSegment = loginUri.Segments.Last(); + int startIndex = lastSegment.IndexOf("('") + 2; // +2 to skip the opening parenthesis and the single quote + int endIndex = lastSegment.IndexOf("')") - 1; // -1 to skip the closing single quote + + var loginUserName = Uri.UnescapeDataString(lastSegment.Substring(startIndex, endIndex - startIndex + 1)); + + // Find the login by ID + var login = _dataSource.Logins?.SingleOrDefault(d => d.Username == loginUserName); + if (login == null) + { + return NotFound(); + } + + var customer = _dataSource.Customers?.SingleOrDefault(c => c.CustomerId == key); + if (customer == null) + { + return NotFound(); + } + + customer.Logins?.Add(login); + + return Ok(login); + } + + [HttpPost("odata/keyasssegmentliteralformattests/Default.ResetDefaultDataSource")] + public IActionResult ResetDefaultDataSource() + { + _dataSource = CommonEndToEndDataSource.CreateInstance(); + + return Ok(); + } +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/UpdateTestsController.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/UpdateTestsController.cs new file mode 100644 index 0000000000..5fa551ba0d --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Server/UpdateTestsController.cs @@ -0,0 +1,249 @@ +//--------------------------------------------------------------------- +// +// 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.Formatter; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd; + +namespace Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Server; + +public class UpdateTestsController : ODataController +{ + private static CommonEndToEndDataSource _dataSource; + + [EnableQuery] + [HttpGet("odata/People")] + public IActionResult GetPeople() + { + var people = _dataSource.People; + return Ok(people); + } + + [EnableQuery] + [HttpGet("odata/People/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee")] + public IActionResult GetPeopleOfTypeEmployee() + { + var people = _dataSource.People?.OfType(); + return Ok(people); + } + + [EnableQuery] + [HttpGet("odata/People({key})")] + public IActionResult GetPerson([FromRoute] int key) + { + var person = _dataSource.People?.SingleOrDefault(a => a.PersonId == key); + if (person == null) + { + return NotFound(); + } + return Ok(person); + } + + [EnableQuery] + [HttpGet("odata/People/{key}")] + public IActionResult GetPersonById([FromRoute] int key) + { + var person = _dataSource.People?.SingleOrDefault(a => a.PersonId == key); + if (person == null) + { + return NotFound(); + } + return Ok(person); + } + + [EnableQuery] + [HttpGet("odata/Orders")] + public IActionResult GetOrders() + { + var orders = _dataSource.Orders; + return Ok(orders); + } + + [EnableQuery] + [HttpGet("odata/Customers")] + public IActionResult GetCustomers() + { + var customers = _dataSource.Customers; + return Ok(customers); + } + + [EnableQuery] + [HttpGet("odata/Customers/{key}")] + public IActionResult GetCustomer([FromODataUri] int key) + { + var customer = _dataSource.Customers?.SingleOrDefault(c => c.CustomerId == key); + if (customer == null) + { + return NotFound(); + } + + return Ok(customer); + } + + [HttpPost("odata/People")] + public IActionResult AddPerson([FromBody] Person person) + { + _dataSource.People?.Add(person); + return Created(person); + } + + [HttpPost("odata/Customers")] + public IActionResult AddCustomer([FromBody] Customer customer) + { + _dataSource.Customers?.Add(customer); + return Created(customer); + } + + [HttpPost("odata/Customers({key})/Orders")] + public IActionResult AddOrderToCustomer([FromODataUri] int key, [FromBody] Order order) + { + var customer = _dataSource.Customers?.SingleOrDefault(c => c.CustomerId == key); + if (customer == null) + { + return NotFound(); + } + + customer.Orders?.Add(order); + return Created(order); + } + + [HttpPost("odata/Customers({key})/Orders/$ref")] + public IActionResult AddOrderRefToCustomer([FromODataUri] int key, [FromBody] Uri orderUri) + { + if (orderUri == null) + { + return BadRequest(); + } + + // Extract the ID from the URI + var lastSegment = orderUri.Segments.Last(); + int startIndex = lastSegment.IndexOf('(') + 1; + int endIndex = lastSegment.IndexOf(')') - 1; + var orderId = int.Parse(Uri.UnescapeDataString(lastSegment.Substring(startIndex, endIndex - startIndex + 1))); + + // Find the order by ID + var order = _dataSource.Orders?.SingleOrDefault(d => d.OrderId == orderId); + if (order == null) + { + return NotFound(); + } + + // Add the order reference to the customer + var customer = _dataSource.Customers?.SingleOrDefault(c => c.CustomerId == key); + if (customer == null) + { + return NotFound(); + } + + customer.Orders?.Add(order); + + return Ok(customer); + } + + [HttpPut("odata/People({key})/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.SpecialEmployee/Manager/$ref")] + public IActionResult UpdateEmployeeManager([FromODataUri] int key, [FromBody] Uri managerUri) + { + if (managerUri == null) + { + return BadRequest(); + } + + // Extract the ID from the URI + var lastSegment = managerUri.Segments.Last(); + int startIndex = lastSegment.IndexOf('(') + 1; + int endIndex = lastSegment.IndexOf(')') - 1; + var managerId = int.Parse(Uri.UnescapeDataString(lastSegment.Substring(startIndex, endIndex - startIndex + 1))); + + // Find the order by ID + var manager = _dataSource.People?.OfType().SingleOrDefault(d => d.PersonId == managerId); + if (manager == null) + { + return NotFound(); + } + + var employee = _dataSource.People?.OfType().SingleOrDefault(c => c.PersonId == key); + if (employee == null) + { + return NotFound(); + } + + // Update the employee manager + employee.Manager = manager; + + return Ok(employee); + } + + [HttpPatch("odata/People/{key}")] + public IActionResult PatchPerson([FromRoute] int key, [FromBody] Delta patch) + { + var person = _dataSource.People?.SingleOrDefault(a => a.PersonId == key); + if (person == null) + { + return NotFound(); + } + patch.Patch(person); + return Updated(person); + } + + [HttpDelete("odata/People({key})/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.SpecialEmployee")] + public IActionResult DeleteSpecialEmployee([FromRoute] int key) + { + var person = _dataSource.People?.SingleOrDefault(a => a.PersonId == key); + if (person == null) + { + return NotFound(); + } + + _dataSource.People?.Remove(person); + return NoContent(); + } + + [HttpDelete("odata/Customers({key})/Orders/$ref")] + public IActionResult RemoveOrderRefFromCustomer([FromODataUri] int key) + { + string? id = Request.Query["$id"]; + var orderUri = id != null ? new Uri(id) : null; + + if (orderUri == null) + { + return BadRequest(); + } + + // Extract the ID from the URI + var lastSegment = orderUri.Segments.Last(); + int startIndex = lastSegment.IndexOf('(') + 1; + int endIndex = lastSegment.IndexOf(')') - 1; + var orderId = int.Parse(Uri.UnescapeDataString(lastSegment.Substring(startIndex, endIndex - startIndex + 1))); + + // Find the order by ID + var order = _dataSource.Orders?.SingleOrDefault(d => d.OrderId == orderId); + if (order == null) + { + return NotFound(); + } + + // Remove the order reference from the customer + var customer = _dataSource.Customers?.SingleOrDefault(c => c.CustomerId == key); + if (customer == null) + { + return NotFound(); + } + + customer.Orders?.Remove(order); + return Ok(customer); + } + + [HttpPost("odata/keyassegmentupdatetests/Default.ResetDefaultDataSource")] + public IActionResult ResetDefaultDataSource() + { + _dataSource = CommonEndToEndDataSource.CreateInstance(); + + return Ok(); + } +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/ClientKeyAsSegmentTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/ClientKeyAsSegmentTests.cs new file mode 100644 index 0000000000..739e7f1dae --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/ClientKeyAsSegmentTests.cs @@ -0,0 +1,320 @@ +//--------------------------------------------------------------------- +// +// 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.Clients.EndToEnd; +using Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Default; +using Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd; +using Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Server; +using Xunit; +using Customer = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Customer; +using Employee = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Employee; +using Person = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Person; +using SpecialEmployee = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.SpecialEmployee; +using DiscontinuedProduct = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.DiscontinuedProduct; +using ProductPhoto = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.ProductPhoto; +using Order = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Order; + +namespace Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Tests; + +public class ClientKeyAsSegmentTests : EndToEndTestBase +{ + private readonly Uri _baseUri; + private readonly Container _context; + + public class TestsStartup : TestStartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(KeyAsSegmentTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", CommonEndToEndEdmModel.GetEdmModel())); + } + } + + public ClientKeyAsSegmentTests(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(); + } + + [Fact] + public void ClientChangesUrlConventionsBetweenQueries() + { + // Arrange & Act & Assert + var contextWrapper = this.CreateWrappedContext(); + + var query = contextWrapper.CreateQuery("Customers").OrderBy(c => c.CustomerId).ToList(); + Assert.Equal(10, query.Count); + + contextWrapper.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Parentheses; + var queryWithDefaultKeys = contextWrapper.CreateQuery("Customers").OrderBy(c => c.CustomerId).ToList(); + Assert.Equal(10, queryWithDefaultKeys.Count); + Assert.Equal(query, queryWithDefaultKeys); + + contextWrapper.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + query = contextWrapper.CreateQuery("Customers").OrderBy(c => c.CustomerId).ToList(); + Assert.Equal(10, query.Count); + Assert.Equal(query, queryWithDefaultKeys); + } + + [Fact] + public void GetSingleEntityWithKeyAsSegment() + { + // Arrange & Act + var contextWrapper = this.CreateWrappedContext(); + + var queryByKey = contextWrapper.CreateQuery("People").ByKey(-10); + var personByKey = queryByKey.GetValue(); + + var query = contextWrapper.CreateQuery("People").Where(p => p.PersonId == -10) as DataServiceQuery; + + // Assert + Assert.EndsWith("/odata/People/-10", queryByKey.RequestUri.OriginalString); + Assert.EndsWith("/odata/People?$filter=PersonId eq -10", query?.RequestUri.OriginalString); + Assert.NotNull(personByKey); + Assert.Equal(personByKey, query?.SingleOrDefault()); + + Assert.Equal("ぺソぞ弌タァ匚タぽひハ欲ぴほ匚せまたバボチマ匚ぁゾソチぁЯそぁミя暦畚ボ歹ひЯほダチそЯせぽゼポЯチaた歹たをタマせをせ匚ミタひぜ畚暦グクひほそたグせяチ匚ヲぺぁ", personByKey.Name); + Assert.Equal(4091, (personByKey as Employee)?.Salary); + Assert.Equal(-37730565, (personByKey as SpecialEmployee)?.Bonus); + } + + [Fact] + public void LinqQueryWithKeyUsingMethodSyntax() + { + // Arrange & Act + var contextWrapper = this.CreateWrappedContext(); + + var queryByKey = contextWrapper.People.ByKey(-10); + var personByKey = queryByKey.GetValue(); + + var query = contextWrapper.People.Where(p => p.PersonId == -10); + + // Assert + Assert.EndsWith("/odata/People/-10", queryByKey.RequestUri.OriginalString); + Assert.EndsWith("/odata/People?$filter=PersonId eq -10", query?.ToString()); + Assert.NotNull(personByKey); + Assert.Equal(personByKey, query?.SingleOrDefault()); + + Assert.Equal("ぺソぞ弌タァ匚タぽひハ欲ぴほ匚せまたバボチマ匚ぁゾソチぁЯそぁミя暦畚ボ歹ひЯほダチそЯせぽゼポЯチaた歹たをタマせをせ匚ミタひぜ畚暦グクひほそたグせяチ匚ヲぺぁ", personByKey.Name); + Assert.Equal(4091, (personByKey as Employee)?.Salary); + Assert.Equal(-37730565, (personByKey as SpecialEmployee)?.Bonus); + } + + [Fact] + public void LinqQueryWithNullStringInKey() + { + // Arrange + var contextWrapper = this.CreateWrappedContext(); + + var query = + from p in contextWrapper.People + where p.Name == "\0te\0st\0" + select p; + + // Act + var person = query.SingleOrDefault(); + + // Assert + Assert.Contains("/odata/People?$filter=Name eq '%00te%00st%00'", query.ToString()); + Assert.Null(person); + } + + [Fact] + public void LinqQueryWithKey() + { + // Arrange + var contextWrapper = this.CreateWrappedContext(); + + var query = + from p in contextWrapper.People + where p.PersonId == -10 + select p; + + // Act + var person = query.SingleOrDefault(); + + // Assert + Assert.Contains("/odata/People?$filter=PersonId eq -10", query.ToString()); + Assert.NotNull(person); + + Assert.Equal("ぺソぞ弌タァ匚タぽひハ欲ぴほ匚せまたバボチマ匚ぁゾソチぁЯそぁミя暦畚ボ歹ひЯほダチそЯせぽゼポЯチaた歹たをタマせをせ匚ミタひぜ畚暦グクひほそたグせяチ匚ヲぺぁ", person.Name); + Assert.Equal(4091, (person as Employee)?.Salary); + Assert.Equal(-37730565, (person as SpecialEmployee)?.Bonus); + } + + [Fact] + public void LinqQueryWithKeyAndOfType() + { + // Arrange + var contextWrapper = this.CreateWrappedContext(); + + var query = ( + from p in contextWrapper.People.OfType() + where p.PersonId == -6 + select p) as DataServiceQuery; + + // Act + var employee = query?.FirstOrDefault(); + + // Assert + Assert.IsType(employee); + Assert.EndsWith("odata/People/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee?$filter=PersonId eq -6", query?.ToString()); + Assert.NotNull(employee); + Assert.Equal("vnqfkvpolnxvurgxpfbfquqrqxqxknjykkuapsqcmbeuslhkqufultvr", employee.Name); + Assert.Equal(2147483647, employee.Salary); + } + + [Fact] + public void MultipleNavigationAndOfTypeInQuery() + { + // Arrange + var contextWrapper = this.CreateWrappedContext(); + + var query = ( + from product in contextWrapper.Products.OfType() + from related in product.RelatedProducts.OfType() + from photo in related.Photos + where + product.ProductId == -9 && + related.ProductId == -9 && + photo.PhotoId == -4 && + photo.ProductId == -4 + + select photo) as IQueryable; + + // Act + var photoResult = query?.SingleOrDefault(); + + // Assert + Assert.EndsWith( + "odata/Products/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct/-9/RelatedProducts/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct/-9/Photos?$filter=PhotoId eq -4 and ProductId eq -4", + query?.ToString()); + + Assert.NotNull(photoResult); + Assert.Equal(-4, photoResult.PhotoId); + Assert.Equal(-4, photoResult.ProductId); + } + + [Fact] + public void AttachToWithKeyAsSegment() + { + // Arrange + var contextWrapper = this.CreateWrappedContext(); + + var person = new Person { PersonId = -10 }; + + // Act + contextWrapper.AttachTo("Person", person); + + // Assert + Assert.Single(contextWrapper.Entities); + Assert.Equal(-10, (contextWrapper.Entities[0].Entity as Person)?.PersonId); + } + + [Fact] + public void AttachToAndLoadPropertyWithKeyAsSegment() + { + // Arrange + var contextWrapper = this.CreateWrappedContext(); + + var person = new Person { PersonId = -10 }; + + // Act + contextWrapper.AttachTo("People", person); + contextWrapper.LoadProperty(person, "PersonMetadata"); + + // Assert + Assert.Equal(3, person.PersonMetadata.Count); + Assert.Equal("ysjrkvxlmdiddnrpxvnizyqvsfurnvhiugqyukiyedbrzgpqlevdfeqainzoauyqvzkx", person.PersonMetadata.Where(p => p.PersonMetadataId == -7).Single().PropertyValue); + Assert.Equal("ァ亜ぽネソぽひァミa弌ゾダソポぼタ黑歹九ぁんЯンёゼミァ弌タ九ヲぞチポポЯぺzたダゾゾンミポチaタマぴ欲яネタЯ亜まaあ", person.PersonMetadata.Where(p => p.PersonMetadataId == -9).Single().PropertyValue); + Assert.Equal("lazcbjlydpauujlvßgszchoxhycaryzbmkuskiqfxyiu", person.PersonMetadata.Where(p => p.PersonMetadataId == -10).Single().PropertyValue); + } + + [Fact] + public void LoadPropertyWithNextLink() + { + // Arrange + var contextWrapper = this.CreateWrappedContext(); + + var response = contextWrapper.Customers.Expand(c => c.Orders).Execute() as QueryOperationResponse; + DataServiceQueryContinuation? customerContinuation = null; + + do + { + if (customerContinuation != null) + { + response = contextWrapper.Execute(customerContinuation); + } + + foreach (var customer in response) + { + DataServiceQueryContinuation orderContinuation = response.GetContinuation(customer.Orders); + + while (orderContinuation != null) + { + var ordersResponse = contextWrapper.LoadProperty(customer, "Orders", orderContinuation); + orderContinuation = ordersResponse.GetContinuation(); + } + } + + } while ((customerContinuation = response.GetContinuation()) != null); + } + + + [Fact] + public void ClientWithKeyAsSegmentSendsRequestsToServerWithoutKeyAsSegment() + { + // Arrange + var contextWrapper = this.CreateWrappedContext(); + + // Act + var queryable = contextWrapper.Orders.ByKey(0); + var exception = Record.Exception(() => queryable.GetValue()); + + // Assert + Assert.EndsWith("/Orders/0", queryable.RequestUri.OriginalString); + Assert.NotNull(exception.InnerException); + Assert.IsType(exception.InnerException); + } + + #region Private methods + + private Container CreateWrappedContext() + { + var context = new Container(_baseUri) + { + HttpClientFactory = HttpClientFactory, + UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash + }; + + return context; + } + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "keyassegmenttests/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/DollarSegmentTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/DollarSegmentTests.cs new file mode 100644 index 0000000000..cebe5d2354 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/DollarSegmentTests.cs @@ -0,0 +1,252 @@ +//--------------------------------------------------------------------- +// +// 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.Clients.EndToEnd.Default; +using Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd; +using Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Server; +using Microsoft.OData.Edm; +using Xunit; +using DiscontinuedProduct = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.DiscontinuedProduct; +using Employee = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Employee; +using Login = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Login; + +namespace Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Tests; + +public class DollarSegmentTests : 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(DollarSegmentTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", CommonEndToEndEdmModel.GetEdmModel())); + } + } + + public DollarSegmentTests(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 = CommonEndToEndEdmModel.GetEdmModel(); + ResetDefaultDataSource(); + } + + [Fact] + public void InsertEntityWithKeyValueSameAsNavigationPropertyName() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + + var newLogin = new Login { Username = "LastLogin", CustomerId = -289 }; + + // Act + _context.AddToLogin(newLogin); + _context.SaveChanges(); + + var loginQuery = _context.CreateQuery("Login").Where(l => l.Username == "LastLogin").ToArray(); + + // Assert + var queried = Assert.Single(loginQuery); + Assert.True(newLogin == queried, "Query result does not equal newly added login"); + Assert.Equal(-289, queried.CustomerId); + } + + [Fact] + public void ClientExecuteEntitySetDerivedType() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + var requestUri = new Uri(_baseUri + "Products/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct"); + + // Act + var discontinuedProductQuery = _context.Execute(requestUri).ToArray(); + + // Assert + Assert.Equal(5, discontinuedProductQuery.Length); + var discontinuedProduct = discontinuedProductQuery.Where(d => d.ProductId == -3).Single(); + Assert.Equal("ißuhmxavnmlsssssjssagmqjpchjußtkcoaldeyyduarovnxspzsskufxxfltußtxfhgjlksrn", discontinuedProduct.Description); + Assert.Equal(-1002345821, discontinuedProduct.ReplacementProductId); + Assert.Equal("そ歹ソボボをグ裹ぴポヲチ", discontinuedProduct.ChildConcurrencyToken); + } + + [Fact] + public void ClientExecuteEntitySetDerivedTypeDollarSegmentAtEnd() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + var requestUri = new Uri(_baseUri + "Products/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct/$"); + + // Act + var discontinuedProductQuery = _context.Execute(requestUri).ToArray(); + + // Assert + Assert.Equal(5, discontinuedProductQuery.Length); + var discontinuedProduct = discontinuedProductQuery.Where(d => d.ProductId == -3).Single(); + Assert.Equal("ißuhmxavnmlsssssjssagmqjpchjußtkcoaldeyyduarovnxspzsskufxxfltußtxfhgjlksrn", discontinuedProduct.Description); + Assert.Equal(-1002345821, discontinuedProduct.ReplacementProductId); + Assert.Equal("そ歹ソボボをグ裹ぴポヲチ", discontinuedProduct.ChildConcurrencyToken); + } + + [Fact] + public void ClientExecuteProjectPropertyDefinedOnDerivedType() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + var requestUri = new Uri(_baseUri + "Products/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct?$select=Discontinued"); + + // Act + var discontinuedProductDatesQuery = _context.Execute(requestUri).ToArray(); + + // Assert + Assert.Equal(5, discontinuedProductDatesQuery.Length); + Assert.Single(discontinuedProductDatesQuery.Where(d => d.Discontinued.ToString() == "7/28/2005 1:09:56 PM +00:00")); + } + + [Fact] + public void ClientExecuteProjectPropertyDefinedOnDerivedTypeMultipleDollarSegments() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + var requestUri = new Uri(_baseUri + "Products/$/$/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.DiscontinuedProduct?$select=Discontinued"); + + // Act + var discontinuedProductDatesQuery = _context.Execute(requestUri).ToArray(); + + // Assert + Assert.Equal(5, discontinuedProductDatesQuery.Length); + Assert.Single(discontinuedProductDatesQuery.Where(d => d.Discontinued.ToString() == "7/28/2005 1:09:56 PM +00:00")); + } + + [Fact] + public void InvokeFeedBoundActionDefinedOnDerivedType() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + long salaryBeforeIncrementForEmployee_6 = 2147483647; + + var requestUri = new Uri(_baseUri + "People/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/$/Default.IncreaseSalaries"); + + // Act + var response = _context.Execute( + requestUri, + "POST", + new OperationParameter[] + { + new BodyOperationParameter("n", 200), + }); + + // Assert + Assert.Equal(200, response.StatusCode); + var salaryAfterIncrement = _context.Execute(new Uri(_baseUri + "People/-6/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/$/Salary")); + Assert.Equal(salaryBeforeIncrementForEmployee_6, salaryAfterIncrement.Single() - 200); + + ResetDefaultDataSource(); + } + + [Fact] + public void InvokeEntryBoundActionDefinedOnDerivedType() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + var totalEmployeesBeforeSack = 7; + var requestUri = new Uri(_baseUri + "People/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/-10/$/Default.Sack"); + + // Act + var response = _context.Execute( + requestUri, + "POST", + new OperationParameter[] { }); + + // Assert + Assert.Equal(204, response.StatusCode); + + var totalEmployeesAfterSack = _context.Execute(new Uri(_baseUri + "People/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee")).Count(); + Assert.Equal(totalEmployeesBeforeSack, totalEmployeesAfterSack + 1); + + ResetDefaultDataSource(); + } + + [Fact] + public void InvokeActionDefinedOnDerivedTypeMultipleDollarSegments() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + long salaryBeforeIncrementForEmployee_6 = 2147483647; + var requestUri = new Uri(_baseUri + "People/$/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/$/$/Default.IncreaseSalaries"); + + // Act + var response = _context.Execute( + requestUri, + "POST", + new OperationParameter[] + { + new BodyOperationParameter("n", -200), + }); + + // Assert + Assert.Equal(200, response.StatusCode); + var salaryAfterIncrement = _context.Execute(new Uri(_baseUri + "People/-6/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/$/Salary")); + Assert.Equal(salaryBeforeIncrementForEmployee_6, salaryAfterIncrement.Single() + 200); + + ResetDefaultDataSource(); + } + + [Fact] + public void InvokeActionDefinedOnDerivedTypeDollarSegmentAtUriEnd() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + long salaryBeforeIncrementForEmployee_6 = 2147483647; + var requestUri = new Uri(_baseUri + "People/$/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/$/Default.IncreaseSalaries/$"); + + // Act + // Actions must be leaf segments, we do not allow anything to follow them + var response = _context.Execute( + requestUri, + "POST", + new OperationParameter[] + { + new BodyOperationParameter("n", -200), + }); + + // Assert + Assert.Equal(200, response.StatusCode); + var salaryAfterIncrement = _context.Execute(new Uri(_baseUri + "People/-6/Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd.Employee/$/Salary")); + Assert.Equal(salaryBeforeIncrementForEmployee_6, salaryAfterIncrement.Single() + 200); + + ResetDefaultDataSource(); + } + + #region Private methods + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "dollarsegmenttests/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/KeyAsSegmentTestHelper.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/KeyAsSegmentTestHelper.cs new file mode 100644 index 0000000000..ba6282f853 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/KeyAsSegmentTestHelper.cs @@ -0,0 +1,32 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Default; + +namespace Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Tests; + +public class KeyAsSegmentTestHelper +{ + private readonly Uri _baseUri; + private readonly Container _context; + + public KeyAsSegmentTestHelper(Uri serviceBaseUri, Container context) + { + this._baseUri = serviceBaseUri; + _context = context; + } + + public Container CreateWrappedContext() + { + var context = new Container(this._baseUri) + { + HttpClientFactory = _context.HttpClientFactory, + UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash + }; + + return context; + } +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/LiteralFormatTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/LiteralFormatTests.cs new file mode 100644 index 0000000000..6b61f16637 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/LiteralFormatTests.cs @@ -0,0 +1,163 @@ +//--------------------------------------------------------------------- +// +// 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.Clients.EndToEnd.Default; +using Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd; +using Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Server; +using Microsoft.OData.Edm; +using Xunit; +using Login = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Login; +using Customer = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Customer; + +namespace Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Tests; + +public class LiteralFormatTests : 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(LiteralFormatTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", CommonEndToEndEdmModel.GetEdmModel())); + } + } + + public LiteralFormatTests(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 = CommonEndToEndEdmModel.GetEdmModel(); + ResetDefaultDataSource(); + } + + [Theory] + [InlineData("$var1")] + [InlineData("$$")] + [InlineData("$$$")] + [InlineData("$$$$")] + [InlineData("$")] + [InlineData("$orderby")] + [InlineData("$filter")] + [InlineData("$format")] + [InlineData("$top")] + [InlineData("$count")] + [InlineData("$expand")] + [InlineData("$select")] + public void PrimaryKeyValueBeginsWithDollarSign(string dollarSignKeyValue) + { + ResetDefaultDataSource(); + + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + _context.MergeOption = MergeOption.PreserveChanges; + + var customer = _context.Customers.Take(1).Single(); + + var newLogin = new Login { Username = dollarSignKeyValue }; + + _context.AddToLogin(newLogin); + _context.SetLink(newLogin, "Customer", customer); + _context.AddLink(customer, "Logins", newLogin); + _context.SaveChanges(); + + // Act & Assert + var loginQuery = _context.CreateQuery("Login").Where(l => l.Username == dollarSignKeyValue).ToArray(); + Assert.True(newLogin == loginQuery.Single(), "Query result does not equal newly added login with key " + dollarSignKeyValue); + Assert.Equal(dollarSignKeyValue, loginQuery[0].Username); + + var customerQuery = _context.Execute(new Uri(_baseUri + "Login/" + dollarSignKeyValue + "/Customer")).ToArray(); + Assert.True(customer == customerQuery.Single(), "Execute query result does not equal associated customer"); + } + + [Theory] + [InlineData(" /")] + [InlineData("/ ")] + [InlineData("var1/baz")] + [InlineData("//var1")] + [InlineData("var1//")] + public void PrimaryKeyValueContainsForwardSlash(string keyValue) + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + _context.MergeOption = MergeOption.PreserveChanges; + + var customer = _context.Customers.Take(1).Single(); + + var newLogin = new Login { Username = keyValue }; + + // Act + _context.AddToLogin(newLogin); + _context.SetLink(newLogin, "Customer", customer); + _context.AddLink(customer, "Logins", newLogin); + _context.SaveChanges(); + + var loginQuery = _context.CreateQuery("Login").Where(l => l.Username == keyValue).ToArray(); + + // Assert + Assert.Single(loginQuery); + Assert.Equal(keyValue, loginQuery[0].Username); + + ResetDefaultDataSource(); + } + + [Theory] + [InlineData("var1 baz")] + [InlineData(" var1")] + public void PrimaryKeyValueContainsWhitespace(string keyValue) + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + _context.MergeOption = MergeOption.PreserveChanges; + + var customer = _context.Customers.Take(1).Single(); + + var newLogin = new Login { Username = keyValue }; + + // Act + _context.AddToLogin(newLogin); + _context.SetLink(newLogin, "Customer", customer); + _context.AddLink(customer, "Logins", newLogin); + _context.SaveChanges(); + + var loginQuery = _context.CreateQuery("Login").Where(l => l.Username == keyValue).ToArray(); + Assert.True(newLogin == loginQuery.Single(), "Query result does not equal newly added login with key " + keyValue); + + var customerQuery = _context.Execute(new Uri(_baseUri + "Login/" + keyValue + "/Customer")).ToArray(); + Assert.True(customer == customerQuery.Single(), "Execute query result does not equal associated customer"); + + ResetDefaultDataSource(); + } + + #region Private methods + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "keyasssegmentliteralformattests/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/StreamTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/StreamTests.cs new file mode 100644 index 0000000000..6a04c6d26f --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/StreamTests.cs @@ -0,0 +1,167 @@ +//--------------------------------------------------------------------- +// +// 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.Clients.EndToEnd; +using Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Default; +using Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd; +using Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Server; +using Microsoft.OData.Edm; +using Xunit; +using Car = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Car; +using CustomerInfo = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.CustomerInfo; + +namespace Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Tests; + +public class StreamTests : 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(KeyAsSegmentTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", CommonEndToEndEdmModel.GetEdmModel())); + } + } + + public StreamTests(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 = CommonEndToEndEdmModel.GetEdmModel(); + ResetDefaultDataSource(); + } + + [Fact] + public void GetReadStreamFromMle() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + var customerInfoQuery = _context.CreateQuery("CustomerInfos"); + + foreach (var customerInfo in customerInfoQuery) + { + var streamResponse = _context.GetReadStream(customerInfo); + Assert.NotNull(streamResponse); + using (var reader = new StreamReader(streamResponse.Stream)) + { + var response = reader.ReadToEnd(); + Assert.NotNull(response); + } + } + } + + [Fact] + public void GetReadStreamUriFromMle() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + + var customerInfoQuery = _context.CreateQuery("CustomerInfos"); + + foreach (var customerInfo in customerInfoQuery) + { + var streamResponseUri = _context.GetReadStreamUri(customerInfo); + Assert.NotNull(streamResponseUri); + + var query = _context.CreateQuery("CustomerInfos").ByKey(customerInfo.CustomerInfoId); + + Assert.False(query.RequestUri.AbsoluteUri.Contains('('), "Uri contains left parentheses"); + Assert.False(query.RequestUri.AbsoluteUri.Contains(')'), "Uri contains right parentheses"); + Assert.EndsWith($"odata/CustomerInfos/{customerInfo.CustomerInfoId}", query.RequestUri.AbsoluteUri); + } + } + + [Fact] + public void GetReadStreamFromNamedStreamProperty() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + + var firstCar = _context.CreateQuery("Cars").Take(1).Single(); + + var streamResponse = _context.GetReadStream(firstCar, "Photo", new DataServiceRequestArgs()); + + // VerifyStreamReadable + using (var reader = new StreamReader(streamResponse.Stream)) + { + var photo = reader.ReadToEnd(); + Assert.NotNull(photo); + } + } + + [Fact] + public void SetSaveStreamOnMle() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + + byte[] binaryTestData = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; + var memoryStream = new MemoryStream(binaryTestData); + + var customerInfo = _context.CustomerInfos.Take(1).Single(); + + // Act + _context.SetSaveStream(customerInfo, memoryStream, true, "application/binary", "var1"); + var dataServiceResponse = _context.SaveChanges(); + var response = dataServiceResponse.SingleOrDefault() as ChangeOperationResponse; + + // Assert + Assert.NotNull(response); + Assert.Equal(204, response.StatusCode); + } + + [Fact] + public void SetSaveStreamOnNamedStreamProperty() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + + byte[] binaryTestData = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; + var memoryStream = new MemoryStream(binaryTestData); + + // Act + var firstCar = _context.CreateQuery("Cars").Take(1).Single(); + + _context.SetSaveStream(firstCar, "Video", memoryStream, true, new DataServiceRequestArgs { ContentType = "application/binary" }); + var dataServiceResponse = _context.SaveChanges(); + var response = dataServiceResponse.SingleOrDefault() as ChangeOperationResponse; + + // Assert + Assert.NotNull(response); + Assert.Equal(204, response.StatusCode); + Assert.IsType(response.Descriptor); + } + + #region Private methods + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "keyassegmenttests/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/UpdateTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/UpdateTests.cs new file mode 100644 index 0000000000..096e653691 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/KeyAsSegmentTests/Tests/UpdateTests.cs @@ -0,0 +1,249 @@ +//--------------------------------------------------------------------- +// +// 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.Batch; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Client.E2E.TestCommon; +using Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd; +using Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Default; +using Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd; +using Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Server; +using Microsoft.OData.Edm; +using Xunit; +using Customer = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Customer; +using Employee = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Employee; +using Order = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Order; +using Person = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.Person; +using SpecialEmployee = Microsoft.OData.Client.E2E.Tests.Common.Clients.EndToEnd.SpecialEmployee; + +namespace Microsoft.OData.Client.E2E.Tests.KeyAsSegmentTests.Tests; + +public class UpdateTests : 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(UpdateTestsController), typeof(MetadataController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", CommonEndToEndEdmModel.GetEdmModel(), batchHandler: new DefaultODataBatchHandler())); + } + } + + public UpdateTests(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 = CommonEndToEndEdmModel.GetEdmModel(); + ResetDefaultDataSource(); + } + + [Fact] + public void InsertSave() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + var newPerson = new Person { PersonId = 9999, Name = "Some Person" }; + var personQuery = _context.People.Where(p => p.PersonId == newPerson.PersonId); + + _context.IgnoreResourceNotFoundException = true; + + // Act & Assert + var retrievedPerson = personQuery.SingleOrDefault(); + Assert.Null(retrievedPerson); + + _context.AddObject("People", newPerson); + _context.SaveChanges(); + + retrievedPerson = personQuery.SingleOrDefault(); + Assert.NotNull(retrievedPerson); + Assert.True(newPerson == retrievedPerson, "New entity and retrieved entity should reference same object"); + } + + [Fact] + public void AttachUpdateObjectSave() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + var specialEmployee = new SpecialEmployee { PersonId = -10 }; + _context.AttachTo("People", specialEmployee); + + specialEmployee.Bonus = Int32.MaxValue; + _context.UpdateObject(specialEmployee); + _context.SaveChanges(); + + var retrievedPerson = _context.People.ByKey(specialEmployee.PersonId).GetValue(); + var retrievedBonus = (retrievedPerson as SpecialEmployee)?.Bonus; + Assert.Equal(Int32.MaxValue, retrievedBonus); + } + + [Fact] + public void AddObjectSave() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + var specialEmployee = new SpecialEmployee { PersonId = 1234 }; + + // Act & Assert + _context.AddObject("People", specialEmployee); + var savedResponse = _context.SaveChanges(); + Assert.Equal(201, (savedResponse.Single() as ChangeOperationResponse)?.StatusCode); + Assert.NotNull(_context.People.ByKey(1234).GetValue()); + + _context.DeleteObject(specialEmployee); + var deletedResponse = _context.SaveChanges(); + Assert.Equal(204, (deletedResponse.Single() as ChangeOperationResponse)?.StatusCode); + var exception = Record.Exception(() => _context.People.ByKey(1234).GetValue()); + Assert.NotNull(exception); + Assert.Equal("NotFound", exception.InnerException?.Message); + } + + [Fact] + public void AddObjectAddRelatedObjectSave() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + _context.MergeOption = MergeOption.PreserveChanges; + + var customer = new Customer { CustomerId = 1234 }; + var order = new Order { OrderId = 12345 }; + + // Act + _context.AddObject("Customers", customer); + _context.AddRelatedObject(customer, "Orders", order); + var savedChangesResponse = _context.SaveChanges(); + + // Assert + Assert.True(savedChangesResponse.All(s => s.StatusCode == 201)); + var customerSaved = _context.Customers.ByKey(1234).Expand(c => c.Orders).GetValue(); + Assert.NotNull(customerSaved); + Assert.Equal(1234, customerSaved.CustomerId); + Assert.Contains(customerSaved.Orders, o => o.OrderId == 12345); + } + + [Fact] + public void AddDeleteLinkSave() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + _context.MergeOption = MergeOption.PreserveChanges; + + var customer = _context.Customers.Expand(c => c.Orders).First(); + var order = _context.Orders.ToArray().Except(customer.Orders).First(); + + // Act & Assert + _context.AddLink(customer, "Orders", order); + _context.SaveChanges(); + + // Order exists in customer's orders + var customerWithOrders = _context.Customers.ByKey(customer.CustomerId).Expand(c => c.Orders).GetValue(); + Assert.Contains(customerWithOrders.Orders, o => o.OrderId == order.OrderId); + + _context.DeleteLink(customer, "Orders", order); + _context.SaveChanges(); + + // Order does not exist in customer's orders + var customerWithoutOrders = _context.Customers.ByKey(customer.CustomerId).Expand(c => c.Orders).GetValue(); + Assert.DoesNotContain(customerWithoutOrders.Orders, o => o.OrderId == order.OrderId); + } + + [Fact] + public void AddDeleteLinkSaveBatch() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + _context.MergeOption = MergeOption.PreserveChanges; + + var customer = _context.Customers.Expand(c => c.Orders).First(); + var order1 = _context.Orders.ToArray().Except(customer.Orders).First(); + var order2 = _context.Orders.OrderByDescending(o => o.OrderId).ToArray().Except(customer.Orders).First(); + + // Act & Assert + _context.AddLink(customer, "Orders", order1); + _context.AddLink(customer, "Orders", order2); + + _context.SaveChanges(SaveChangesOptions.BatchWithSingleChangeset); + + // Order exists in customer's orders + var customerWithoutOrders = _context.Customers.ByKey(customer.CustomerId).Expand(c => c.Orders).GetValue(); + Assert.Contains(customerWithoutOrders.Orders, o => o.OrderId == order1.OrderId || o.OrderId == order2.OrderId); + + _context.DeleteLink(customer, "Orders", order1); + _context.DeleteLink(customer, "Orders", order2); + _context.SaveChanges(SaveChangesOptions.BatchWithSingleChangeset); + + // Order does not exist in customer's orders + var customerWithOrders = _context.Customers.ByKey(customer.CustomerId).Expand(c => c.Orders).GetValue(); + Assert.DoesNotContain(customerWithOrders.Orders, o => o.OrderId == order1.OrderId || o.OrderId == order2.OrderId); + } + + [Fact] + public void SetLinkSave() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + _context.MergeOption = MergeOption.PreserveChanges; + + // Act & Assert + var employee = _context.People.OfType().First(); + + _context.SetLink(employee, "Manager", employee); + _context.SaveChanges(); + + var employeeWithManager = (_context.People.OfType().Where(s => s.PersonId == employee.PersonId) as DataServiceQuery)?.Expand(e => e.Manager).Single(); + Assert.NotNull(employeeWithManager?.Manager); + Assert.Equal(employee.PersonId, employeeWithManager.Manager.PersonId); + } + + [Fact] + public void AddRelatedObjectSave() + { + // Arrange + _context.UrlKeyDelimiter = DataServiceUrlKeyDelimiter.Slash; + _context.MergeOption = MergeOption.PreserveChanges; + + // Act + var customer = _context.Customers.First(); + var order = new Order { OrderId = 1234 }; + + _context.AddRelatedObject(customer, "Orders", order); + var savedChangeResponse = _context.SaveChanges(); + + // Assert + var response = savedChangeResponse.Single() as ChangeOperationResponse; + Assert.Equal(201, response?.StatusCode); + + var entity = response?.Descriptor as EntityDescriptor; + Assert.EndsWith("/Orders(1234)", entity?.EditLink.OriginalString); + } + + #region Private methods + + private void ResetDefaultDataSource() + { + var actionUri = new Uri(_baseUri + "keyassegmentupdatetests/Default.ResetDefaultDataSource", UriKind.Absolute); + _context.Execute(actionUri, "POST"); + } + + #endregion +}